diff --git a/apps/studio/assets/appearance-dark.svg b/apps/studio/assets/appearance-dark.svg new file mode 100644 index 0000000000..ca23245617 --- /dev/null +++ b/apps/studio/assets/appearance-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/studio/assets/appearance-light.svg b/apps/studio/assets/appearance-light.svg new file mode 100644 index 0000000000..77f80cab31 --- /dev/null +++ b/apps/studio/assets/appearance-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/studio/assets/appearance-system.svg b/apps/studio/assets/appearance-system.svg new file mode 100644 index 0000000000..44b2811d5b --- /dev/null +++ b/apps/studio/assets/appearance-system.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio/e2e/appearance.test.ts b/apps/studio/e2e/appearance.test.ts new file mode 100644 index 0000000000..f59a0f528f --- /dev/null +++ b/apps/studio/e2e/appearance.test.ts @@ -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(); + } ); +} ); diff --git a/apps/studio/e2e/page-objects/user-settings-modal.ts b/apps/studio/e2e/page-objects/user-settings-modal.ts index ce33dfcd06..df24e04102 100644 --- a/apps/studio/e2e/page-objects/user-settings-modal.ts +++ b/apps/studio/e2e/page-objects/user-settings-modal.ts @@ -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 } ); } diff --git a/apps/studio/src/about-menu/about-menu.html b/apps/studio/src/about-menu/about-menu.html index c3c00087e0..3f9c224522 100644 --- a/apps/studio/src/about-menu/about-menu.html +++ b/apps/studio/src/about-menu/about-menu.html @@ -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; @@ -53,7 +65,7 @@ p a { text-decoration: underline; - color: #3858e9; + color: var( --color-frame-theme ); } .links { diff --git a/apps/studio/src/components/action-button.tsx b/apps/studio/src/components/action-button.tsx index ad0837dd7a..59671ea58a 100644 --- a/apps/studio/src/components/action-button.tsx +++ b/apps/studio/src/components/action-button.tsx @@ -143,7 +143,7 @@ export const ActionButton = ( { buttonLabel = __( 'Running' ); buttonProps = { icon: , - className: cx( defaultButtonClassName, '!text-a8c-green-50' ), + className: cx( defaultButtonClassName, '!text-frame-running' ), 'data-testid': 'site-status-running', }; break; diff --git a/apps/studio/src/components/ai-input.tsx b/apps/studio/src/components/ai-input.tsx index f9ccd05a00..8270582e57 100644 --- a/apps/studio/src/components/ai-input.tsx +++ b/apps/studio/src/components/ai-input.tsx @@ -34,7 +34,7 @@ const SparklesIcon = () => ( @@ -218,8 +218,8 @@ const UnforwardedAIInput = ( return (
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/components/assistant-thinking.tsx b/apps/studio/src/components/assistant-thinking.tsx index 12e071695a..373bbb9654 100644 --- a/apps/studio/src/components/assistant-thinking.tsx +++ b/apps/studio/src/components/assistant-thinking.tsx @@ -7,12 +7,12 @@ export function MessageThinking() { className="flex justify-center items-center gap-1 p-0.5 min-h-5" >
-
+
diff --git a/apps/studio/src/components/button.tsx b/apps/studio/src/components/button.tsx index 9452f92dee..dadc3460be 100644 --- a/apps/studio/src/components/button.tsx +++ b/apps/studio/src/components/button.tsx @@ -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] @@ -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 = ` @@ -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, ' ' ); /** diff --git a/apps/studio/src/components/chat-message.tsx b/apps/studio/src/components/chat-message.tsx index e88e26b7aa..d1bda2c2e1 100644 --- a/apps/studio/src/components/chat-message.tsx +++ b/apps/studio/src/components/chat-message.tsx @@ -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' ) } >
diff --git a/apps/studio/src/components/content-tab-assistant.tsx b/apps/studio/src/components/content-tab-assistant.tsx index e2dcb75877..42e1622479 100644 --- a/apps/studio/src/components/content-tab-assistant.tsx +++ b/apps/studio/src/components/content-tab-assistant.tsx @@ -43,7 +43,7 @@ const TelexIcon = () => ( @@ -454,10 +454,10 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps return (
{ isTelexBannerVisible && ( -
+
- + { createInterpolateElement( __( 'Build blocks with ' ), { @@ -480,7 +480,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
-
+
handleExport( exportDatabase ) } type="submit" variant="secondary" - className={ cx( isExportDisabled ? '' : '!text-a8c-blue-50 !shadow-a8c-blue-50' ) } + className={ cx( isExportDisabled ? '' : '!text-frame-theme !shadow-frame-theme' ) } disabled={ isExportDisabled } > { __( 'Export database' ) } @@ -150,8 +150,8 @@ const InitialImportButton = ( { className={ cx( 'w-full', disabled - ? '[&>div.border-zinc-300]:border-gray-400 cursor-not-allowed opacity-50' - : '[&>div.border-zinc-300]:hover:border-a8c-blue-50' + ? '[&>div.border-zinc-300]:border-frame-border cursor-not-allowed opacity-50' + : '[&>div.border-zinc-300]:hover:border-frame-theme' ) } onClick={ openFileSelector } disabled={ disabled } @@ -277,8 +277,8 @@ const ImportSite = ( { >
{ isImporting && ( @@ -311,8 +311,8 @@ const ImportSite = ( { ) } { isInitial && ( <> - - + + { isDraggingOver ? __( 'Drop file' ) : __( 'Drag a file here, or click to select a file' ) } diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 3a1c17ec31..25642dd1f3 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -30,7 +30,8 @@ interface ContentTabOverviewProps { selectedSite: SiteDetails; } -const skeletonBg = 'animate-pulse bg-gradient-to-r from-[#F6F7F7] via-[#DCDCDE] to-[#F6F7F7]'; +const skeletonBg = + 'animate-pulse bg-gradient-to-r from-frame-surface via-frame-surface-alt to-frame-surface'; const ButtonSectionSkeleton = ( { title }: { title: string } ) => { return ( @@ -221,17 +222,17 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )

{ __( 'Theme' ) }

{ ! loading && (
-
+
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 (
diff --git a/apps/studio/src/modules/preview-site/components/preview-sites-table-header.tsx b/apps/studio/src/modules/preview-site/components/preview-sites-table-header.tsx index d0e89bd876..16cd29003a 100644 --- a/apps/studio/src/modules/preview-site/components/preview-sites-table-header.tsx +++ b/apps/studio/src/modules/preview-site/components/preview-sites-table-header.tsx @@ -4,7 +4,7 @@ import { useI18n } from '@wordpress/react-i18n'; export function PreviewSitesTableHeader() { const { __ } = useI18n(); return ( -
+
{ __( 'Preview site' ) }
diff --git a/apps/studio/src/modules/sync/components/environment-badge.tsx b/apps/studio/src/modules/sync/components/environment-badge.tsx index a628bc252c..0781122fde 100644 --- a/apps/studio/src/modules/sync/components/environment-badge.tsx +++ b/apps/studio/src/modules/sync/components/environment-badge.tsx @@ -15,7 +15,7 @@ interface EnvironmentBadgeProps { export function EnvironmentBadge( { type, selected, className }: EnvironmentBadgeProps ) { const getClassName = () => { if ( selected ) { - return 'bg-white text-a8c-blue-50 text-a8c-blue-50'; + return 'bg-frame text-frame-theme text-frame-theme'; } const classes: Record< string, string > = { diff --git a/apps/studio/src/modules/sync/components/no-wpcom-sites-content.tsx b/apps/studio/src/modules/sync/components/no-wpcom-sites-content.tsx index e5b46e2d68..56f6de6f0b 100644 --- a/apps/studio/src/modules/sync/components/no-wpcom-sites-content.tsx +++ b/apps/studio/src/modules/sync/components/no-wpcom-sites-content.tsx @@ -31,7 +31,7 @@ export function NoWpcomSitesContent( {
{ features.map( ( text ) => (
- + { text }
) ) } diff --git a/apps/studio/src/modules/sync/components/no-wpcom-sites-modal.tsx b/apps/studio/src/modules/sync/components/no-wpcom-sites-modal.tsx index f03c60ace0..dcba98fe96 100644 --- a/apps/studio/src/modules/sync/components/no-wpcom-sites-modal.tsx +++ b/apps/studio/src/modules/sync/components/no-wpcom-sites-modal.tsx @@ -20,7 +20,7 @@ export function NoWpcomSitesModal( { onRequestClose, selectedSite }: NoWpcomSite
diff --git a/apps/studio/src/modules/sync/components/site-name-box.tsx b/apps/studio/src/modules/sync/components/site-name-box.tsx index c874cbd3bb..48a60b217b 100644 --- a/apps/studio/src/modules/sync/components/site-name-box.tsx +++ b/apps/studio/src/modules/sync/components/site-name-box.tsx @@ -16,7 +16,7 @@ export const SiteNameBox = ( { siteName, envType }: SiteNameBoxProps ) => { ) } - { siteName } + { siteName } ); }; diff --git a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx index bffba5c613..abb083f5a0 100644 --- a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx +++ b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx @@ -110,7 +110,7 @@ const SyncConnectedSiteControls = ( { ! isOffline && ! isAnySitePulling && ! isAnySitePushing && - '!text-black hover:!text-a8c-blue-50' + '!text-frame-text hover:!text-frame-theme' ) } onClick={ () => setSyncDialogType( 'pull' ) } disabled={ isAnySiteSyncing || isOffline } @@ -151,7 +151,7 @@ const SyncConnectedSiteControls = ( { ! isOffline && ! isAnySitePulling && ! isAnySitePushing && - '!text-black hover:!text-a8c-blue-50' + '!text-frame-text hover:!text-frame-theme' ) } onClick={ () => setSyncDialogType( 'push' ) } disabled={ isAnySiteSyncing || isOffline } @@ -269,7 +269,7 @@ const SyncConnectedSitesSectionItem = ( { ' diff --git a/apps/studio/src/modules/sync/components/sync-dialog.tsx b/apps/studio/src/modules/sync/components/sync-dialog.tsx index 4844298892..80d62125e9 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( {