Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
54f90e0
feat: add dark mode foundation with CSS custom properties and Tailwin…
shaunandrews Feb 24, 2026
fddcf11
fix: make Button component dark-mode-aware with frame-* tokens
shaunandrews Feb 24, 2026
9952c69
fix: add data-fullscreen-modal attribute for dark mode CSS targeting
shaunandrews Feb 24, 2026
cc8a69b
fix: migrate content area components from hardcoded colors to frame-*…
shaunandrews Feb 24, 2026
aa4c565
fix: migrate add-site flow from hardcoded colors to frame-* tokens
shaunandrews Feb 24, 2026
64eb023
fix: migrate sync, preview, onboarding, and whats-new to frame-* tokens
shaunandrews Feb 24, 2026
e324730
fix: use explicit white SVG for assistant icon in dark mode instead o…
shaunandrews Feb 24, 2026
1f6df9e
refactor: rename frame-link tokens to frame-theme and bridge WP admin…
shaunandrews Feb 24, 2026
848cab0
refactor: migrate a8c-blue-50 Tailwind classes to frame-theme token
shaunandrews Feb 24, 2026
c6a9355
refactor: migrate hardcoded #3858E9 hex values to frame-theme token
shaunandrews Feb 24, 2026
03e6a9f
fix: button hover consistency and icon-variant background
shaunandrews Feb 24, 2026
b5a6080
fix: wire WP component theme vars and progress bar for dark mode
shaunandrews Feb 24, 2026
36d6bdd
fix: unify running status green with frame-running token
shaunandrews Feb 24, 2026
2c17d1c
feat: add color scheme preference (System / Light / Dark)
shaunandrews Feb 25, 2026
473b0bf
feat: redesign appearance picker as visual card selector
shaunandrews Feb 25, 2026
1205de4
fix: align Studio CLI toggle with first line of label text
shaunandrews Feb 25, 2026
d40a1de
fix: use frame tokens for error fallback screen colors
shaunandrews Feb 25, 2026
5e343f2
feat: add dark mode announcement to What's New modal
shaunandrews Feb 25, 2026
2728c05
test: add e2e tests for color scheme preference, fix environment badg…
shaunandrews Feb 25, 2026
a2b2ff5
fix: prettier formatting for dark mode token changes
shaunandrews Feb 25, 2026
fcbcbe8
Merge remote-tracking branch 'origin/trunk' into add-dark-mode-support
shaunandrews Feb 25, 2026
4c80d71
Merge branch 'trunk' into add-dark-mode-support
shaunandrews Feb 25, 2026
f126c84
Fix progress bar label colors for dark mode
shaunandrews Feb 25, 2026
15a1059
Merge branch 'trunk' into add-dark-mode-support
shaunandrews Feb 26, 2026
29c9488
Fix prettier formatting in progress-bar.tsx
shaunandrews Feb 26, 2026
3a5fcc5
Merge branch 'add-dark-mode-support' of https://github.com/Automattic…
shaunandrews Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/studio/assets/appearance-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions apps/studio/assets/appearance-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions apps/studio/assets/appearance-system.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions apps/studio/e2e/appearance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { test, expect } from '@playwright/test';
import { E2ESession } from './e2e-helpers';
import Onboarding from './page-objects/onboarding';
import SiteContent from './page-objects/site-content';
import UserSettingsModal from './page-objects/user-settings-modal';

test.describe( 'Appearance', () => {
const session = new E2ESession();

const openSettings = async ( page: typeof session.mainWindow ) => {
const settingsButton = page.getByTestId( 'settings-button' );
await expect( settingsButton ).toBeVisible();
await settingsButton.click();
};

test.beforeAll( async () => {
await session.launch();
const onboarding = new Onboarding( session.mainWindow );
await onboarding.completeOnboarding();
await onboarding.closeWhatsNew();
const siteContent = new SiteContent( session.mainWindow, 'My WordPress Website' );
await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } );
} );

test.afterAll( async () => {
await session.cleanup();
} );

