From 54f90e0277f6e35223e4b9364369366747b8e85a Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Mon, 23 Feb 2026 23:04:31 -0500 Subject: [PATCH 01/22] feat: add dark mode foundation with CSS custom properties and Tailwind tokens Define 14 frame-* CSS custom properties in :root with prefers-color-scheme dark overrides. Add matching Tailwind color tokens (frame, frame-text, frame-border, frame-surface, etc.). Enable Electron nativeTheme.themeSource = 'system' so the renderer respects OS dark mode. Apply bg-frame and text-frame-text to the content frame in app.tsx. Add dark mode override block in index.css targeting WP component classes (inputs, buttons, tabs, modals, popovers) using :is() for scoped element cascade. Co-Authored-By: Claude Opus 4.6 --- apps/studio/src/components/app.tsx | 2 +- apps/studio/src/index.css | 217 ++++++++++++++++++++++++++--- apps/studio/src/main-window.ts | 4 +- apps/studio/tailwind.config.js | 12 ++ 4 files changed, 215 insertions(+), 20 deletions(-) diff --git a/apps/studio/src/components/app.tsx b/apps/studio/src/components/app.tsx index c0329b99d7..e4515c016c 100644 --- a/apps/studio/src/components/app.tsx +++ b/apps/studio/src/components/app.tsx @@ -79,7 +79,7 @@ export default function App() { />
diff --git a/apps/studio/src/index.css b/apps/studio/src/index.css index b4d5e8ca62..26ea208d2d 100644 --- a/apps/studio/src/index.css +++ b/apps/studio/src/index.css @@ -3,6 +3,42 @@ @tailwind components; @tailwind utilities; +:root { + --color-frame-bg: #fff; + --color-frame-text: #1e1e1e; + --color-frame-text-secondary: #757575; + --color-frame-border: #DCDCDE; + --color-frame-surface: #f0f0f0; + --color-frame-surface-alt: #DCDCDE; + --color-frame-scrollbar-thumb: #7f7f7f; + --color-frame-scrollbar-thumb-hover: #484848; + --color-frame-scrollbar-border: #fff; + --color-frame-link: #3858e9; + --color-frame-link-hover: #2145e6; + --color-frame-code-text: #1d2327; + --color-frame-error: #d63638; + --color-frame-tab-active: #000; +} + +@media ( prefers-color-scheme: dark ) { + :root { + --color-frame-bg: #2F2F2F; + --color-frame-text: #e0e0e0; + --color-frame-text-secondary: #949494; + --color-frame-border: #474747; + --color-frame-surface: #383838; + --color-frame-surface-alt: #474747; + --color-frame-scrollbar-thumb: #666666; + --color-frame-scrollbar-thumb-hover: #888888; + --color-frame-scrollbar-border: #2F2F2F; + --color-frame-link: #6b8aff; + --color-frame-link-hover: #8da6ff; + --color-frame-code-text: #e0e0e0; + --color-frame-error: #f87171; + --color-frame-tab-active: #fff; + } +} + @layer utilities { .interpolate-size-allow-keywords { interpolate-size: allow-keywords; @@ -57,7 +93,7 @@ blockquote { .components-tab-panel__tabs { padding: 0 theme( 'spacing.4' ); - border-bottom: 1px solid theme( 'colors.a8c-gray-5' ); + border-bottom: 1px solid var(--color-frame-border); } .components-tab-panel__tabs-item { @@ -77,8 +113,14 @@ blockquote { content: url( 'data:image/svg+xml;utf8,' ); } +@media ( prefers-color-scheme: dark ) { + .components-tab-panel__tabs--assistant::before { + filter: invert( 1 ); + } +} + .components-tab-panel__tabs-item.is-active::after { - background: theme( 'colors.black' ); + background: var(--color-frame-tab-active); } /* Customize scrollbar area for the sites sidebar and tab panel */ @@ -97,9 +139,9 @@ blockquote { } .components-tab-panel__tab-content::-webkit-scrollbar-thumb, .assistant-textarea::-webkit-scrollbar-thumb { - background: #7f7f7f; + background: var(--color-frame-scrollbar-thumb); border-radius: 10px; - border: 2px solid white; + border: 2px solid var(--color-frame-scrollbar-border); } /* Add hover effect to thumb appearance on the scrollbar */ @@ -108,7 +150,7 @@ blockquote { } .components-tab-panel__tab-content::-webkit-scrollbar-thumb:hover, .assistant-textarea::-webkit-scrollbar-thumb:hover { - background: #484848; + background: var(--color-frame-scrollbar-thumb-hover); } /* Avoid selecting text on dropdown menu, like preview links */ @@ -127,24 +169,24 @@ blockquote { } .assistant-markdown a { - color: #3858e9; + color: var(--color-frame-link); } .assistant-markdown a:hover, .assistant-markdown a:focus { - color: #2145e6; + color: var(--color-frame-link-hover); text-decoration: underline; } .assistant-markdown blockquote { - background-color: theme( 'colors.a8c-gray.0' ); + background-color: var(--color-frame-surface); border-radius: 2px; margin: 0 0 1rem; padding: 0.5rem 1rem; } .assistant-markdown blockquote > blockquote { - background-color: theme( 'colors.a8c-gray.5' ); + background-color: var(--color-frame-surface-alt); margin: 0; } @@ -153,8 +195,8 @@ blockquote { } .assistant-markdown code { - background-color: theme( 'colors.a8c-gray.0' ); - color: #1d2327; + background-color: var(--color-frame-surface); + color: var(--color-frame-code-text); font-family: 'Courier New', Courier, monospace; font-size: 13px; padding: 0.25rem; @@ -222,7 +264,7 @@ blockquote { .assistant-markdown hr { border: none; - border-top: 1px solid theme( 'colors.a8c-gray.5' ); + border-top: 1px solid var(--color-frame-border); margin: 1rem 0; } @@ -264,18 +306,18 @@ blockquote { } .assistant-markdown tr { - background-color: theme( 'colors.a8c-white.DEFAULT' ); - border-top: 1px solid theme( 'colors.a8c-gray.0' ); + background-color: var(--color-frame-bg); + border-top: 1px solid var(--color-frame-border); } .assistant-markdown tr:nth-child( 2n ) { - background-color: theme( 'colors.a8c-gray.0' ); + background-color: var(--color-frame-surface); } .assistant-markdown td, .assistant-markdown th { padding: 6px 13px; - border: 1px solid theme( 'colors.a8c-gray.10' ); + border: 1px solid var(--color-frame-border); } .assistant-markdown th { @@ -287,7 +329,7 @@ blockquote { } .error-message { - color: #d63638; + color: var(--color-frame-error); } .error-select-control .components-input-control__backdrop { @@ -299,10 +341,149 @@ blockquote { bottom: 0; left: 0; right: 0; - background: white; + background: var(--color-frame-bg); } .components-button.components-guide__back-button { outline: none; border: 1px solid theme( 'colors.a8c-blue.50' ); } + +/* Dark mode: override WP component colors. + .components-* classes only exist in content areas, not the sidebar chrome. + Element cascade uses :is() to cover all three render contexts (content frame, + WP modals, fullscreen modal) without tripling every selector. */ +@media ( prefers-color-scheme: dark ) { + /* Element text cascade — WP Heading/Text set explicit color on elements. + :is() groups the scopes; specificity = 0-1-1, same as [attr] element. */ + :is([data-testid="site-content"], .components-modal__frame, [data-fullscreen-modal]) :is(h1, h2, h3, h4, h5, h6, p, span, label, li, td, th) { + color: var(--color-frame-text); + } + + /* Modal/fullscreen surfaces */ + .components-modal__frame, + .components-modal__content, + [data-fullscreen-modal] { + background-color: var(--color-frame-bg); + color: var(--color-frame-text); + } + + /* Modal chrome */ + .components-modal__header { + border-bottom-color: var(--color-frame-border); + } + + /* Tab labels */ + .components-tab-panel__tabs-item { + color: var(--color-frame-text-secondary); + } + .components-tab-panel__tabs-item.is-active { + color: var(--color-frame-text); + } + .components-tab-panel__tabs-item:hover { + color: var(--color-frame-text); + } + .components-tab-panel__tabs { + border-bottom-color: var(--color-frame-border); + } + .components-tab-panel__tabs-item.is-active::after { + background: var(--color-frame-tab-active); + } + + /* Buttons — tertiary/link get secondary text, hover brightens */ + .components-button.is-tertiary, + .components-button.is-link:not(.is-destructive) { + color: var(--color-frame-text-secondary); + } + .components-button.is-tertiary:hover, + .components-button.is-link:not(.is-destructive):hover { + color: var(--color-frame-text); + } + + /* Button SVG icons */ + .components-button svg { + fill: var(--color-frame-text); + } + + /* Preserve primary/destructive button colors */ + .components-button.is-primary, + .components-button.is-primary svg, + .components-button.is-destructive:not(.is-secondary) { + color: #fff; + fill: #fff; + } + + /* Input/select controls */ + .components-text-control__input, + .components-select-control__input, + .components-input-control__input, + .components-search-control__input { + color: var(--color-frame-text) !important; + background-color: var(--color-frame-surface) !important; + border-color: var(--color-frame-border); + } + .components-input-control__container { + background-color: var(--color-frame-surface); + } + .components-input-control__backdrop { + border-color: var(--color-frame-border) !important; + } + .components-search-control__input::placeholder { + color: var(--color-frame-text-secondary); + } + + /* Checkbox / Toggle */ + .components-checkbox-control__input[type="checkbox"], + input[type="checkbox"] { + border-color: var(--color-frame-border); + background-color: var(--color-frame-surface); + } + .components-form-toggle:not(.is-checked) .components-form-toggle__track { + border-color: var(--color-frame-border); + } + + /* Typography tokens */ + .a8c-title-medium, + .a8c-subtitle, + .a8c-subtitle-small, + .a8c-body { + color: var(--color-frame-text); + } + + /* Popover / DropdownMenu (portaled to body) */ + .components-popover .components-popover__content { + background-color: var(--color-frame-bg); + border-color: var(--color-frame-border); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); + } + .components-dropdown-menu__menu { + background-color: var(--color-frame-bg); + } + .components-menu-item__button, + .components-menu-item__button .components-menu-item__item { + color: var(--color-frame-text-secondary); + } + .components-menu-item__button:hover, + .components-menu-item__button:focus { + background-color: transparent !important; + } + .components-menu-item__button.is-destructive, + .components-menu-item__button.is-destructive .components-menu-item__item { + color: var(--color-frame-error); + } + .components-menu-item__button.is-destructive svg { + fill: var(--color-frame-error); + } + .components-menu-item__button svg { + fill: var(--color-frame-text-secondary); + } + .components-menu-group + .components-menu-group { + border-top-color: var(--color-frame-border); + } + + /* WP Guide (What's New modal) */ + .components-guide { + background-color: var(--color-frame-bg); + color: var(--color-frame-text); + } +} diff --git a/apps/studio/src/main-window.ts b/apps/studio/src/main-window.ts index 833e76ffa8..ba7c34d2b2 100644 --- a/apps/studio/src/main-window.ts +++ b/apps/studio/src/main-window.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, type BrowserWindowConstructorOptions, screen, app } from 'electron'; +import { BrowserWindow, type BrowserWindowConstructorOptions, screen, app, nativeTheme } from 'electron'; import * as path from 'path'; import { portFinder } from '@studio/common/lib/port-finder'; import { @@ -57,6 +57,8 @@ export async function createMainWindow(): Promise< BrowserWindow > { return mainWindow; } + nativeTheme.themeSource = 'system'; + const savedBounds = await loadWindowBounds(); let windowOptions: BrowserWindowConstructorOptions = { height: MAIN_MIN_HEIGHT, diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index 930453cc59..707eba5f80 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -153,6 +153,18 @@ module.exports = { 'development-text': 'hsl(200, 95%, 28%)', 'circle-env-production': '#069e08', 'circle-env-staging': '#f7ba42', + // Content frame colors (CSS custom properties, swap in dark mode) + frame: 'var(--color-frame-bg)', + 'frame-text': 'var(--color-frame-text)', + 'frame-text-secondary': 'var(--color-frame-text-secondary)', + 'frame-border': 'var(--color-frame-border)', + 'frame-surface': 'var(--color-frame-surface)', + 'frame-surface-alt': 'var(--color-frame-surface-alt)', + 'frame-link': 'var(--color-frame-link)', + 'frame-link-hover': 'var(--color-frame-link-hover)', + 'frame-code-text': 'var(--color-frame-code-text)', + 'frame-error': 'var(--color-frame-error)', + 'frame-tab-active': 'var(--color-frame-tab-active)', }, spacing: { chrome: `${ APP_CHROME_SPACING }px`, From fddcf11e969bd827ae720bde4507a356aa1b467a Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Mon, 23 Feb 2026 23:04:38 -0500 Subject: [PATCH 02/22] fix: make Button component dark-mode-aware with frame-* tokens Replace hardcoded text-black with text-frame-text in secondary and link active states. Replace bg-gray-100 with bg-frame-surface in outlined hover/active states. Replace hover:bg-white with hover:bg-frame-surface in icon variant. This eliminates the root cause of most !important overrides in consumer components. Co-Authored-By: Claude Opus 4.6 --- apps/studio/src/components/button.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/studio/src/components/button.tsx b/apps/studio/src/components/button.tsx index 9452f92dee..b3c8c6757b 100644 --- a/apps/studio/src/components/button.tsx +++ b/apps/studio/src/components/button.tsx @@ -48,13 +48,13 @@ const primaryStyles = ` `.replace( /\n/g, ' ' ); const secondaryStyles = ` -[&.is-secondary]:text-black +[&.is-secondary]:text-frame-text [&.is-secondary]:shadow-[inset_0_0_0_1px_black] [&.is-secondary]:shadow-a8c-gray-5 [&.is-secondary]:focus:shadow-a8c-gray-5 [&.is-secondary]:focus-visible:shadow-a8c-blue-50 [&.is-secondary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:text-a8c-blue-50 -[&.is-secondary:not(.is-destructive,:disabled,[aria-disabled=true])]:active:text-black +[&.is-secondary:not(.is-destructive,:disabled,[aria-disabled=true])]:active:text-frame-text [&.is-secondary:disabled:not(:focus)]:shadow-[inset_0_0_0_1px_black] [&.is-secondary:disabled:not(:focus)]:shadow-a8c-gray-5 [&.is-secondary:not(:focus)]:aria-disabled:shadow-[inset_0_0_0_1px_black] @@ -66,10 +66,10 @@ const secondaryStyles = ` const outlinedStyles = ` outlined text-white -[&.components-button]:hover:text-black -[&.components-button]:hover:bg-gray-100 -[&.components-button]:active:text-black -[&.components-button]:active:bg-gray-100 +[&.components-button]:hover:text-frame-text +[&.components-button]:hover:bg-frame-surface +[&.components-button]:active:text-frame-text +[&.components-button]:active:bg-frame-surface [&.components-button]:shadow-[inset_0_0_0_1px_white] [&.components-button.outlined]:focus:shadow-[inset_0_0_0_1px_white] [&.components-button]:focus-visible:outline-none @@ -89,14 +89,14 @@ const destructiveStyles = ` const linkStyles = ` [&.is-link]:no-underline [&.is-link]:hover:text-[#2145e6] -[&.is-link]:active:text-black +[&.is-link]:active:text-frame-text [&.is-link]:disabled:text-a8c-gray-50 `.replace( /\n/g, ' ' ); const iconStyles = ` [&.components-button]:p-0 h-auto -hover:bg-white +hover:bg-frame-surface hover:bg-opacity-10 `.replace( /\n/g, ' ' ); From 9952c69353f0c3f7b25fdacd1faf048d6ea1519f Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Mon, 23 Feb 2026 23:04:45 -0500 Subject: [PATCH 03/22] fix: add data-fullscreen-modal attribute for dark mode CSS targeting The fullscreen modal renders outside both [data-testid='site-content'] and .components-modal__frame, so dark mode overrides couldn't reach its children. Add data-fullscreen-modal attribute as a CSS hook. Co-Authored-By: Claude Opus 4.6 --- apps/studio/src/components/fullscreen-modal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/components/fullscreen-modal.tsx b/apps/studio/src/components/fullscreen-modal.tsx index cec5cec15a..d6301f6962 100644 --- a/apps/studio/src/components/fullscreen-modal.tsx +++ b/apps/studio/src/components/fullscreen-modal.tsx @@ -58,10 +58,11 @@ export const FullscreenModal: React.FC< FullscreenModalProps > = ( { return ( ' ), { @@ -480,7 +480,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
' diff --git a/apps/studio/src/modules/sync/components/sync-dialog.tsx b/apps/studio/src/modules/sync/components/sync-dialog.tsx index 4844298892..947ebb34d3 100644 --- a/apps/studio/src/modules/sync/components/sync-dialog.tsx +++ b/apps/studio/src/modules/sync/components/sync-dialog.tsx @@ -301,7 +301,7 @@ export function SyncDialog( {