From 2053340c3a9dacb65c3c09fb3d855b7e52d4cb3c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 15 May 2026 14:12:05 -0700 Subject: [PATCH 1/2] feat(recorder): inspector-driven dark mode for codegen overlay The injected codegen overlay (toolbar, tooltip, action dialog, assert-text editor) now follows the inspector's Theme setting (Auto / Dark mode / Light mode). Plumbing: inspector setting -> `useDocumentTheme()` -> `backend.setOverlayTheme` -> server `Recorder._overlayTheme` -> `UIState.overlayTheme` (polled) -> injected `Highlight.setOverlayTheme`, which toggles `:host(.dark-mode)` on the glass pane. "Auto" is resolved in the inspector against its own `prefers-color-scheme`; the wire protocol only carries concrete `dark-mode` / `light-mode`. Builds on #40863. --- packages/injected/src/highlight.css | 58 +++++++++++++++++++ packages/injected/src/highlight.ts | 10 ++++ packages/injected/src/recorder/recorder.ts | 1 + .../playwright-core/src/server/recorder.ts | 8 ++- .../src/server/recorder/recorderApp.ts | 5 +- packages/recorder/src/recorder.tsx | 7 ++- packages/recorder/src/recorderTypes.d.ts | 4 ++ packages/web/src/theme.ts | 11 +++- 8 files changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/injected/src/highlight.css b/packages/injected/src/highlight.css index 54dc68ceb31c9..b60521d2e842c 100644 --- a/packages/injected/src/highlight.css +++ b/packages/injected/src/highlight.css @@ -365,3 +365,61 @@ x-pw-action-item:last-child { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; } + +:host(.dark-mode) { + color: #cccccc; + color-scheme: dark; +} + +:host(.dark-mode) x-pw-tooltip, +:host(.dark-mode) x-pw-dialog { + background-color: #252526; + box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.6); +} + +:host(.dark-mode) x-pw-tooltip-footer { + color: rgba(204, 204, 204, 0.7); +} + +:host(.dark-mode) x-pw-tools-list { + border-bottom: 1px solid #3c3c3c; +} + +:host(.dark-mode) x-pw-overlay x-pw-tools-list { + background-color: #252526dd; + box-shadow: rgba(0, 0, 0, 0.4) 0px 5px 5px; +} + +:host(.dark-mode) x-pw-tool-item:not(.disabled):hover, +:host(.dark-mode) x-pw-tool-item.record.toggled:not(.disabled):hover { + background-color: hsl(0, 0%, 25%); +} + +:host(.dark-mode) x-pw-tool-item > x-div { + background-color: #c5c5c5; +} + +:host(.dark-mode) x-pw-tool-item.disabled > x-div { + background-color: rgba(197, 197, 197, 0.4); +} + +:host(.dark-mode) x-pw-tool-gripper > x-div { + background-color: #969696; +} + +:host(.dark-mode) x-pw-tool-item.record.toggled > x-div { + background-color: #f48771; +} + +:host(.dark-mode) textarea.text-editor { + color: #cccccc; + background-color: transparent; +} + +:host(.dark-mode) x-locator-editor { + border-bottom: 1px solid #3c3c3c; +} + +:host(.dark-mode) x-pw-action-item:hover { + background-color: hsl(0, 0%, 20%); +} diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index e1c927bc62dfe..3f10973e6182c 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -50,6 +50,8 @@ export type HighlightEntry = { cssStyle?: string, }; +type OverlayTheme = 'dark-mode' | 'light-mode'; + export class Highlight { private _glassPaneElement: HTMLElement; private _glassPaneShadow: ShadowRoot; @@ -64,6 +66,7 @@ export class Highlight { private _rafRequest: number | undefined; private _language: Language = 'javascript'; private _elementHighlightSelectors = new Map(); + private _overlayTheme: OverlayTheme = 'light-mode'; constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; @@ -106,6 +109,13 @@ export class Highlight { this._glassPaneShadow.appendChild(this._userOverlayContainer); } + setOverlayTheme(theme: OverlayTheme) { + if (this._overlayTheme === theme) + return; + this._overlayTheme = theme; + this._glassPaneElement.classList.toggle('dark-mode', theme === 'dark-mode'); + } + install() { // NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078 if (!this._injectedScript.document.documentElement) diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 7b4c98f59ba99..32f009b830691 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -1488,6 +1488,7 @@ export class Recorder { this.state = state; this.highlight.setLanguage(state.language); + this.highlight.setOverlayTheme(state.overlayTheme ?? 'light-mode'); this._switchCurrentTool(); this.overlay?.setUIState(state); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index e67f56ac505c3..b3cc7fb0054ce 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -41,7 +41,7 @@ import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; import type { Progress } from './progress'; import type * as channels from '@protocol/channels'; import type * as actions from '@recorder/actions'; -import type { CallLog, CallLogStatus, ElementInfo, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; +import type { CallLog, CallLogStatus, ElementInfo, Mode, OverlayState, OverlayTheme, Source, UIState } from '@recorder/recorderTypes'; import type { RegisteredListener } from '@utils/eventsHelper'; const recorderSymbol = Symbol('recorderSymbol'); @@ -80,6 +80,7 @@ export class Recorder extends EventEmitter implements Instrume private _mode: Mode; private _highlightedElement: { selector?: string, ariaTemplate?: AriaTemplateNode } = {}; private _overlayState: OverlayState = { offsetX: 0 }; + private _overlayTheme: OverlayTheme = 'light-mode'; private _currentCallsMetadata = new Map(); private _actionPoints = new Map(); private _userSources = new Map(); @@ -192,6 +193,7 @@ export class Recorder extends EventEmitter implements Instrume language: this._currentLanguage, testIdAttributeName: this._testIdAttributeName(), overlay: this._overlayState, + overlayTheme: this._overlayTheme, }; return uiState; }); @@ -257,6 +259,10 @@ export class Recorder extends EventEmitter implements Instrume return this._mode; } + setOverlayTheme(theme: OverlayTheme) { + this._overlayTheme = theme; + } + async setMode(mode: Mode) { if (this._mode === mode) return; diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index f85f69574138a..af4ace9b02c10 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -32,7 +32,7 @@ import { BrowserContext } from '../browserContext'; import type { Page } from '../page'; import type * as actions from '@recorder/actions'; -import type { CallLog, ElementInfo, Mode, RecorderBackend, RecorderFrontend, Source } from '@recorder/recorderTypes'; +import type { CallLog, ElementInfo, Mode, OverlayTheme, RecorderBackend, RecorderFrontend, Source } from '@recorder/recorderTypes'; import type { Language, LanguageGeneratorOptions } from '../codegen/types'; import type * as channels from '@protocol/channels'; import type { Progress } from '../progress'; @@ -150,6 +150,9 @@ export class RecorderApp { this._languageGeneratorOptions.generateAutoExpect = params.autoExpect; this._updateActions(); }, + setOverlayTheme: async (params: { theme: OverlayTheme }) => { + this._recorder.setOverlayTheme(params.theme); + }, setMode: async (params: { mode: Mode }) => { await this._recorder.setMode(params.mode); }, diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 548649d65bb4b..42a4e05443557 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import { CallLogView } from './callLog'; import './recorder.css'; import { asLocator } from '@isomorphic/locatorGenerators'; -import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme'; +import { kThemeOptions, type Theme, useDocumentTheme, useThemeSetting } from '@web/theme'; import { copy, useSetting } from '@web/uiUtils'; import yaml from 'yaml'; import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot'; @@ -45,6 +45,7 @@ export const Recorder: React.FC = ({}) => { const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState(); const [settingsOpen, setSettingsOpen] = React.useState(false); const [theme, setTheme] = useThemeSetting(); + const documentTheme = useDocumentTheme(); const [autoExpect, setAutoExpect] = useSetting('autoExpect', false); const settingsButtonRef = React.useRef(null); const backend = React.useMemo(createRecorderBackend, []); @@ -104,6 +105,10 @@ export const Recorder: React.FC = ({}) => { backend.setAutoExpect({ autoExpect }); }, [autoExpect, backend]); + React.useEffect(() => { + backend.setOverlayTheme({ theme: documentTheme }); + }, [documentTheme, backend]); + React.useLayoutEffect(() => { messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' }); }, [messagesEndRef]); diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts index 512905f754e99..16e97fc117850 100644 --- a/packages/recorder/src/recorderTypes.d.ts +++ b/packages/recorder/src/recorderTypes.d.ts @@ -51,6 +51,8 @@ export type OverlayState = { offsetX: number; }; +export type OverlayTheme = 'dark-mode' | 'light-mode'; + export type UIState = { mode: Mode; actionPoint?: Point; @@ -59,6 +61,7 @@ export type UIState = { language: Language; testIdAttributeName: string; overlay: OverlayState; + overlayTheme?: OverlayTheme; }; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; @@ -108,6 +111,7 @@ declare global { export interface RecorderBackend { setMode(params: { mode: Mode }): Promise; setAutoExpect(params: { autoExpect: boolean }): Promise; + setOverlayTheme(params: { theme: OverlayTheme }): Promise; resume(): Promise; pause(): Promise; step(): Promise; diff --git a/packages/web/src/theme.ts b/packages/web/src/theme.ts index 5d175ca3bd416..c72db688b8fd0 100644 --- a/packages/web/src/theme.ts +++ b/packages/web/src/theme.ts @@ -24,7 +24,7 @@ declare global { } } -type DocumentTheme = 'dark-mode' | 'light-mode'; +export type DocumentTheme = 'dark-mode' | 'light-mode'; export type Theme = DocumentTheme | 'system'; const kDefaultTheme: Theme = 'system'; @@ -103,3 +103,12 @@ export function useThemeSetting(): [Theme, (value: Theme) => void] { return [theme, setTheme]; } + +export function useDocumentTheme(): DocumentTheme { + const [theme, setTheme] = React.useState(() => currentDocumentTheme() ?? 'light-mode'); + React.useEffect(() => { + addThemeListener(setTheme); + return () => removeThemeListener(setTheme); + }, []); + return theme; +} From d5e71bd7c5d1ea2c4abcfc040e02c830a2b22fff Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 18 May 2026 10:27:55 -0700 Subject: [PATCH 2/2] chore(recorder): address review comments - Fold `theme` into `OverlayState`; drop the separate `_overlayTheme` field and `UIState.overlayTheme`. The page->server overlay-state binding now sends only the page-owned `offsetX` and the server merges it into the full state, so the theme isn't clobbered. - Combine `useDocumentTheme` into `useThemeSetting` so callers get `[theme, documentTheme, setTheme]` from a single hook. --- packages/dashboard/src/settingsView.tsx | 2 +- packages/html-reporter/src/headerView.tsx | 2 +- packages/injected/src/recorder/pollingRecorder.ts | 6 +++--- packages/injected/src/recorder/recorder.ts | 8 ++++---- packages/playwright-core/src/server/recorder.ts | 10 ++++------ packages/recorder/src/recorder.tsx | 5 ++--- packages/recorder/src/recorderTypes.d.ts | 6 +++--- .../trace-viewer/src/ui/defaultSettingsView.tsx | 2 +- packages/trace-viewer/src/ui/snapshotTab.tsx | 2 +- packages/web/src/theme.ts | 15 ++++++--------- 10 files changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/dashboard/src/settingsView.tsx b/packages/dashboard/src/settingsView.tsx index 21335b38d91f0..73b9e3caa4ec7 100644 --- a/packages/dashboard/src/settingsView.tsx +++ b/packages/dashboard/src/settingsView.tsx @@ -22,7 +22,7 @@ import { GearIcon } from './icons'; export const SettingsButton: React.FC = () => { const [open, setOpen] = React.useState(false); - const [theme, setTheme] = useThemeSetting(); + const [theme, , setTheme] = useThemeSetting(); const containerRef = React.useRef(null); React.useEffect(() => { diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index ab456e86ca75e..e34efd2e48e58 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -131,7 +131,7 @@ const NavLink: React.FC<{ const SettingsButton: React.FC = () => { const settingsRef = React.useRef(null); const [settingsOpen, setSettingsOpen] = React.useState(false); - const [theme, setTheme] = useThemeSetting(); + const [theme, , setTheme] = useThemeSetting(); const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false); return <> diff --git a/packages/injected/src/recorder/pollingRecorder.ts b/packages/injected/src/recorder/pollingRecorder.ts index 55dd2f0ea42e4..8af742473b2eb 100644 --- a/packages/injected/src/recorder/pollingRecorder.ts +++ b/packages/injected/src/recorder/pollingRecorder.ts @@ -19,7 +19,7 @@ import { Recorder } from './recorder'; import type { InjectedScript } from '../injectedScript'; import type { RecorderDelegate } from './recorder'; import type * as actions from '@recorder/actions'; -import type { ElementInfo, Mode, OverlayState, UIState } from '@recorder/recorderTypes'; +import type { ElementInfo, Mode, UIState } from '@recorder/recorderTypes'; interface Embedder { __pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise; @@ -27,7 +27,7 @@ interface Embedder { __pw_recorderState(): Promise; __pw_recorderElementPicked(element: { selector: string, ariaSnapshot?: string }): Promise; __pw_recorderSetMode(mode: Mode): Promise; - __pw_recorderSetOverlayState(state: OverlayState): Promise; + __pw_recorderSetOverlayState(state: { offsetX: number }): Promise; __pw_refreshOverlay(): void; } @@ -92,7 +92,7 @@ export class PollingRecorder implements RecorderDelegate { await this._embedder.__pw_recorderSetMode(mode); } - async setOverlayState(state: OverlayState): Promise { + async setOverlayState(state: { offsetX: number }): Promise { await this._embedder.__pw_recorderSetOverlayState(state); } } diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 32f009b830691..efdf03aa16b0c 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -24,7 +24,7 @@ import type { Highlight, HighlightEntry } from '../highlight'; import type { InjectedScript } from '../injectedScript'; import type { ElementText } from '../selectorUtils'; import type * as actions from '@recorder/actions'; -import type { ElementInfo, Mode, OverlayState, UIState } from '@recorder/recorderTypes'; +import type { ElementInfo, Mode, UIState } from '@recorder/recorderTypes'; import type { Language } from '@isomorphic/locatorGenerators'; const HighlightColors = { @@ -39,7 +39,7 @@ export interface RecorderDelegate { recordAction?(action: actions.Action): Promise; elementPicked?(elementInfo: ElementInfo): Promise; setMode?(mode: Mode): Promise; - setOverlayState?(state: OverlayState): Promise; + setOverlayState?(state: { offsetX: number }): Promise; highlightUpdated?(): void; } @@ -1383,7 +1383,7 @@ export class Recorder { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', - overlay: { offsetX: 0 }, + overlay: { offsetX: 0, theme: 'light-mode' }, }; readonly document: Document; private _delegate: RecorderDelegate = {}; @@ -1488,7 +1488,7 @@ export class Recorder { this.state = state; this.highlight.setLanguage(state.language); - this.highlight.setOverlayTheme(state.overlayTheme ?? 'light-mode'); + this.highlight.setOverlayTheme(state.overlay.theme); this._switchCurrentTool(); this.overlay?.setUIState(state); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index b3cc7fb0054ce..1f5a7ed66b8af 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -79,8 +79,7 @@ export class Recorder extends EventEmitter implements Instrume private _params: RecorderParams; private _mode: Mode; private _highlightedElement: { selector?: string, ariaTemplate?: AriaTemplateNode } = {}; - private _overlayState: OverlayState = { offsetX: 0 }; - private _overlayTheme: OverlayTheme = 'light-mode'; + private _overlayState: OverlayState = { offsetX: 0, theme: 'light-mode' }; private _currentCallsMetadata = new Map(); private _actionPoints = new Map(); private _userSources = new Map(); @@ -193,7 +192,6 @@ export class Recorder extends EventEmitter implements Instrume language: this._currentLanguage, testIdAttributeName: this._testIdAttributeName(), overlay: this._overlayState, - overlayTheme: this._overlayTheme, }; return uiState; }); @@ -209,10 +207,10 @@ export class Recorder extends EventEmitter implements Instrume await this.setMode(mode); }); - await this._context.exposeBinding(progress, '__pw_recorderSetOverlayState', async ({ frame }, state: OverlayState) => { + await this._context.exposeBinding(progress, '__pw_recorderSetOverlayState', async ({ frame }, state: { offsetX: number }) => { if (frame.parentFrame()) return; - this._overlayState = state; + this._overlayState = { ...this._overlayState, ...state }; }); await this._context.exposeBinding(progress, '__pw_resume', () => { @@ -260,7 +258,7 @@ export class Recorder extends EventEmitter implements Instrume } setOverlayTheme(theme: OverlayTheme) { - this._overlayTheme = theme; + this._overlayState = { ...this._overlayState, theme }; } async setMode(mode: Mode) { diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 42a4e05443557..b11460ca8e9c4 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import { CallLogView } from './callLog'; import './recorder.css'; import { asLocator } from '@isomorphic/locatorGenerators'; -import { kThemeOptions, type Theme, useDocumentTheme, useThemeSetting } from '@web/theme'; +import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme'; import { copy, useSetting } from '@web/uiUtils'; import yaml from 'yaml'; import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot'; @@ -44,8 +44,7 @@ export const Recorder: React.FC = ({}) => { const [ariaSnapshot, setAriaSnapshot] = React.useState(); const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState(); const [settingsOpen, setSettingsOpen] = React.useState(false); - const [theme, setTheme] = useThemeSetting(); - const documentTheme = useDocumentTheme(); + const [theme, documentTheme, setTheme] = useThemeSetting(); const [autoExpect, setAutoExpect] = useSetting('autoExpect', false); const settingsButtonRef = React.useRef(null); const backend = React.useMemo(createRecorderBackend, []); diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts index 16e97fc117850..66c0588c8349c 100644 --- a/packages/recorder/src/recorderTypes.d.ts +++ b/packages/recorder/src/recorderTypes.d.ts @@ -47,12 +47,13 @@ export type EventData = { params: any; }; +export type OverlayTheme = 'dark-mode' | 'light-mode'; + export type OverlayState = { offsetX: number; + theme: OverlayTheme; }; -export type OverlayTheme = 'dark-mode' | 'light-mode'; - export type UIState = { mode: Mode; actionPoint?: Point; @@ -61,7 +62,6 @@ export type UIState = { language: Language; testIdAttributeName: string; overlay: OverlayState; - overlayTheme?: OverlayTheme; }; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; diff --git a/packages/trace-viewer/src/ui/defaultSettingsView.tsx b/packages/trace-viewer/src/ui/defaultSettingsView.tsx index c0d8b5fbcd11e..a8617d28496b9 100644 --- a/packages/trace-viewer/src/ui/defaultSettingsView.tsx +++ b/packages/trace-viewer/src/ui/defaultSettingsView.tsx @@ -29,7 +29,7 @@ export const DefaultSettingsView: React.FC<{ shouldPopulateCanvasFromScreenshot, setShouldPopulateCanvasFromScreenshot, ] = useSetting('shouldPopulateCanvasFromScreenshot', false); - const [theme, setTheme] = useThemeSetting(); + const [theme, , setTheme] = useThemeSetting(); const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false); return ( diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 4a9d4846dda85..88bd0c1e8784e 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -279,7 +279,7 @@ export const InspectModeController: React.FunctionComponent<{ ariaTemplate, language: sdkLanguage, testIdAttributeName, - overlay: { offsetX: 0 }, + overlay: { offsetX: 0, theme: 'light-mode' }, }, { async elementPicked(elementInfo: ElementInfo) { setHighlightedElement({ diff --git a/packages/web/src/theme.ts b/packages/web/src/theme.ts index c72db688b8fd0..c6199c3b05df1 100644 --- a/packages/web/src/theme.ts +++ b/packages/web/src/theme.ts @@ -93,22 +93,19 @@ export function currentDocumentTheme(): DocumentTheme | null { return null; } -export function useThemeSetting(): [Theme, (value: Theme) => void] { +export function useThemeSetting(): [Theme, DocumentTheme, (value: Theme) => void] { const [theme, setTheme] = React.useState(currentTheme()); + const [documentTheme, setDocumentTheme] = React.useState(() => currentDocumentTheme() ?? 'light-mode'); React.useEffect(() => { settings.setString(kThemeSettingsKey, theme); updateDocumentTheme(theme); }, [theme]); - return [theme, setTheme]; -} - -export function useDocumentTheme(): DocumentTheme { - const [theme, setTheme] = React.useState(() => currentDocumentTheme() ?? 'light-mode'); React.useEffect(() => { - addThemeListener(setTheme); - return () => removeThemeListener(setTheme); + addThemeListener(setDocumentTheme); + return () => removeThemeListener(setDocumentTheme); }, []); - return theme; + + return [theme, documentTheme, setTheme]; }