test( 'changes color scheme from settings', async () => {
await openSettings( session.mainWindow );
const settings = new UserSettingsModal( session.mainWindow );
await expect( settings.locator ).toBeVisible( { timeout: 10_000 } );

// Preferences tab should be active by default with appearance radio group visible
await expect( settings.appearanceRadioGroup ).toBeVisible();

// Default should be System
await expect( settings.getAppearanceOption( 'System' ) ).toHaveAttribute(
'aria-checked',
'true'
);

// Switch to Dark
await settings.selectColorScheme( 'Dark' );
const isDark = await session.electronApp.evaluate(
( { nativeTheme } ) => nativeTheme.shouldUseDarkColors
);
expect( isDark ).toBe( true );

// Switch to Light
await settings.selectColorScheme( 'Light' );
const isLight = await session.electronApp.evaluate(
( { nativeTheme } ) => nativeTheme.shouldUseDarkColors
);
expect( isLight ).toBe( false );

// Switch back to System
await settings.selectColorScheme( 'System' );
await expect( settings.getAppearanceOption( 'System' ) ).toHaveAttribute(
'aria-checked',
'true'
);

await settings.close();
} );

test( 'persists color scheme across app restart', async () => {
// Select Dark
await openSettings( session.mainWindow );
const settings = new UserSettingsModal( session.mainWindow );
await expect( settings.locator ).toBeVisible( { timeout: 10_000 } );
await settings.selectColorScheme( 'Dark' );
await settings.close();

// Restart the app
await session.restart();
await session.mainWindow.waitForLoadState( 'domcontentloaded' );

const onboarding = new Onboarding( session.mainWindow );
try {
const visible = await onboarding.heading.isVisible( { timeout: 2000 } );
if ( visible ) {
await onboarding.completeOnboarding();
}
} catch ( error ) {
// Onboarding not visible, continue with test
}

await onboarding.closeWhatsNew();

const siteContent = new SiteContent( session.mainWindow, 'My WordPress Website' );
await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } );

// Verify Dark is still selected
await openSettings( session.mainWindow );
const settingsAfterRestart = new UserSettingsModal( session.mainWindow );
await expect( settingsAfterRestart.locator ).toBeVisible( { timeout: 10_000 } );
await expect( settingsAfterRestart.getAppearanceOption( 'Dark' ) ).toHaveAttribute(
'aria-checked',
'true'
);

// Reset to System for other tests
await settingsAfterRestart.selectColorScheme( 'System' );
await settingsAfterRestart.close();
} );
} );
14 changes: 14 additions & 0 deletions apps/studio/e2e/page-objects/user-settings-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ export default class UserSettingsModal {
return this.locator.getByRole( 'button', { name: 'Close' } );
}

get appearanceRadioGroup() {
return this.locator.getByRole( 'radiogroup', { name: 'Appearance' } );
}

getAppearanceOption( name: string ) {
return this.appearanceRadioGroup.getByRole( 'radio', { name } );
}

async selectColorScheme( scheme: 'System' | 'Light' | 'Dark' ) {
const option = this.getAppearanceOption( scheme );
await option.click();
await expect( option ).toHaveAttribute( 'aria-checked', 'true' );
}

async selectLanguage( language: string ) {
await this.languageSelect.selectOption( { label: language } );
}
Expand Down
14 changes: 13 additions & 1 deletion apps/studio/src/about-menu/about-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
local( 'LucidaGrandeUI' );
}

:root {
--color-frame-theme: #3858e9;
--color-frame-theme-hover: #2145e6;
}

@media ( prefers-color-scheme: dark ) {
:root {
--color-frame-theme: #6b8aff;
--color-frame-theme-hover: #8da6ff;
}
}

