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 Telex ' ), { @@ -480,7 +480,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps ✕ @@ -526,7 +526,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 && ( - + { text } ) ) } @@ -114,7 +114,7 @@ function NoAuth( { selectedSite }: React.ComponentProps< typeof EmptyGeneric > ) { if ( isOffline ) { return; @@ -191,7 +191,7 @@ export function ContentTabPreviews( { selectedSite }: ContentTabPreviewsProps ) - + { activeOperation && ( ) } @@ -207,7 +207,7 @@ export function ContentTabPreviews( { selectedSite }: ContentTabPreviewsProps ) key={ snapshot.atomicSiteId } /> ) ) } - + { void dispatch( diff --git a/apps/studio/src/components/content-tab-settings.tsx b/apps/studio/src/components/content-tab-settings.tsx index 7efd46f345..2bfeb037ff 100644 --- a/apps/studio/src/components/content-tab-settings.tsx +++ b/apps/studio/src/components/content-tab-settings.tsx @@ -80,7 +80,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) return ( - + { __( 'Site details' ) } @@ -162,7 +162,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) - { __( 'Debugging' ) } + { __( 'Debugging' ) } @@ -184,7 +184,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) - { __( 'WP Admin' ) } + { __( 'WP Admin' ) } diff --git a/apps/studio/src/components/copy-text-button.tsx b/apps/studio/src/components/copy-text-button.tsx index 310a5c62f3..23e63b3670 100644 --- a/apps/studio/src/components/copy-text-button.tsx +++ b/apps/studio/src/components/copy-text-button.tsx @@ -51,8 +51,8 @@ export function CopyTextButton( { return ( { }; return ( - + { __( 'Uh oh!' ) } - + { __( "Something's broken." ) } - + { __( 'We’ve logged the issue to help us track down the problem.' ) } { __( 'Try restarting the app, if the problem persists' ) }{ ' ' } { window.location.reload() } > @@ -135,7 +135,7 @@ export default function DefaultErrorFallback() { - + 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/components/progress-bar.tsx b/apps/studio/src/components/progress-bar.tsx index 74195d523e..24d0417c39 100644 --- a/apps/studio/src/components/progress-bar.tsx +++ b/apps/studio/src/components/progress-bar.tsx @@ -76,9 +76,9 @@ type TwoColorProgressBarProps = { export function TwoColorProgressBar( { value, maxValue, - normalColorClass = 'bg-a8c-blue-50', + normalColorClass = 'bg-frame-theme', overLimitColorClass = 'bg-a8c-red-50', - trackColorClass = 'bg-a8c-gray-5', + trackColorClass = 'bg-frame-text-secondary', showLabels = false, valueLabel, limitLabel, @@ -91,12 +91,14 @@ export function TwoColorProgressBar( { { showLabels && ( valueLabel || limitLabel || overLimitLabel ) && ( - { valueLabel } + { valueLabel } { isOverLimit && overLimitLabel ? ( - { overLimitLabel } + { overLimitLabel } ) : ( - limitLabel && { limitLabel } + limitLabel && ( + { limitLabel } + ) ) } diff --git a/apps/studio/src/components/screenshot-demo-site.tsx b/apps/studio/src/components/screenshot-demo-site.tsx index 1a1bcd791f..01e98d79b2 100644 --- a/apps/studio/src/components/screenshot-demo-site.tsx +++ b/apps/studio/src/components/screenshot-demo-site.tsx @@ -13,7 +13,7 @@ const backgroundSvg = ( > - { __( 'Select a site to view details.' ) } + + { __( 'Select a site to view details.' ) } + ); } @@ -96,7 +98,7 @@ export function SiteContentTabs() { - { siteName } + { siteName } { displayMessage } diff --git a/apps/studio/src/components/site-menu.tsx b/apps/studio/src/components/site-menu.tsx index 7c3c1b44bb..e7b43f9090 100644 --- a/apps/studio/src/components/site-menu.tsx +++ b/apps/studio/src/components/site-menu.tsx @@ -94,7 +94,7 @@ function ButtonToRun( site: SiteDetails ) { } return running ? stopServer( id ) : startServer( site ); } } - className="w-7 h-8 rounded-tr rounded-br group grid focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-a8c-blue-50" + className="w-7 h-8 rounded-tr rounded-br group grid focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-frame-theme" aria-label={ sprintf( running ? __( 'stop %s site' ) : __( 'start %s site' ), name ) } > { /* Circle or Xdebug icon */ } @@ -114,8 +114,9 @@ function ButtonToRun( site: SiteDetails ) { 'w-2.5 h-2.5 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0 border-[0.5px]', 'row-start-1 col-start-1 place-self-center', classCircle, - loadingServer[ id ] && 'animate-pulse border-[#00BA3775] bg-[#1ED15A75] duration-100', - running && 'border-[#00BA37] bg-[#1ED15A] duration-100', + loadingServer[ id ] && + 'animate-pulse border-a8c-green-20/50 bg-a8c-green-20/50 duration-100', + running && 'border-a8c-green-20 bg-a8c-green-20 duration-100', ! running && ! loadingServer[ id ] && 'border-[#ffffff19] bg-[#ffffff26]' ) } > @@ -222,7 +223,7 @@ function SiteItem( { > { setSelectedSiteId( site.id ); } } diff --git a/apps/studio/src/components/tree-view.tsx b/apps/studio/src/components/tree-view.tsx index 5be742d648..a14d45bca3 100644 --- a/apps/studio/src/components/tree-view.tsx +++ b/apps/studio/src/components/tree-view.tsx @@ -165,13 +165,13 @@ const TreeItem = ( { { expanded && node.children && ( { node.children.length === 0 ? ( renderEmptyContent ? ( renderEmptyContent( node.id, node ) ) : ( - + { __( 'Empty' ) } ) diff --git a/apps/studio/src/components/welcome-message-prompt.tsx b/apps/studio/src/components/welcome-message-prompt.tsx index 011622b7e7..c470623eff 100644 --- a/apps/studio/src/components/welcome-message-prompt.tsx +++ b/apps/studio/src/components/welcome-message-prompt.tsx @@ -35,7 +35,7 @@ const WelcomeMessagePrompt = React.forwardRef< HTMLDivElement, WelcomeMessagePro role="group" aria-labelledby={ id } className={ cx( - 'inline-block p-3 rounded border border-gray-300 lg:max-w-[70%] select-text bg-white', + 'inline-block p-3 rounded border border-frame-border lg:max-w-[70%] select-text bg-frame', className ) } > @@ -95,10 +95,10 @@ const WelcomeComponent = React.forwardRef< HTMLDivElement, WelcomeComponentProps return ( - + - + ); diff --git a/apps/studio/src/components/wordpress-logo-circle.tsx b/apps/studio/src/components/wordpress-logo-circle.tsx index 86a9fefbf2..077283fbe0 100644 --- a/apps/studio/src/components/wordpress-logo-circle.tsx +++ b/apps/studio/src/components/wordpress-logo-circle.tsx @@ -1,6 +1,6 @@ export function WordPressLogoCircle( { size = 16, - color = '#3858E9', + color = 'var(--color-frame-theme)', }: { size?: number; color?: string; diff --git a/apps/studio/src/components/wordpress-short-logo.tsx b/apps/studio/src/components/wordpress-short-logo.tsx index 68e705d970..1a2d88f42b 100644 --- a/apps/studio/src/components/wordpress-short-logo.tsx +++ b/apps/studio/src/components/wordpress-short-logo.tsx @@ -10,36 +10,37 @@ export const WordPressShortLogo: React.FC< WordPressShortLogoProps > = ( { class fill="none" xmlns="http://www.w3.org/2000/svg" className={ className } + style={ { color: 'var(--color-frame-theme)' } } > ); diff --git a/apps/studio/src/index.css b/apps/studio/src/index.css index b4d5e8ca62..46eda8a7b9 100644 --- a/apps/studio/src/index.css +++ b/apps/studio/src/index.css @@ -3,6 +3,46 @@ @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-theme: #3858e9; + --color-frame-theme-hover: #2145e6; + --color-frame-code-text: #1d2327; + --color-frame-running: #008a20; + --color-frame-error: #d63638; + --color-frame-tab-active: #000; + --wp-admin-theme-color: var( --color-frame-theme ); + --wp-admin-theme-color-darker-20: var( --color-frame-theme-hover ); +} + +@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-theme: #6b8aff; + --color-frame-theme-hover: #8da6ff; + --color-frame-code-text: #e0e0e0; + --color-frame-running: #1ed15a; + --color-frame-error: #f87171; + --color-frame-tab-active: #fff; + } +} + @layer utilities { .interpolate-size-allow-keywords { interpolate-size: allow-keywords; @@ -49,6 +89,11 @@ blockquote { text-wrap: pretty; } +/* Settings modal — reduce WP's default 128px viewport margin to 32px */ +.components-modal__frame.has-size-medium { + max-height: calc( 100% - 32px ); +} + /* Tabs */ .components-tab-panel__tab-content { flex: 1; @@ -57,7 +102,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 +122,34 @@ blockquote { content: url( 'data:image/svg+xml;utf8,' ); } +@media ( prefers-color-scheme: dark ) { + .components-tab-panel__tabs--assistant::before { + content: url( 'data:image/svg+xml;utf8,' ); + } + + .components-tab-panel__tabs--assistant:hover:not( :active )::before { + content: url( 'data:image/svg+xml;utf8,' ); + } + + .components-button.is-secondary:hover span { + color: var( --color-frame-theme ); + } +} + +/* WP component theme — wire foreground/background so Emotion-styled components + (ProgressBar, etc.) adapt to dark mode via color-mix(). */ +:root { + --wp-components-color-foreground: var( --color-frame-text ); + --wp-components-color-background: var( --color-frame-bg ); +} + +/* WP ProgressBar indicator — override Emotion's foreground color-mix with theme blue */ +div:has( > progress ) > div:first-child { + background-color: var( --color-frame-theme ) !important; +} + .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 +168,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 +179,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 +198,24 @@ blockquote { } .assistant-markdown a { - color: #3858e9; + color: var( --color-frame-theme ); } .assistant-markdown a:hover, .assistant-markdown a:focus { - color: #2145e6; + color: var( --color-frame-theme-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 +224,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; @@ -163,8 +234,8 @@ blockquote { } .assistant-markdown code.file-block { - color: theme( 'colors.a8c-blue.50' ); - fill: theme( 'colors.a8c-blue.50' ); + color: var( --color-frame-theme ); + fill: var( --color-frame-theme ); display: inline-flex; align-items: center; padding: 0 0 0 3px; @@ -172,8 +243,8 @@ blockquote { } .assistant-markdown code.file-block:hover { - color: theme( 'colors.a8c-blue.70' ); - fill: theme( 'colors.a8c-blue.70' ); + color: var( --color-frame-theme-hover ); + fill: var( --color-frame-theme-hover ); } .assistant-markdown pre { @@ -222,7 +293,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 +335,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 +358,7 @@ blockquote { } .error-message { - color: #d63638; + color: var( --color-frame-error ); } .error-select-control .components-input-control__backdrop { @@ -299,10 +370,150 @@ 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' ); + border: 1px solid var( --color-frame-theme ); +} + +/* 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/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 15f0da1eca..84e828e815 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -116,10 +116,12 @@ export { } from 'src/modules/preview-site/lib/ipc-handlers'; export { + getColorScheme, getInstalledAppsAndTerminals, getUserEditor, getUserLocale, getUserTerminal, + saveColorScheme, saveUserEditor, saveUserLocale, saveUserTerminal, diff --git a/apps/studio/src/main-window.ts b/apps/studio/src/main-window.ts index 833e76ffa8..e0a322e082 100644 --- a/apps/studio/src/main-window.ts +++ b/apps/studio/src/main-window.ts @@ -1,4 +1,10 @@ -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 +63,9 @@ export async function createMainWindow(): Promise< BrowserWindow > { return mainWindow; } + const userData = await loadUserData(); + nativeTheme.themeSource = userData.colorScheme ?? 'system'; + const savedBounds = await loadWindowBounds(); let windowOptions: BrowserWindowConstructorOptions = { height: MAIN_MIN_HEIGHT, @@ -96,11 +105,8 @@ export async function createMainWindow(): Promise< BrowserWindow > { // Open the DevTools if the user had it open last time they used the app. // During development the dev tools default to open. - void loadUserData().then( ( userData ) => { - const { devToolsOpen, sites } = userData; - setupDevTools( mainWindow, devToolsOpen ); - initializePortFinder( sites ); - } ); + setupDevTools( mainWindow, userData.devToolsOpen ); + initializePortFinder( userData.sites ); mainWindow.webContents.on( 'devtools-opened', async () => { await updateAppdata( { devToolsOpen: true } ); diff --git a/apps/studio/src/modules/add-site/components/blueprint-details.tsx b/apps/studio/src/modules/add-site/components/blueprint-details.tsx index 1cac37ce9c..1b3b6d5386 100644 --- a/apps/studio/src/modules/add-site/components/blueprint-details.tsx +++ b/apps/studio/src/modules/add-site/components/blueprint-details.tsx @@ -39,7 +39,7 @@ export default function BlueprintDetails( { return ( - + { __( 'Start from a Blueprint' ) } - + - + - + { blueprintTitle } { blueprintDescription && ( @@ -71,10 +71,10 @@ export default function BlueprintDetails( { - - { sourceLabel } + + { sourceLabel } diff --git a/apps/studio/src/modules/add-site/components/blueprint-icon.tsx b/apps/studio/src/modules/add-site/components/blueprint-icon.tsx index 9e4233ec01..a661249692 100644 --- a/apps/studio/src/modules/add-site/components/blueprint-icon.tsx +++ b/apps/studio/src/modules/add-site/components/blueprint-icon.tsx @@ -5,52 +5,53 @@ export const BlueprintIcon = ( { size = 32 }: { size?: number } ) => ( viewBox={ `0 0 ${ size } ${ size }` } fill="none" xmlns="http://www.w3.org/2000/svg" + style={ { color: 'var(--color-frame-theme)' } } > - - { fileName } + + { fileName } { warnings?.length && @@ -53,7 +53,7 @@ function BlueprintIssuesModal( { { warnings?.map( ( warningItem, index ) => ( @@ -68,7 +68,7 @@ function BlueprintIssuesModal( { - + { __( 'Your Blueprint will still work, but these features will be skipped during site creation.' ) } diff --git a/apps/studio/src/modules/add-site/components/blueprints.css b/apps/studio/src/modules/add-site/components/blueprints.css index 7e3585d112..9b19ad367f 100644 --- a/apps/studio/src/modules/add-site/components/blueprints.css +++ b/apps/studio/src/modules/add-site/components/blueprints.css @@ -14,7 +14,7 @@ } .blueprints-container .dataviews-view-grid__card:has( .is-selected ) { - @apply outline outline-1 outline-a8c-blue-50; + @apply outline outline-1 outline-frame-theme; } /* Small icon on the bottom right of the card if selected (bottom left in RTL) */ diff --git a/apps/studio/src/modules/add-site/components/blueprints.tsx b/apps/studio/src/modules/add-site/components/blueprints.tsx index 8f06f6651c..544109d9f9 100644 --- a/apps/studio/src/modules/add-site/components/blueprints.tsx +++ b/apps/studio/src/modules/add-site/components/blueprints.tsx @@ -122,7 +122,7 @@ export function AddSiteBlueprintSelector( { render: ( { item }: { item: DataViewBlueprint } ) => ( - + { item.title } ) => handlePreviewClick( e, item ) } @@ -168,7 +168,7 @@ export function AddSiteBlueprintSelector( { render: ( { item }: { item: DataViewBlueprint } ) => ( handleBlueprintClick( item ) }> - + { __( 'Start from a Blueprint' ) } { __( 'Loading Blueprints...' ) } @@ -280,10 +280,10 @@ export function AddSiteBlueprintSelector( { return ( - + { __( 'Start from a Blueprint' ) } - + { createInterpolateElement( __( 'Create a new site from a featured Blueprint on your own. ' ), { @@ -314,7 +314,7 @@ export function AddSiteBlueprintSelector( { { selectedFileName ? ( { selectedFileName } diff --git a/apps/studio/src/modules/add-site/components/create-site-form.tsx b/apps/studio/src/modules/add-site/components/create-site-form.tsx index 05d98b5806..734af6dcc4 100644 --- a/apps/studio/src/modules/add-site/components/create-site-form.tsx +++ b/apps/studio/src/modules/add-site/components/create-site-form.tsx @@ -117,7 +117,7 @@ function FormPathInputComponent( { type="button" aria-label={ `${ value }, ${ __( 'Select different local path' ) }` } className={ cx( - 'flex flex-row items-stretch rounded-sm border border-[#949494] focus:border-a8c-blue-50 focus:shadow-[0_0_0_0.5px_black] focus:shadow-a8c-blue-50 outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-a8c-blue-50 [&:disabled]:cursor-not-allowed', + 'flex flex-row items-stretch rounded-sm border border-frame-border focus:border-frame-theme focus:shadow-[0_0_0_0.5px_black] focus:shadow-frame-theme outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-frame-theme [&:disabled]:cursor-not-allowed', error && 'border-red-500 [&_.local-path-icon]:border-l-red-500' ) } data-testid="select-path-button" @@ -136,7 +136,7 @@ function FormPathInputComponent( { aria-hidden="true" className="local-path-icon flex items-center py-[9px] px-2.5 self-center" > - + diff --git a/apps/studio/src/modules/add-site/components/create-site.tsx b/apps/studio/src/modules/add-site/components/create-site.tsx index f36dc43d59..8dad157a83 100644 --- a/apps/studio/src/modules/add-site/components/create-site.tsx +++ b/apps/studio/src/modules/add-site/components/create-site.tsx @@ -46,8 +46,8 @@ export default function CreateSite( { const { __ } = useI18n(); return ( - - + + { __( 'Add a site' ) } diff --git a/apps/studio/src/modules/add-site/components/import-backup.tsx b/apps/studio/src/modules/add-site/components/import-backup.tsx index 6c289ff96a..97f36806a1 100644 --- a/apps/studio/src/modules/add-site/components/import-backup.tsx +++ b/apps/studio/src/modules/add-site/components/import-backup.tsx @@ -134,7 +134,7 @@ export default function ImportBackup( { return ( - + { __( 'Import from a backup' ) } @@ -144,7 +144,7 @@ export default function ImportBackup( { 'transition-colors cursor-pointer', isDragging ? 'border-blue-500 bg-blue-50' - : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' + : 'border-frame-border hover:border-frame-text-secondary hover:bg-frame-surface' ) } onDragOver={ handleDragOver } onDragLeave={ handleDragLeave } @@ -161,14 +161,14 @@ export default function ImportBackup( { { selectedFile ? ( { truncateMiddle( selectedFile.name ) } - + { formatFileSize( selectedFile.size ) } ) : ( - + { isDragging ? ( diff --git a/apps/studio/src/modules/add-site/components/options.tsx b/apps/studio/src/modules/add-site/components/options.tsx index e1e7c2deb7..2f8c60c2b1 100644 --- a/apps/studio/src/modules/add-site/components/options.tsx +++ b/apps/studio/src/modules/add-site/components/options.tsx @@ -52,9 +52,9 @@ function OptionButton( { { title } - + { description } - + ); @@ -89,14 +94,14 @@ export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps return ( - + { __( 'Add a site' ) } - + { __( 'Add a clean site, start from a Blueprint or import site from a backup' ) } } + icon={ } title={ __( 'Create a site' ) } description={ __( 'Start with an empty site' ) } onClick={ () => onOptionSelect( 'create' ) } @@ -113,7 +118,7 @@ export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps /> ) } } + icon={ } title={ __( 'Pull an existing site' ) } description={ __( 'Download directly from WordPress.com or Pressable' ) } onClick={ () => onOptionSelect( 'pullRemote' ) } @@ -121,7 +126,7 @@ export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps disabledTooltip={ importOfflineMessage } /> } + icon={ } title={ __( 'Import from a backup' ) } description={ __( 'Start a site from a backup' ) } onClick={ () => onOptionSelect( 'backup' ) } diff --git a/apps/studio/src/modules/add-site/components/pull-remote-site.tsx b/apps/studio/src/modules/add-site/components/pull-remote-site.tsx index ba459a7891..a6f22ebfbb 100644 --- a/apps/studio/src/modules/add-site/components/pull-remote-site.tsx +++ b/apps/studio/src/modules/add-site/components/pull-remote-site.tsx @@ -40,7 +40,7 @@ function SiteSyncDescription( { children }: PropsWithChildren ) { __( 'Start working locally with your site data.' ), ].map( ( text ) => ( - + { text } ) ) } @@ -61,7 +61,7 @@ function NoWpcomSitesView() { { __( 'Find a perfect plan' ) } - + @@ -108,7 +108,7 @@ function NoAuthPullRemoteSiteView() { { if ( isOffline ) { return; @@ -157,11 +157,11 @@ export function PullRemoteSite( { return ( - + { __( 'Pull an existing site' ) } { isAuthenticated ? ( - + { showNoSitesView ? ( ) : ( diff --git a/apps/studio/src/modules/add-site/components/stepper.tsx b/apps/studio/src/modules/add-site/components/stepper.tsx index 5f7ad4a421..0e773b9476 100644 --- a/apps/studio/src/modules/add-site/components/stepper.tsx +++ b/apps/studio/src/modules/add-site/components/stepper.tsx @@ -78,7 +78,9 @@ export default function Stepper( { { stepNumber } @@ -86,7 +88,7 @@ export default function Stepper( { { step.label } diff --git a/apps/studio/src/modules/onboarding/components/connect-to-wpcom.tsx b/apps/studio/src/modules/onboarding/components/connect-to-wpcom.tsx index e5eeb094bf..ac75998d1f 100644 --- a/apps/studio/src/modules/onboarding/components/connect-to-wpcom.tsx +++ b/apps/studio/src/modules/onboarding/components/connect-to-wpcom.tsx @@ -35,7 +35,7 @@ export function OnboardingConnectToWpcom( { onSkip }: { onSkip: () => void } ) { __( 'Get smart suggestions from the Studio Assistant' ), ].map( ( text ) => ( - + { text } ) ) } @@ -77,7 +77,7 @@ export function OnboardingConnectToWpcom( { onSkip }: { onSkip: () => void } ) { { if ( isOffline ) { return; diff --git a/apps/studio/src/modules/onboarding/index.tsx b/apps/studio/src/modules/onboarding/index.tsx index be4446c11c..538d4dec65 100644 --- a/apps/studio/src/modules/onboarding/index.tsx +++ b/apps/studio/src/modules/onboarding/index.tsx @@ -15,7 +15,7 @@ const GradientBox = () => { className="gap-0 flex flex-col font-normal text-[42px] leading-[42px] text-white" > - + { __( 'Imagine' ) } { __( 'Create' ) } { __( 'Design' ) } @@ -49,14 +49,14 @@ export function Onboarding() { return ( - + - + diff --git a/apps/studio/src/modules/preview-site/components/preview-site-row.tsx b/apps/studio/src/modules/preview-site/components/preview-site-row.tsx index d2fae0f02e..d0b6a9257c 100644 --- a/apps/studio/src/modules/preview-site/components/preview-site-row.tsx +++ b/apps/studio/src/modules/preview-site/components/preview-site-row.tsx @@ -137,7 +137,7 @@ export function PreviewSiteRow( { disabled={ isSiteInactive } className={ cx( '!text-a8c-gray-700 max-w-full', - isSiteInactive ? 'pointer-events-none' : 'hover:!text-a8c-blue-50' + isSiteInactive ? 'pointer-events-none' : 'hover:!text-frame-theme' ) } onClick={ () => getIpcApi().openURL( `https://${ url }` ) } > @@ -154,7 +154,7 @@ export function PreviewSiteRow( { { updateOperation?.status === 'pending' ? ( - + { __( 'Updating' ) } @@ -182,7 +182,7 @@ export function PreviewSiteRow( { } ) ); } } - className={ '!text-a8c-blue-50 hover:!text-a8c-red-50' } + className={ '!text-frame-theme hover:!text-a8c-red-50' } > { __( 'Clear' ) } 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 = ( { { getIpcApi().openURL( connectedSite.url ); } } @@ -569,7 +569,7 @@ const SyncConnectedSiteSection = ( { } return ( - + { logo } @@ -586,7 +586,9 @@ const SyncConnectedSiteSection = ( { - + { createInterpolateElement( __( ' appears to be deleted or is currently unreachable. Get help ↗' 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( { { syncFrom } @@ -369,7 +369,7 @@ export function SyncDialog( { ) }`; const backupDate = format( parseInt( rewindId ) * 1000, 'MMM d, y, h:mm a' ); return ( - + { sprintf( __( 'Content from the latest backup: %s.' ), backupDate ) }{ ' ' } { if ( nodeId === 'wp-content' && type === 'push' && localFileTreeError ) { return ( - + { __( 'Could not load files. Please close and reopen this dialog to try again.' ) } @@ -398,7 +398,7 @@ export function SyncDialog( { node.hasError ) { return ( - + { __( 'Error retrieving remote files and directories. Please close and reopen this dialog to try again.' ) } @@ -406,7 +406,10 @@ export function SyncDialog( { ); } return ( - + { __( 'Empty' ) } ); @@ -417,7 +420,7 @@ export function SyncDialog( { - + { type === 'push' && ( { isOffline && ( - + ) } @@ -123,9 +123,9 @@ function SearchSites( { const { __ } = useI18n(); const locale = useI18nLocale(); return ( - + { setSearchQuery( value ); @@ -134,7 +134,7 @@ function SearchSites( { autoFocus __nextHasNoMarginBottom={ true } /> - + { __( "Can't find your site?" ) }{ ' ' } getIpcApi().openURL( site.url ) } onKeyDown={ ( e: React.KeyboardEvent ) => { @@ -426,12 +426,12 @@ function Footer( { }, [ disabled ] ); return ( - + diff --git a/apps/studio/src/modules/sync/components/sync-tab-image.tsx b/apps/studio/src/modules/sync/components/sync-tab-image.tsx index 4cab01d8d1..0765b33517 100644 --- a/apps/studio/src/modules/sync/components/sync-tab-image.tsx +++ b/apps/studio/src/modules/sync/components/sync-tab-image.tsx @@ -9,7 +9,7 @@ export const SyncTabImage = () => ( > - + @@ -44,7 +44,7 @@ export const SyncTabImage = () => ( ( /> - + - + ( - + { text } ) ) } @@ -103,7 +103,7 @@ function NoAuthSyncTab() { { if ( isOffline ) { return; @@ -203,7 +203,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } disconnectSite( { siteId: id, localSiteId: selectedSite.id } ) } /> - + dispatch( connectedSitesActions.openModal( 'connect' ) ) } diff --git a/apps/studio/src/modules/sync/tests/environment-badge.test.tsx b/apps/studio/src/modules/sync/tests/environment-badge.test.tsx index e96bf19dee..63d725b2dc 100644 --- a/apps/studio/src/modules/sync/tests/environment-badge.test.tsx +++ b/apps/studio/src/modules/sync/tests/environment-badge.test.tsx @@ -56,7 +56,7 @@ describe( 'EnvironmentBadge', () => { const badgeElement = container.firstChild as HTMLElement; expect( badgeElement ).toBeInTheDocument(); - expect( badgeElement.className ).toContain( 'bg-white' ); - expect( badgeElement.className ).toContain( 'text-a8c-blue-50' ); + expect( badgeElement.className ).toContain( 'bg-frame' ); + expect( badgeElement.className ).toContain( 'text-frame-theme' ); } ); } ); diff --git a/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx b/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx new file mode 100644 index 0000000000..adf2572ac9 --- /dev/null +++ b/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx @@ -0,0 +1,66 @@ +import { useI18n } from '@wordpress/react-i18n'; +import { cx } from 'src/lib/cx'; +import darkAppearanceIllustration from '../../../../assets/appearance-dark.svg'; +import lightAppearanceIllustration from '../../../../assets/appearance-light.svg'; +import systemAppearanceIllustration from '../../../../assets/appearance-system.svg'; +import { SettingsFormField } from './settings-form-field'; + +interface ColorSchemePickerProps { + value: 'system' | 'light' | 'dark'; + onChange: ( value: 'system' | 'light' | 'dark' ) => void; +} + +export const ColorSchemePicker = ( { value, onChange }: ColorSchemePickerProps ) => { + const { __ } = useI18n(); + const colorSchemeOptions: Array< { + value: 'system' | 'light' | 'dark'; + label: string; + illustration: string; + } > = [ + { value: 'system', label: __( 'System' ), illustration: systemAppearanceIllustration }, + { value: 'light', label: __( 'Light' ), illustration: lightAppearanceIllustration }, + { value: 'dark', label: __( 'Dark' ), illustration: darkAppearanceIllustration }, + ]; + + return ( + + + { colorSchemeOptions.map( ( option ) => { + const isSelected = value === option.value; + + return ( + onChange( option.value ) } + className={ cx( + 'group flex flex-col items-center p-0 bg-transparent rounded-[4px] focus-visible:outline-none focus-visible:outline focus-visible:outline-[2px] focus-visible:outline-frame-theme focus-visible:outline-offset-[4px]' + ) } + > + + + { option.label } + + + ); + } ) } + + + ); +}; diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx index f88d75ca89..6ed18d09c5 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -3,6 +3,7 @@ import { useI18n } from '@wordpress/react-i18n'; import { useState } from 'react'; import Button from 'src/components/button'; import { isWindowsStore } from 'src/lib/app-globals'; +import { ColorSchemePicker } from 'src/modules/user-settings/components/color-scheme-picker'; import { EditorPicker } from 'src/modules/user-settings/components/editor-picker'; import { LanguagePicker } from 'src/modules/user-settings/components/language-picker'; import { StudioCliToggle } from 'src/modules/user-settings/components/studio-cli-toggle'; @@ -12,8 +13,10 @@ import { SupportedTerminal } from 'src/modules/user-settings/lib/terminal'; import { useAppDispatch, useI18nLocale } from 'src/stores'; import { saveUserLocale } from 'src/stores/i18n-slice'; import { + useGetColorSchemeQuery, useGetUserEditorQuery, useGetUserTerminalQuery, + useSaveColorSchemeMutation, useSaveUserEditorMutation, useSaveUserTerminalMutation, useGetStudioCliIsInstalledQuery, @@ -25,10 +28,12 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const savedLocale = useI18nLocale(); const dispatch = useAppDispatch(); + const { data: colorScheme } = useGetColorSchemeQuery(); const { data: editor } = useGetUserEditorQuery(); const { data: terminal } = useGetUserTerminalQuery(); const { data: isCliInstalled } = useGetStudioCliIsInstalledQuery(); + const [ saveColorSchemePreference ] = useSaveColorSchemeMutation(); const [ saveEditor ] = useSaveUserEditorMutation(); const [ saveTerminal ] = useSaveUserTerminalMutation(); const [ saveCliIsInstalled ] = useSaveStudioCliIsInstalledMutation(); @@ -68,13 +73,19 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { return ( <> - - saveColorSchemePreference( value ) } /> - + + + + + { ! isWindowsStore() && ( ) } diff --git a/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx b/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx index 679017b088..e8675ce89d 100644 --- a/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx +++ b/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx @@ -16,8 +16,9 @@ export function StudioCliToggle( { value, onChange }: StudioCLIToggleProps ) { return ( - + onChange( event.target.checked ) } diff --git a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts index 25019a3a94..2e4dd1c2b3 100644 --- a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts +++ b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, IpcMainInvokeEvent } from 'electron'; +import { BrowserWindow, IpcMainInvokeEvent, nativeTheme } from 'electron'; import { DEFAULT_TERMINAL } from 'src/constants'; import { sendIpcEventToRenderer, sendIpcEventToRendererWithWindow } from 'src/ipc-utils'; import { isInstalled } from 'src/lib/is-installed'; @@ -58,6 +58,19 @@ export async function getUserEditor(): Promise< SupportedEditor | null > { return userData.preferredEditor ?? null; } +export async function saveColorScheme( + event: IpcMainInvokeEvent, + colorScheme: 'system' | 'light' | 'dark' +) { + nativeTheme.themeSource = colorScheme; + await updateAppdata( { colorScheme } ); +} + +export async function getColorScheme(): Promise< 'system' | 'light' | 'dark' > { + const userData = await loadUserData(); + return userData.colorScheme ?? 'system'; +} + export function showUserSettings( event: IpcMainInvokeEvent, tabName?: UserSettingsTabName ) { const parentWindow = BrowserWindow.fromWebContents( event.sender ); sendIpcEventToRendererWithWindow( parentWindow, 'user-settings', { tabName } ); diff --git a/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg b/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg new file mode 100644 index 0000000000..30ddaa762c --- /dev/null +++ b/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx b/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx index ca4af529dd..3661531e48 100644 --- a/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx +++ b/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx @@ -7,8 +7,7 @@ import { getIpcApi } from 'src/lib/get-ipc-api'; import { getLocalizedLink } from 'src/lib/get-localized-link'; import blueprintsIllustration from 'src/modules/whats-new/assets/blueprints-illustration.svg'; import cliIllustration from 'src/modules/whats-new/assets/cli-illustration.svg'; -import pressableSyncIllustration from 'src/modules/whats-new/assets/pressable-sync-illustration.svg'; -import selectiveSyncIllustration from 'src/modules/whats-new/assets/selective-sync-illustration.svg'; +import darkModeIllustration from 'src/modules/whats-new/assets/dark-mode-illustration.svg'; import { useI18nLocale } from 'src/stores'; interface WhatsNewPage { @@ -32,10 +31,10 @@ const PageContent = ( { isIntroPage = false, }: Omit< WhatsNewPage, 'image' > & { isIntroPage?: boolean } ) => ( - { title } + { title } @@ -45,7 +44,7 @@ const PageContent = ( { { learnMoreUrl && ( getIpcApi().openURL( learnMoreUrl ) } - className="text-a8c-blue-50 text-m leading-s cursor-pointer" + className="text-frame-theme text-m leading-s cursor-pointer" > { learnMoreLabel || __( 'Learn more' ) } @@ -57,6 +56,13 @@ const PageContent = ( { export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProps ) { const locale = useI18nLocale(); const whatsNewPages: WhatsNewPage[] = [ + { + image: darkModeIllustration, + title: __( 'Dark mode is here' ), + description: __( + 'Studio now supports light, dark, and system appearance modes. Head to Settings to choose your preferred look.' + ), + }, { image: cliIllustration, title: __( 'WP-CLI support and CLI site management' ), @@ -82,22 +88,6 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp ), learnMoreUrl: getLocalizedLink( locale, 'docsBlueprints' ), }, - { - image: selectiveSyncIllustration, - title: __( 'Synchronize with precision' ), - description: __( - 'Synchronize specific plugins, themes, or the database for fast, precise updates to your WordPress.com or Pressable sites.' - ), - learnMoreUrl: `${ getLocalizedLink( locale, 'docsSync' ) }#pull`, - }, - { - image: pressableSyncIllustration, - title: __( 'Sync to your favorite host' ), - description: __( - 'Pull and push your Studio sites to WordPress.com or Pressable with a single click. No more manual uploads or FTP transfers!' - ), - learnMoreUrl: getLocalizedLink( locale, 'docsSync' ), - }, ]; if ( ! showModal ) { @@ -109,7 +99,7 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp onFinish={ onClose } contentLabel={ __( "What's New in Studio" ) } className={ cx( - 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-a8c-blue-50', + 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-frame-theme', '[&_*]:select-none', 'focus:outline-none' ) } diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index e5f92d685f..2e707143e8 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -144,6 +144,8 @@ const api: IpcApi = { saveUserTerminal: ( preferredTerminal ) => ipcRendererInvoke( 'saveUserTerminal', preferredTerminal ), getUserTerminal: () => ipcRendererInvoke( 'getUserTerminal' ), + saveColorScheme: ( colorScheme ) => ipcRendererInvoke( 'saveColorScheme', colorScheme ), + getColorScheme: () => ipcRendererInvoke( 'getColorScheme' ), getUserEditor: () => ipcRendererInvoke( 'getUserEditor' ), saveUserEditor: ( editor ) => ipcRendererInvoke( 'saveUserEditor', editor ), comparePaths: ( path1, path2 ) => ipcRendererInvoke( 'comparePaths', path1, path2 ), diff --git a/apps/studio/src/storage/storage-types.ts b/apps/studio/src/storage/storage-types.ts index 199c7b7719..0909dfd0a2 100644 --- a/apps/studio/src/storage/storage-types.ts +++ b/apps/studio/src/storage/storage-types.ts @@ -31,6 +31,7 @@ export interface UserData { lastSeenVersion?: string; preferredTerminal?: SupportedTerminal; preferredEditor?: SupportedEditor; + colorScheme?: 'system' | 'light' | 'dark'; betaFeatures?: BetaFeatures; stopSitesOnQuit?: boolean; } diff --git a/apps/studio/src/stores/installed-apps-api.ts b/apps/studio/src/stores/installed-apps-api.ts index b5684ebd18..07b2700489 100644 --- a/apps/studio/src/stores/installed-apps-api.ts +++ b/apps/studio/src/stores/installed-apps-api.ts @@ -27,7 +27,13 @@ const getFirstInstalledEditor = async (): Promise< SupportedEditor | null > => { export const installedAppsApi = createApi( { reducerPath: 'installedAppsApi', baseQuery: fetchBaseQuery(), - tagTypes: [ 'StudioCliIsInstalled', 'InstalledApps', 'UserEditor', 'UserTerminal' ], + tagTypes: [ + 'StudioCliIsInstalled', + 'InstalledApps', + 'UserEditor', + 'UserTerminal', + 'ColorScheme', + ], endpoints: ( builder ) => ( { getStudioCliIsInstalled: builder.query< boolean, void >( { queryFn: async () => { @@ -91,6 +97,20 @@ export const installedAppsApi = createApi( { }, invalidatesTags: [ 'UserTerminal' ], } ), + getColorScheme: builder.query< 'system' | 'light' | 'dark', void >( { + queryFn: async () => { + const colorScheme = await getIpcApi().getColorScheme(); + return { data: colorScheme }; + }, + providesTags: [ 'ColorScheme' ], + } ), + saveColorScheme: builder.mutation< 'system' | 'light' | 'dark', 'system' | 'light' | 'dark' >( { + queryFn: async ( colorScheme ) => { + await getIpcApi().saveColorScheme( colorScheme ); + return { data: colorScheme }; + }, + invalidatesTags: [ 'ColorScheme' ], + } ), } ), } ); @@ -102,6 +122,8 @@ export const { useSaveUserTerminalMutation, useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, + useGetColorSchemeQuery, + useSaveColorSchemeMutation, } = installedAppsApi; export const selectInstalledEditors = createSelector( diff --git a/apps/studio/src/tests/main-window.test.ts b/apps/studio/src/tests/main-window.test.ts index f0119b3bb8..f9261d5fa7 100644 --- a/apps/studio/src/tests/main-window.test.ts +++ b/apps/studio/src/tests/main-window.test.ts @@ -74,6 +74,12 @@ vi.mock( 'electron', () => { dialog: { showMessageBox: vi.fn(), }, + nativeTheme: { + themeSource: 'system', + }, + screen: { + getAllDisplays: vi.fn().mockReturnValue( [] ), + }, BrowserWindow: MockBrowserWindow, shell: { trashItem: vi.fn(), diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index 930453cc59..52267fde4d 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -153,6 +153,19 @@ 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-theme': 'var(--color-frame-theme)', + 'frame-theme-hover': 'var(--color-frame-theme-hover)', + 'frame-code-text': 'var(--color-frame-code-text)', + 'frame-running': 'var(--color-frame-running)', + 'frame-error': 'var(--color-frame-error)', + 'frame-tab-active': 'var(--color-frame-tab-active)', }, spacing: { chrome: `${ APP_CHROME_SPACING }px`,
{ __( 'We’ve logged the issue to help us track down the problem.' ) }
{ __( 'Try restarting the app, if the problem persists' ) }{ ' ' } {
{ __( 'Select a site to view details.' ) }
+ { __( 'Select a site to view details.' ) } +
{ __( 'Imagine' ) }
{ __( 'Create' ) }
{ __( 'Design' ) }
+
{ __( "Can't find your site?" ) }{ ' ' } getIpcApi().openURL( site.url ) } onKeyDown={ ( e: React.KeyboardEvent ) => { @@ -426,12 +426,12 @@ function Footer( { }, [ disabled ] ); return ( - + diff --git a/apps/studio/src/modules/sync/components/sync-tab-image.tsx b/apps/studio/src/modules/sync/components/sync-tab-image.tsx index 4cab01d8d1..0765b33517 100644 --- a/apps/studio/src/modules/sync/components/sync-tab-image.tsx +++ b/apps/studio/src/modules/sync/components/sync-tab-image.tsx @@ -9,7 +9,7 @@ export const SyncTabImage = () => ( > - + @@ -44,7 +44,7 @@ export const SyncTabImage = () => ( ( /> - + - + ( - + { text } ) ) } @@ -103,7 +103,7 @@ function NoAuthSyncTab() { { if ( isOffline ) { return; @@ -203,7 +203,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } disconnectSite( { siteId: id, localSiteId: selectedSite.id } ) } /> - + dispatch( connectedSitesActions.openModal( 'connect' ) ) } diff --git a/apps/studio/src/modules/sync/tests/environment-badge.test.tsx b/apps/studio/src/modules/sync/tests/environment-badge.test.tsx index e96bf19dee..63d725b2dc 100644 --- a/apps/studio/src/modules/sync/tests/environment-badge.test.tsx +++ b/apps/studio/src/modules/sync/tests/environment-badge.test.tsx @@ -56,7 +56,7 @@ describe( 'EnvironmentBadge', () => { const badgeElement = container.firstChild as HTMLElement; expect( badgeElement ).toBeInTheDocument(); - expect( badgeElement.className ).toContain( 'bg-white' ); - expect( badgeElement.className ).toContain( 'text-a8c-blue-50' ); + expect( badgeElement.className ).toContain( 'bg-frame' ); + expect( badgeElement.className ).toContain( 'text-frame-theme' ); } ); } ); diff --git a/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx b/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx new file mode 100644 index 0000000000..adf2572ac9 --- /dev/null +++ b/apps/studio/src/modules/user-settings/components/color-scheme-picker.tsx @@ -0,0 +1,66 @@ +import { useI18n } from '@wordpress/react-i18n'; +import { cx } from 'src/lib/cx'; +import darkAppearanceIllustration from '../../../../assets/appearance-dark.svg'; +import lightAppearanceIllustration from '../../../../assets/appearance-light.svg'; +import systemAppearanceIllustration from '../../../../assets/appearance-system.svg'; +import { SettingsFormField } from './settings-form-field'; + +interface ColorSchemePickerProps { + value: 'system' | 'light' | 'dark'; + onChange: ( value: 'system' | 'light' | 'dark' ) => void; +} + +export const ColorSchemePicker = ( { value, onChange }: ColorSchemePickerProps ) => { + const { __ } = useI18n(); + const colorSchemeOptions: Array< { + value: 'system' | 'light' | 'dark'; + label: string; + illustration: string; + } > = [ + { value: 'system', label: __( 'System' ), illustration: systemAppearanceIllustration }, + { value: 'light', label: __( 'Light' ), illustration: lightAppearanceIllustration }, + { value: 'dark', label: __( 'Dark' ), illustration: darkAppearanceIllustration }, + ]; + + return ( + + + { colorSchemeOptions.map( ( option ) => { + const isSelected = value === option.value; + + return ( + onChange( option.value ) } + className={ cx( + 'group flex flex-col items-center p-0 bg-transparent rounded-[4px] focus-visible:outline-none focus-visible:outline focus-visible:outline-[2px] focus-visible:outline-frame-theme focus-visible:outline-offset-[4px]' + ) } + > + + + { option.label } + + + ); + } ) } + + + ); +}; diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx index f88d75ca89..6ed18d09c5 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -3,6 +3,7 @@ import { useI18n } from '@wordpress/react-i18n'; import { useState } from 'react'; import Button from 'src/components/button'; import { isWindowsStore } from 'src/lib/app-globals'; +import { ColorSchemePicker } from 'src/modules/user-settings/components/color-scheme-picker'; import { EditorPicker } from 'src/modules/user-settings/components/editor-picker'; import { LanguagePicker } from 'src/modules/user-settings/components/language-picker'; import { StudioCliToggle } from 'src/modules/user-settings/components/studio-cli-toggle'; @@ -12,8 +13,10 @@ import { SupportedTerminal } from 'src/modules/user-settings/lib/terminal'; import { useAppDispatch, useI18nLocale } from 'src/stores'; import { saveUserLocale } from 'src/stores/i18n-slice'; import { + useGetColorSchemeQuery, useGetUserEditorQuery, useGetUserTerminalQuery, + useSaveColorSchemeMutation, useSaveUserEditorMutation, useSaveUserTerminalMutation, useGetStudioCliIsInstalledQuery, @@ -25,10 +28,12 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const savedLocale = useI18nLocale(); const dispatch = useAppDispatch(); + const { data: colorScheme } = useGetColorSchemeQuery(); const { data: editor } = useGetUserEditorQuery(); const { data: terminal } = useGetUserTerminalQuery(); const { data: isCliInstalled } = useGetStudioCliIsInstalledQuery(); + const [ saveColorSchemePreference ] = useSaveColorSchemeMutation(); const [ saveEditor ] = useSaveUserEditorMutation(); const [ saveTerminal ] = useSaveUserTerminalMutation(); const [ saveCliIsInstalled ] = useSaveStudioCliIsInstalledMutation(); @@ -68,13 +73,19 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { return ( <> - - saveColorSchemePreference( value ) } /> - + + + + + { ! isWindowsStore() && ( ) } diff --git a/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx b/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx index 679017b088..e8675ce89d 100644 --- a/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx +++ b/apps/studio/src/modules/user-settings/components/studio-cli-toggle.tsx @@ -16,8 +16,9 @@ export function StudioCliToggle( { value, onChange }: StudioCLIToggleProps ) { return ( - + onChange( event.target.checked ) } diff --git a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts index 25019a3a94..2e4dd1c2b3 100644 --- a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts +++ b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, IpcMainInvokeEvent } from 'electron'; +import { BrowserWindow, IpcMainInvokeEvent, nativeTheme } from 'electron'; import { DEFAULT_TERMINAL } from 'src/constants'; import { sendIpcEventToRenderer, sendIpcEventToRendererWithWindow } from 'src/ipc-utils'; import { isInstalled } from 'src/lib/is-installed'; @@ -58,6 +58,19 @@ export async function getUserEditor(): Promise< SupportedEditor | null > { return userData.preferredEditor ?? null; } +export async function saveColorScheme( + event: IpcMainInvokeEvent, + colorScheme: 'system' | 'light' | 'dark' +) { + nativeTheme.themeSource = colorScheme; + await updateAppdata( { colorScheme } ); +} + +export async function getColorScheme(): Promise< 'system' | 'light' | 'dark' > { + const userData = await loadUserData(); + return userData.colorScheme ?? 'system'; +} + export function showUserSettings( event: IpcMainInvokeEvent, tabName?: UserSettingsTabName ) { const parentWindow = BrowserWindow.fromWebContents( event.sender ); sendIpcEventToRendererWithWindow( parentWindow, 'user-settings', { tabName } ); diff --git a/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg b/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg new file mode 100644 index 0000000000..30ddaa762c --- /dev/null +++ b/apps/studio/src/modules/whats-new/assets/dark-mode-illustration.svg @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx b/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx index ca4af529dd..3661531e48 100644 --- a/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx +++ b/apps/studio/src/modules/whats-new/components/whats-new-modal.tsx @@ -7,8 +7,7 @@ import { getIpcApi } from 'src/lib/get-ipc-api'; import { getLocalizedLink } from 'src/lib/get-localized-link'; import blueprintsIllustration from 'src/modules/whats-new/assets/blueprints-illustration.svg'; import cliIllustration from 'src/modules/whats-new/assets/cli-illustration.svg'; -import pressableSyncIllustration from 'src/modules/whats-new/assets/pressable-sync-illustration.svg'; -import selectiveSyncIllustration from 'src/modules/whats-new/assets/selective-sync-illustration.svg'; +import darkModeIllustration from 'src/modules/whats-new/assets/dark-mode-illustration.svg'; import { useI18nLocale } from 'src/stores'; interface WhatsNewPage { @@ -32,10 +31,10 @@ const PageContent = ( { isIntroPage = false, }: Omit< WhatsNewPage, 'image' > & { isIntroPage?: boolean } ) => ( - { title } + { title } @@ -45,7 +44,7 @@ const PageContent = ( { { learnMoreUrl && ( getIpcApi().openURL( learnMoreUrl ) } - className="text-a8c-blue-50 text-m leading-s cursor-pointer" + className="text-frame-theme text-m leading-s cursor-pointer" > { learnMoreLabel || __( 'Learn more' ) } @@ -57,6 +56,13 @@ const PageContent = ( { export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProps ) { const locale = useI18nLocale(); const whatsNewPages: WhatsNewPage[] = [ + { + image: darkModeIllustration, + title: __( 'Dark mode is here' ), + description: __( + 'Studio now supports light, dark, and system appearance modes. Head to Settings to choose your preferred look.' + ), + }, { image: cliIllustration, title: __( 'WP-CLI support and CLI site management' ), @@ -82,22 +88,6 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp ), learnMoreUrl: getLocalizedLink( locale, 'docsBlueprints' ), }, - { - image: selectiveSyncIllustration, - title: __( 'Synchronize with precision' ), - description: __( - 'Synchronize specific plugins, themes, or the database for fast, precise updates to your WordPress.com or Pressable sites.' - ), - learnMoreUrl: `${ getLocalizedLink( locale, 'docsSync' ) }#pull`, - }, - { - image: pressableSyncIllustration, - title: __( 'Sync to your favorite host' ), - description: __( - 'Pull and push your Studio sites to WordPress.com or Pressable with a single click. No more manual uploads or FTP transfers!' - ), - learnMoreUrl: getLocalizedLink( locale, 'docsSync' ), - }, ]; if ( ! showModal ) { @@ -109,7 +99,7 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp onFinish={ onClose } contentLabel={ __( "What's New in Studio" ) } className={ cx( - 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-a8c-blue-50', + 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-frame-theme', '[&_*]:select-none', 'focus:outline-none' ) } diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index e5f92d685f..2e707143e8 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -144,6 +144,8 @@ const api: IpcApi = { saveUserTerminal: ( preferredTerminal ) => ipcRendererInvoke( 'saveUserTerminal', preferredTerminal ), getUserTerminal: () => ipcRendererInvoke( 'getUserTerminal' ), + saveColorScheme: ( colorScheme ) => ipcRendererInvoke( 'saveColorScheme', colorScheme ), + getColorScheme: () => ipcRendererInvoke( 'getColorScheme' ), getUserEditor: () => ipcRendererInvoke( 'getUserEditor' ), saveUserEditor: ( editor ) => ipcRendererInvoke( 'saveUserEditor', editor ), comparePaths: ( path1, path2 ) => ipcRendererInvoke( 'comparePaths', path1, path2 ), diff --git a/apps/studio/src/storage/storage-types.ts b/apps/studio/src/storage/storage-types.ts index 199c7b7719..0909dfd0a2 100644 --- a/apps/studio/src/storage/storage-types.ts +++ b/apps/studio/src/storage/storage-types.ts @@ -31,6 +31,7 @@ export interface UserData { lastSeenVersion?: string; preferredTerminal?: SupportedTerminal; preferredEditor?: SupportedEditor; + colorScheme?: 'system' | 'light' | 'dark'; betaFeatures?: BetaFeatures; stopSitesOnQuit?: boolean; } diff --git a/apps/studio/src/stores/installed-apps-api.ts b/apps/studio/src/stores/installed-apps-api.ts index b5684ebd18..07b2700489 100644 --- a/apps/studio/src/stores/installed-apps-api.ts +++ b/apps/studio/src/stores/installed-apps-api.ts @@ -27,7 +27,13 @@ const getFirstInstalledEditor = async (): Promise< SupportedEditor | null > => { export const installedAppsApi = createApi( { reducerPath: 'installedAppsApi', baseQuery: fetchBaseQuery(), - tagTypes: [ 'StudioCliIsInstalled', 'InstalledApps', 'UserEditor', 'UserTerminal' ], + tagTypes: [ + 'StudioCliIsInstalled', + 'InstalledApps', + 'UserEditor', + 'UserTerminal', + 'ColorScheme', + ], endpoints: ( builder ) => ( { getStudioCliIsInstalled: builder.query< boolean, void >( { queryFn: async () => { @@ -91,6 +97,20 @@ export const installedAppsApi = createApi( { }, invalidatesTags: [ 'UserTerminal' ], } ), + getColorScheme: builder.query< 'system' | 'light' | 'dark', void >( { + queryFn: async () => { + const colorScheme = await getIpcApi().getColorScheme(); + return { data: colorScheme }; + }, + providesTags: [ 'ColorScheme' ], + } ), + saveColorScheme: builder.mutation< 'system' | 'light' | 'dark', 'system' | 'light' | 'dark' >( { + queryFn: async ( colorScheme ) => { + await getIpcApi().saveColorScheme( colorScheme ); + return { data: colorScheme }; + }, + invalidatesTags: [ 'ColorScheme' ], + } ), } ), } ); @@ -102,6 +122,8 @@ export const { useSaveUserTerminalMutation, useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, + useGetColorSchemeQuery, + useSaveColorSchemeMutation, } = installedAppsApi; export const selectInstalledEditors = createSelector( diff --git a/apps/studio/src/tests/main-window.test.ts b/apps/studio/src/tests/main-window.test.ts index f0119b3bb8..f9261d5fa7 100644 --- a/apps/studio/src/tests/main-window.test.ts +++ b/apps/studio/src/tests/main-window.test.ts @@ -74,6 +74,12 @@ vi.mock( 'electron', () => { dialog: { showMessageBox: vi.fn(), }, + nativeTheme: { + themeSource: 'system', + }, + screen: { + getAllDisplays: vi.fn().mockReturnValue( [] ), + }, BrowserWindow: MockBrowserWindow, shell: { trashItem: vi.fn(), diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index 930453cc59..52267fde4d 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -153,6 +153,19 @@ 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-theme': 'var(--color-frame-theme)', + 'frame-theme-hover': 'var(--color-frame-theme-hover)', + 'frame-code-text': 'var(--color-frame-code-text)', + 'frame-running': 'var(--color-frame-running)', + 'frame-error': 'var(--color-frame-error)', + 'frame-tab-active': 'var(--color-frame-tab-active)', }, spacing: { chrome: `${ APP_CHROME_SPACING }px`,
@@ -45,7 +44,7 @@ const PageContent = ( { { learnMoreUrl && ( getIpcApi().openURL( learnMoreUrl ) } - className="text-a8c-blue-50 text-m leading-s cursor-pointer" + className="text-frame-theme text-m leading-s cursor-pointer" > { learnMoreLabel || __( 'Learn more' ) } @@ -57,6 +56,13 @@ const PageContent = ( { export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProps ) { const locale = useI18nLocale(); const whatsNewPages: WhatsNewPage[] = [ + { + image: darkModeIllustration, + title: __( 'Dark mode is here' ), + description: __( + 'Studio now supports light, dark, and system appearance modes. Head to Settings to choose your preferred look.' + ), + }, { image: cliIllustration, title: __( 'WP-CLI support and CLI site management' ), @@ -82,22 +88,6 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp ), learnMoreUrl: getLocalizedLink( locale, 'docsBlueprints' ), }, - { - image: selectiveSyncIllustration, - title: __( 'Synchronize with precision' ), - description: __( - 'Synchronize specific plugins, themes, or the database for fast, precise updates to your WordPress.com or Pressable sites.' - ), - learnMoreUrl: `${ getLocalizedLink( locale, 'docsSync' ) }#pull`, - }, - { - image: pressableSyncIllustration, - title: __( 'Sync to your favorite host' ), - description: __( - 'Pull and push your Studio sites to WordPress.com or Pressable with a single click. No more manual uploads or FTP transfers!' - ), - learnMoreUrl: getLocalizedLink( locale, 'docsSync' ), - }, ]; if ( ! showModal ) { @@ -109,7 +99,7 @@ export default function WhatsNewModal( { showModal, onClose }: WhatsNewModalProp onFinish={ onClose } contentLabel={ __( "What's New in Studio" ) } className={ cx( - 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-a8c-blue-50', + 'whats-new-modal !w-[360px] !h-[470px] overflow-hidden [&_.components-button.is-compact.has-icon_svg]:!fill-white [&_.components-button.is-tertiary]:!outline-1 [&_.components-button.is-tertiary]:!outline-solid [&_.components-button.is-tertiary]:!outline-frame-theme', '[&_*]:select-none', 'focus:outline-none' ) } diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index e5f92d685f..2e707143e8 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -144,6 +144,8 @@ const api: IpcApi = { saveUserTerminal: ( preferredTerminal ) => ipcRendererInvoke( 'saveUserTerminal', preferredTerminal ), getUserTerminal: () => ipcRendererInvoke( 'getUserTerminal' ), + saveColorScheme: ( colorScheme ) => ipcRendererInvoke( 'saveColorScheme', colorScheme ), + getColorScheme: () => ipcRendererInvoke( 'getColorScheme' ), getUserEditor: () => ipcRendererInvoke( 'getUserEditor' ), saveUserEditor: ( editor ) => ipcRendererInvoke( 'saveUserEditor', editor ), comparePaths: ( path1, path2 ) => ipcRendererInvoke( 'comparePaths', path1, path2 ), diff --git a/apps/studio/src/storage/storage-types.ts b/apps/studio/src/storage/storage-types.ts index 199c7b7719..0909dfd0a2 100644 --- a/apps/studio/src/storage/storage-types.ts +++ b/apps/studio/src/storage/storage-types.ts @@ -31,6 +31,7 @@ export interface UserData { lastSeenVersion?: string; preferredTerminal?: SupportedTerminal; preferredEditor?: SupportedEditor; + colorScheme?: 'system' | 'light' | 'dark'; betaFeatures?: BetaFeatures; stopSitesOnQuit?: boolean; } diff --git a/apps/studio/src/stores/installed-apps-api.ts b/apps/studio/src/stores/installed-apps-api.ts index b5684ebd18..07b2700489 100644 --- a/apps/studio/src/stores/installed-apps-api.ts +++ b/apps/studio/src/stores/installed-apps-api.ts @@ -27,7 +27,13 @@ const getFirstInstalledEditor = async (): Promise< SupportedEditor | null > => { export const installedAppsApi = createApi( { reducerPath: 'installedAppsApi', baseQuery: fetchBaseQuery(), - tagTypes: [ 'StudioCliIsInstalled', 'InstalledApps', 'UserEditor', 'UserTerminal' ], + tagTypes: [ + 'StudioCliIsInstalled', + 'InstalledApps', + 'UserEditor', + 'UserTerminal', + 'ColorScheme', + ], endpoints: ( builder ) => ( { getStudioCliIsInstalled: builder.query< boolean, void >( { queryFn: async () => { @@ -91,6 +97,20 @@ export const installedAppsApi = createApi( { }, invalidatesTags: [ 'UserTerminal' ], } ), + getColorScheme: builder.query< 'system' | 'light' | 'dark', void >( { + queryFn: async () => { + const colorScheme = await getIpcApi().getColorScheme(); + return { data: colorScheme }; + }, + providesTags: [ 'ColorScheme' ], + } ), + saveColorScheme: builder.mutation< 'system' | 'light' | 'dark', 'system' | 'light' | 'dark' >( { + queryFn: async ( colorScheme ) => { + await getIpcApi().saveColorScheme( colorScheme ); + return { data: colorScheme }; + }, + invalidatesTags: [ 'ColorScheme' ], + } ), } ), } ); @@ -102,6 +122,8 @@ export const { useSaveUserTerminalMutation, useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, + useGetColorSchemeQuery, + useSaveColorSchemeMutation, } = installedAppsApi; export const selectInstalledEditors = createSelector( diff --git a/apps/studio/src/tests/main-window.test.ts b/apps/studio/src/tests/main-window.test.ts index f0119b3bb8..f9261d5fa7 100644 --- a/apps/studio/src/tests/main-window.test.ts +++ b/apps/studio/src/tests/main-window.test.ts @@ -74,6 +74,12 @@ vi.mock( 'electron', () => { dialog: { showMessageBox: vi.fn(), }, + nativeTheme: { + themeSource: 'system', + }, + screen: { + getAllDisplays: vi.fn().mockReturnValue( [] ), + }, BrowserWindow: MockBrowserWindow, shell: { trashItem: vi.fn(), diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index 930453cc59..52267fde4d 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -153,6 +153,19 @@ 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-theme': 'var(--color-frame-theme)', + 'frame-theme-hover': 'var(--color-frame-theme-hover)', + 'frame-code-text': 'var(--color-frame-code-text)', + 'frame-running': 'var(--color-frame-running)', + 'frame-error': 'var(--color-frame-error)', + 'frame-tab-active': 'var(--color-frame-tab-active)', }, spacing: { chrome: `${ APP_CHROME_SPACING }px`,