html,
body {
-webkit-touch-callout: none;
Expand Down Expand Up @@ -53,7 +65,7 @@

p a {
text-decoration: underline;
color: #3858e9;
color: var( --color-frame-theme );
}

.links {
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/components/action-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const ActionButton = ( {
buttonLabel = __( 'Running' );
buttonProps = {
icon: <CircleIcon height={ iconSize } width={ iconSize } />,
className: cx( defaultButtonClassName, '!text-a8c-green-50' ),
className: cx( defaultButtonClassName, '!text-frame-running' ),
'data-testid': 'site-status-running',
};
break;
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/src/components/ai-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const SparklesIcon = () => (
<g transform="translate(5, 5)" clipPath="url(#clip0_2870_30744)">
<path
d="M13.7035 6.58213L10.8309 5.59124C9.69491 5.20089 8.79911 4.30509 8.40876 3.16908L7.41787 0.296515C7.28275 -0.0988382 6.71725 -0.0988382 6.58213 0.296515L5.59124 3.16908C5.20089 4.30509 4.30509 5.20089 3.16908 5.59124L0.296515 6.58213C-0.0988382 6.71725 -0.0988382 7.28275 0.296515 7.41787L3.16908 8.40876C4.30509 8.79911 5.20089 9.69491 5.59124 10.8309L6.58213 13.7035C6.71725 14.0988 7.28275 14.0988 7.41787 13.7035L8.40876 10.8309C8.79911 9.69491 9.69491 8.79911 10.8309 8.40876L13.7035 7.41787C14.0988 7.28275 14.0988 6.71725 13.7035 6.58213ZM10.3505 7.21269L8.91421 7.70813C8.3437 7.90331 7.8983 8.35371 7.70313 8.91921L7.20768 10.3555C7.13762 10.5557 6.85737 10.5557 6.79231 10.3555L6.29687 8.91921C6.1017 8.3487 5.6513 7.90331 5.08579 7.70813L3.64951 7.21269C3.44933 7.14263 3.44933 6.86238 3.64951 6.79232L5.08579 6.29687C5.6563 6.1017 6.1017 5.6513 6.29687 5.08579L6.79231 3.64951C6.86238 3.44933 7.14263 3.44933 7.20768 3.64951L7.70313 5.08579C7.8983 5.6563 8.3487 6.1017 8.91421 6.29687L10.3505 6.79232C10.5507 6.86238 10.5507 7.14263 10.3505 7.21269Z"
fill="black"
fill="currentColor"
/>
</g>
<defs>
Expand Down Expand Up @@ -218,8 +218,8 @@ const UnforwardedAIInput = (
return (
<div
className={ cx(
`flex items-end w-full border rounded-sm bg-white/[0.9] ${
disabled ? 'border-a8c-gray-5' : 'border-gray-300 focus-within:border-a8c-blue-50'
`flex items-end w-full border rounded-sm bg-frame ${
disabled ? 'border-frame-border' : 'border-frame-border focus-within:border-frame-theme'
}`
) }
>
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function App() {
/>
<main
data-testid="site-content"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden z-10"
className="bg-frame text-frame-text h-full flex-grow rounded-chrome overflow-hidden z-10"
>
<SiteContentTabs />
</main>
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/src/components/assistant-thinking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export function MessageThinking() {
className="flex justify-center items-center gap-1 p-0.5 min-h-5"
>
<div
className="animate-pulse h-1.5 w-1.5 bg-a8c-blue-50 rounded-full"
className="animate-pulse h-1.5 w-1.5 bg-frame-theme rounded-full"
style={ { animationDelay: '0.2s' } }
/>
<div className="animate-pulse h-1.5 w-1.5 bg-a8c-blue-50 rounded-full" />
<div className="animate-pulse h-1.5 w-1.5 bg-frame-theme rounded-full" />
<div
className="animate-pulse h-1.5 w-1.5 bg-a8c-blue-50 rounded-full"
className="animate-pulse h-1.5 w-1.5 bg-frame-theme rounded-full"
style={ { animationDelay: '0.4s' } }
/>
</div>
Expand Down
34 changes: 17 additions & 17 deletions apps/studio/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,26 @@ justify-center
disabled:cursor-not-allowed
aria-disabled:cursor-not-allowed
[&.components-button]:focus:shadow-[inset_0_0_0_1px_transparent]
[&.components-button]:focus-visible:shadow-[0_0_0_1px_#3858E9]
[&.components-button]:focus-visible:shadow-a8c-blue-50
[&.components-button]:focus-visible:shadow-[0_0_0_1px_var(--color-frame-theme)]
[&.components-button]:focus-visible:shadow-frame-theme
[&.components-button.is-destructive]:focus-visible:shadow-a8c-red-50
[&_svg]:shrink-0
`.replace( /\n/g, ' ' );

const primaryStyles = `
[&.is-primary:not(:disabled)]:focus:shadow-[inset_0_0_0_1px_transparent]
[&.is-primary:not(:disabled)]:focus-visible:shadow-[inset_0_0_0_1px_white,0_0_0_1px_#3858E9]
[&.is-primary:not(:disabled)]:focus-visible:shadow-[inset_0_0_0_1px_white,0_0_0_1px_var(--color-frame-theme)]
`.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]:focus-visible:shadow-frame-theme
[&.is-secondary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:text-frame-theme
[&.is-secondary:not(.is-destructive,:disabled,[aria-disabled=true]):hover_svg]:fill-frame-theme
[&.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]
Expand All @@ -66,15 +67,15 @@ 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
[&.components-button.outlined]:focus-visible:shadow-[inset_0_0_0_1px_#3858E9]
[&.components-button]:focus-visible:shadow-a8c-blue-50
[&.components-button.outlined]:focus-visible:shadow-[inset_0_0_0_1px_var(--color-frame-theme)]
[&.components-button]:focus-visible:shadow-frame-theme
`.replace( /\n/g, ' ' );

const destructiveStyles = `
Expand All @@ -88,16 +89,15 @@ const destructiveStyles = `

const linkStyles = `
[&.is-link]:no-underline
[&.is-link]:hover:text-[#2145e6]
[&.is-link]:active:text-black
[&.is-link]:hover:text-frame-theme
[&.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-opacity-10
hover:bg-white/10
`.replace( /\n/g, ' ' );

/**
Expand Down
8 changes: 4 additions & 4 deletions apps/studio/src/components/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ export const ChatMessage = forwardRef< HTMLDivElement, ChatMessageProps >(
'inline-block p-3 rounded border overflow-x-auto overflow-y-hidden select-text',
isUnauthenticated ? 'lg:max-w-[90%]' : 'lg:max-w-[70%]',
message.failedMessage
? 'border-[#FACFD2] bg-[#F7EBEC]'
? 'border-frame-error/30 bg-frame-error/10'
: message.role === 'user'
? 'bg-white'
: 'bg-white/45',
! message.failedMessage && 'border-gray-300'
? 'bg-frame'
: 'bg-frame/45',
! message.failedMessage && 'border-frame-border'
) }
>
<div className="relative">
Expand Down
10 changes: 5 additions & 5 deletions apps/studio/src/components/content-tab-assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const TelexIcon = () => (
<g transform="translate(5, 5)" clipPath="url(#clip0_telex_icon)">
<path
d="M13.7035 6.58213L10.8309 5.59124C9.69491 5.20089 8.79911 4.30509 8.40876 3.16908L7.41787 0.296515C7.28275 -0.0988382 6.71725 -0.0988382 6.58213 0.296515L5.59124 3.16908C5.20089 4.30509 4.30509 5.20089 3.16908 5.59124L0.296515 6.58213C-0.0988382 6.71725 -0.0988382 7.28275 0.296515 7.41787L3.16908 8.40876C4.30509 8.79911 5.20089 9.69491 5.59124 10.8309L6.58213 13.7035C6.71725 14.0988 7.28275 14.0988 7.41787 13.7035L8.40876 10.8309C8.79911 9.69491 9.69491 8.79911 10.8309 8.40876L13.7035 7.41787C14.0988 7.28275 14.0988 6.71725 13.7035 6.58213ZM10.3505 7.21269L8.91421 7.70813C8.3437 7.90331 7.8983 8.35371 7.70313 8.91921L7.20768 10.3555C7.13762 10.5557 6.85737 10.5557 6.79231 10.3555L6.29687 8.91921C6.1017 8.3487 5.6513 7.90331 5.08579 7.70813L3.64951 7.21269C3.44933 7.14263 3.44933 6.86238 3.64951 6.79232L5.08579 6.29687C5.6563 6.1017 6.1017 5.6513 6.29687 5.08579L6.79231 3.64951C6.86238 3.44933 7.14263 3.44933 7.20768 3.64951L7.70313 5.08579C7.8983 5.6563 8.3487 6.1017 8.91421 6.29687L10.3505 6.79232C10.5507 6.86238 10.5507 7.14263 10.3505 7.21269Z"
fill="#2C2C2C"
fill="currentColor"
/>
</g>
<defs>
Expand Down Expand Up @@ -454,10 +454,10 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
return (
<div className="relative min-h-full flex flex-col" ref={ wrapperRef }>
{ isTelexBannerVisible && (
<div className="bg-white border border-gray-300 rounded-sm m-8 mb-0 p-2 pr-4 flex items-center justify-between">
<div className="bg-frame border border-frame-border rounded-sm m-8 mb-0 p-2 pr-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<TelexIcon />
<span className="text-gray-900">
<span className="text-frame-text">
{ createInterpolateElement(
__( 'Build blocks with <button>Telex <ArrowIcon /></button>' ),
{
Expand All @@ -480,7 +480,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
</div>
<button
onClick={ handleDismissBanner }
className="text-gray-500 hover:text-gray-700"
className="text-frame-text-secondary hover:text-frame-text"
aria-label={ __( 'Dismiss' ) }
>
Expand Down Expand Up @@ -526,7 +526,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
</div>
</div>

<div className="sticky bottom-0 bg-gray-50/[0.8] backdrop-blur-sm w-full px-8 pt-4 flex items-center">
<div className="sticky bottom-0 bg-frame/80 backdrop-blur-sm w-full px-8 pt-4 flex items-center">
<div className="w-full flex flex-col items-center">
<AIInput
ref={ inputRef }
Expand Down
Loading