From e0714aebca3c22bfbc5e5e687cfc4efc5c76dd28 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:20:13 -0800 Subject: [PATCH] feat(onboarding): theme step with color mode + optional theme carousel (#38021) * feat(onboarding): studio theme picker with plugin themes and Match Terminal palettes Replace the three System/Light/Dark tiles with a live look picker: big preview, bundled GitHub/VS Code app themes, curated terminal palettes that recolor Orca via Match Terminal, and per-slot dark/light looks under System. Picks repaint the app live and never change the mode; skip restores the full arrival look. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(onboarding): simple theme tiles with optional look carousel Restore the original System/Dark/Light tiles and move theme looks into an optional carousel beneath them. Add a theme-plugin directory link that opens the system browser at onorca.dev/plugins?kind=themes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(onboarding): label color mode and theme sections on theme step Add Color mode / Theme section headings and replace the step subtitle with guidance on what to pick. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(onboarding): preserve arrival look across Ghostty import * fix(onboarding): follow moved terminal theme modules --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- config/direct-link-open-baseline.txt | 1 + .../components/onboarding/OnboardingFlow.tsx | 6 +- .../onboarding/ThemeLookCarousel.test.tsx | 61 +++++ .../onboarding/ThemeLookCarousel.tsx | 175 +++++++++++++ .../onboarding/ThemeLookGallery.tsx | 84 +++++++ .../onboarding/ThemeLookPreview.tsx | 80 ++++++ .../components/onboarding/ThemeModeTiles.tsx | 76 ++++++ .../onboarding/ThemeStep.fixture.tsx | 57 +++++ .../src/components/onboarding/ThemeStep.tsx | 213 ++++++++++------ .../onboarding/onboarding-flow-state.ts | 10 +- .../onboarding-theme-fixture-themes.ts | 55 ++++ .../onboarding-theme-look-settings.test.ts | 86 +++++++ .../onboarding-theme-look-settings.ts | 118 +++++++++ .../onboarding/onboarding-theme-looks.ts | 234 ++++++++++++++++++ .../onboarding/use-onboarding-flow-actions.ts | 9 +- .../onboarding/use-onboarding-flow.test.ts | 23 +- .../onboarding/use-onboarding-flow.ts | 26 +- src/renderer/src/i18n/locales/de.json | 28 ++- src/renderer/src/i18n/locales/en.json | 28 ++- src/renderer/src/i18n/locales/es.json | 28 ++- src/renderer/src/i18n/locales/fr.json | 28 ++- src/renderer/src/i18n/locales/ja.json | 28 ++- src/renderer/src/i18n/locales/ko.json | 28 ++- src/renderer/src/i18n/locales/pt.json | 28 ++- src/renderer/src/i18n/locales/ru.json | 28 ++- src/renderer/src/i18n/locales/tr.json | 28 ++- src/renderer/src/i18n/locales/zh-Hant.json | 28 ++- src/renderer/src/i18n/locales/zh.json | 28 ++- 28 files changed, 1501 insertions(+), 121 deletions(-) create mode 100644 src/renderer/src/components/onboarding/ThemeLookCarousel.test.tsx create mode 100644 src/renderer/src/components/onboarding/ThemeLookCarousel.tsx create mode 100644 src/renderer/src/components/onboarding/ThemeLookGallery.tsx create mode 100644 src/renderer/src/components/onboarding/ThemeLookPreview.tsx create mode 100644 src/renderer/src/components/onboarding/ThemeModeTiles.tsx create mode 100644 src/renderer/src/components/onboarding/ThemeStep.fixture.tsx create mode 100644 src/renderer/src/components/onboarding/onboarding-theme-fixture-themes.ts create mode 100644 src/renderer/src/components/onboarding/onboarding-theme-look-settings.test.ts create mode 100644 src/renderer/src/components/onboarding/onboarding-theme-look-settings.ts create mode 100644 src/renderer/src/components/onboarding/onboarding-theme-looks.ts diff --git a/config/direct-link-open-baseline.txt b/config/direct-link-open-baseline.txt index d8dbe600d3b..88377cbc4ee 100644 --- a/config/direct-link-open-baseline.txt +++ b/config/direct-link-open-baseline.txt @@ -19,6 +19,7 @@ 1 src/renderer/src/components/jira-connect-credential-guide.tsx 2 src/renderer/src/components/linear-api-key-dialog.tsx 1 src/renderer/src/components/native-chat/NativeChatMarkdownImage.tsx +1 src/renderer/src/components/onboarding/ThemeLookCarousel.tsx 1 src/renderer/src/components/right-sidebar/ssh-forwarded-ports-section.tsx 1 src/renderer/src/components/right-sidebar/ssh-ports-panel.tsx 1 src/renderer/src/components/settings/accounts/codex/CodexLoginLinkNotice.tsx diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index 152e0fbdb50..0d721cb2ec8 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -42,8 +42,8 @@ const stepCopy = { }, get subtitle() { return translate( - 'auto.components.onboarding.OnboardingFlow.04ae28d8ca', - 'Pick the look you want to stare at for hours.' + 'components.onboarding.theme.subtitle', + 'Choose light or dark, then optionally a theme. Change either any time in Settings.' ) } }, @@ -331,6 +331,8 @@ export default function OnboardingFlow({ onThemeChange={flow.setTheme} settings={flow.settings} updateSettings={flow.updateSettings} + lookBaseline={flow.themeLookBaseline} + onLookBaselineChange={flow.setThemeLookBaseline} /> )} {currentStep.id === 'notifications' && ( diff --git a/src/renderer/src/components/onboarding/ThemeLookCarousel.test.tsx b/src/renderer/src/components/onboarding/ThemeLookCarousel.test.tsx new file mode 100644 index 00000000000..194f0c91da3 --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeLookCarousel.test.tsx @@ -0,0 +1,61 @@ +// @vitest-environment happy-dom +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' +import { buildThemeLooks } from './onboarding-theme-looks' +import { THEME_PLUGINS_URL, ThemeLookCarousel } from './ThemeLookCarousel' + +const openUrl = vi.fn() +const looks = buildThemeLooks({ + base: 'dark', + pluginThemes: [], + pluginTerminalThemes: [], + baseline: getDefaultSettings('/tmp'), + labels: { orca: 'Orca', community: 'Terminal palette', builtIn: () => 'Orca Dark' } +}) + +beforeEach(() => { + openUrl.mockReset().mockResolvedValue(undefined) + Object.defineProperty(window, 'api', { configurable: true, value: { shell: { openUrl } } }) +}) + +afterEach(cleanup) + +function renderCarousel(onPick = vi.fn()): void { + render( + + ) +} + +describe('ThemeLookCarousel', () => { + it('opens the themes plugin directory in the system browser', () => { + renderCarousel() + fireEvent.click(screen.getByRole('button', { name: /More themes/ })) + expect(openUrl).toHaveBeenCalledWith(THEME_PLUGINS_URL) + expect(new URL(THEME_PLUGINS_URL).searchParams.get('kind')).toBe('themes') + }) + + it('moves focus with arrow keys and applies the focused look on click', () => { + const onPick = vi.fn() + renderCarousel(onPick) + const cards = [ + ...screen + .getByRole('group', { name: 'Dark looks' }) + .querySelectorAll('[data-theme-look]') + ] + cards[0].focus() + fireEvent.keyDown(cards[0], { key: 'ArrowRight' }) + expect(document.activeElement).toBe(cards[1]) + fireEvent.keyDown(cards[1], { key: 'End' }) + const last = cards.at(-1)! + expect(document.activeElement).toBe(last) + fireEvent.click(last) + expect(onPick).toHaveBeenCalledWith(looks.at(-1)) + }) +}) diff --git a/src/renderer/src/components/onboarding/ThemeLookCarousel.tsx b/src/renderer/src/components/onboarding/ThemeLookCarousel.tsx new file mode 100644 index 00000000000..9e50dceacaf --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeLookCarousel.tsx @@ -0,0 +1,175 @@ +import type React from 'react' +import { useEffect, useRef, useState } from 'react' +import { Check, ChevronLeft, ChevronRight, ExternalLink, Puzzle } from 'lucide-react' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import { Button } from '@/components/ui/button' +import type { ThemeLook } from './onboarding-theme-looks' +import { ThemeLookCardPreview } from './ThemeLookPreview' + +export const THEME_PLUGINS_URL = 'https://onorca.dev/plugins?kind=themes' + +export function openThemePluginDirectory(): void { + // Why the system browser: onboarding has no workspace browser to host the directory. + void window.api.shell.openUrl(THEME_PLUGINS_URL) +} + +type ScrollEdges = { start: boolean; end: boolean } + +function readEdges(el: HTMLElement): ScrollEdges { + return { + start: el.scrollLeft <= 1, + end: el.scrollLeft + el.clientWidth >= el.scrollWidth - 1 + } +} + +/** Optional looks for one mode in a snap carousel; click or Enter applies, the app repaints live. */ +export function ThemeLookCarousel({ + looks, + selectedId, + onPick, + ariaLabel, + header +}: { + looks: ThemeLook[] + selectedId: string | null + onPick: (look: ThemeLook) => void + ariaLabel: string + header: React.ReactNode +}): React.JSX.Element { + const trackRef = useRef(null) + const [edges, setEdges] = useState({ start: true, end: false }) + const tabStopId = looks.some((look) => look.id === selectedId) ? selectedId : looks[0]?.id + + // Why: bring the applied look into view when the mode or slot swaps the card set. + useEffect(() => { + const el = trackRef.current + if (!el) { + return + } + const card = selectedId + ? el.querySelector(`[data-theme-look="${CSS.escape(selectedId)}"]`) + : null + el.scrollLeft = card + ? Math.max(0, card.offsetLeft - (el.clientWidth - card.clientWidth) / 2) + : 0 + setEdges(readEdges(el)) + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on card-set swaps, not every pick + }, [looks]) + + const page = (direction: 1 | -1): void => { + const el = trackRef.current + el?.scrollBy({ left: direction * el.clientWidth * 0.8, behavior: 'smooth' }) + } + + const onKeyDown = (event: React.KeyboardEvent): void => { + const moves: Record number> = { + ArrowRight: (index) => Math.min(index + 1, looks.length - 1), + ArrowLeft: (index) => Math.max(index - 1, 0), + Home: () => 0, + End: () => looks.length - 1 + } + const move = moves[event.key] + const targetId = event.target instanceof HTMLElement ? event.target.dataset.themeLook : null + const current = looks.findIndex((look) => look.id === targetId) + if (!move || current === -1) { + return + } + event.preventDefault() + const next = looks[move(current)] + event.currentTarget + .querySelector(`[data-theme-look="${CSS.escape(next.id)}"]`) + ?.focus() + } + + return ( +
+
+
{header}
+ + +
+
setEdges(readEdges(event.currentTarget))} + className={cn( + 'scrollbar-none relative -mx-1 flex snap-x snap-mandatory scroll-px-1 gap-2.5 overflow-x-auto px-1 py-1', + !edges.start && + !edges.end && + '[mask-image:linear-gradient(to_right,transparent,black_3rem,black_calc(100%-3rem),transparent)]', + edges.start && + !edges.end && + '[mask-image:linear-gradient(to_right,black_calc(100%-3rem),transparent)]', + !edges.start && + edges.end && + '[mask-image:linear-gradient(to_right,transparent,black_3rem)]' + )} + > + {looks.map((look) => { + const selected = look.id === selectedId + return ( + + ) + })} + +
+
+ ) +} diff --git a/src/renderer/src/components/onboarding/ThemeLookGallery.tsx b/src/renderer/src/components/onboarding/ThemeLookGallery.tsx new file mode 100644 index 00000000000..0d55657fd58 --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeLookGallery.tsx @@ -0,0 +1,84 @@ +import type React from 'react' +import { useMemo } from 'react' +import { translate } from '@/i18n/i18n' +import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision' +import { usePluginTerminalThemes, usePluginThemes } from '@/store/plugin-themes' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { SettingsSegmentedControl } from '../settings/form-controls/SettingsFormControls' +import { buildThemeLooks, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks' +import { selectedThemeLookId } from './onboarding-theme-look-settings' + +export type ThemeLooksByBase = Record + +/** The looks for both modes and which one each mode's slot holds now. */ +export function useThemeLooks( + settings: GlobalSettings | null, + baseline: GlobalSettings | null +): { + looks: ThemeLooksByBase + selected: Record + pluginTerminalThemes: ReturnType +} { + const pluginThemes = usePluginThemes() + const pluginTerminalThemes = usePluginTerminalThemes() + const languageRevision = useUiLanguageRevision() + const looks = useMemo(() => { + if (!baseline) { + return { dark: [], light: [] } + } + const labels = { + orca: translate('components.onboarding.theme.looks.sourceBuiltIn', 'Built in'), + community: translate('components.onboarding.theme.looks.sourceCommunity', 'Terminal palette'), + builtIn: (base: ThemeLookBase) => + base === 'dark' + ? translate('components.onboarding.theme.looks.orcaDark', 'Orca Dark') + : translate('components.onboarding.theme.looks.orcaLight', 'Orca Light') + } + const build = (base: ThemeLookBase): ThemeLook[] => + buildThemeLooks({ base, pluginThemes, pluginTerminalThemes, baseline, labels }) + return { dark: build('dark'), light: build('light') } + // eslint-disable-next-line react-hooks/exhaustive-deps -- languageRevision re-reads translated labels + }, [baseline, pluginThemes, pluginTerminalThemes, languageRevision]) + const selected = { + dark: settings ? selectedThemeLookId(settings, 'dark', looks.dark) : null, + light: settings ? selectedThemeLookId(settings, 'light', looks.light) : null + } + return { looks, selected, pluginTerminalThemes } +} + +/** Under System: which of the two persisted looks the list edits; marks the one showing now. */ +export function ThemeSlotControl({ + value, + osBase, + onChange +}: { + value: ThemeLookBase + osBase: ThemeLookBase + onChange: (base: ThemeLookBase) => void +}): React.JSX.Element { + const label = (base: ThemeLookBase): React.ReactNode => ( + + {base === 'dark' + ? translate('components.onboarding.theme.looks.darkLook', 'Dark look') + : translate('components.onboarding.theme.looks.lightLook', 'Light look')} + {base === osBase && ( + + {translate('components.onboarding.theme.looks.activeNow', 'now')} + + )} + + ) + return ( + + ) +} diff --git a/src/renderer/src/components/onboarding/ThemeLookPreview.tsx b/src/renderer/src/components/onboarding/ThemeLookPreview.tsx new file mode 100644 index 00000000000..cfc3df94764 --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeLookPreview.tsx @@ -0,0 +1,80 @@ +import type React from 'react' +import type { ThemeLookPalette } from './onboarding-theme-looks' + +// Why inline colors throughout: each preview paints another theme's palette, not the live tokens. + +/** A tiny Orca window in one look: sidebar, tab strip, and an agent transcript as color bars. */ +export function ThemeLookCardPreview({ + palette +}: { + palette: ThemeLookPalette +}): React.JSX.Element { + const { terminal: t } = palette + const line = (parts: [string, string][]): React.JSX.Element => ( +
+ {parts.map(([color, width], index) => ( + + ))} +
+ ) + return ( + + ) +} diff --git a/src/renderer/src/components/onboarding/ThemeModeTiles.tsx b/src/renderer/src/components/onboarding/ThemeModeTiles.tsx new file mode 100644 index 00000000000..29bb794b1fa --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeModeTiles.tsx @@ -0,0 +1,76 @@ +import type React from 'react' +import { Check, Monitor, Moon, Sun } from 'lucide-react' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { ChromePreview } from './theme-chrome-preview' + +/** The step's primary choice: System, Dark, or Light. */ +export function ThemeModeTiles({ + value, + onChange +}: { + value: GlobalSettings['theme'] + onChange: (theme: GlobalSettings['theme']) => void +}): React.JSX.Element { + const themes: { + id: GlobalSettings['theme'] + label: string + hint: string + icon: typeof Monitor + }[] = [ + { + id: 'system', + label: translate('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'), + hint: translate('components.onboarding.theme.hints.system', 'Match OS'), + icon: Monitor + }, + { + id: 'dark', + label: translate('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'), + hint: translate('components.onboarding.theme.hints.dark', 'Easy on the eyes'), + icon: Moon + }, + { + id: 'light', + label: translate('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'), + hint: translate('components.onboarding.theme.hints.light', 'Bright & crisp'), + icon: Sun + } + ] + + return ( +
+ {themes.map(({ id, label, hint, icon: Icon }) => { + const selected = value === id + return ( + + ) + })} +
+ ) +} diff --git a/src/renderer/src/components/onboarding/ThemeStep.fixture.tsx b/src/renderer/src/components/onboarding/ThemeStep.fixture.tsx new file mode 100644 index 00000000000..7535fbcfe29 --- /dev/null +++ b/src/renderer/src/components/onboarding/ThemeStep.fixture.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react' +import { defineFixture } from '../../component-fixtures/component-fixture' +import { getDefaultSettings } from '../../../../shared/constants' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { ThemeStep } from './ThemeStep' +import { seedPluginThemes } from './onboarding-theme-fixture-themes' + +function ThemeStepHarness({ initialTheme }: { initialTheme: GlobalSettings['theme'] }) { + const [settings, setSettings] = useState(() => ({ + ...getDefaultSettings('/Users/preview'), + theme: initialTheme + })) + const [theme, setTheme] = useState(initialTheme) + const [baseline, setBaseline] = useState(settings) + return ( +
+ setSettings((current) => ({ ...current, ...updates }))} + lookBaseline={baseline} + onLookBaselineChange={setBaseline} + /> +
+ ) +} + +export const darkMode = defineFixture({ + width: 1036, + setup: seedPluginThemes, + render: () => +}) + +export const systemNarrow = defineFixture({ + width: 760, + setup: seedPluginThemes, + render: () => +}) + +const pickLook = (id: string) => (frame: HTMLElement) => { + frame.querySelector(`[data-theme-look="${id}"]`)?.click() +} + +export const pickedPalette = defineFixture({ + width: 1036, + setup: seedPluginThemes, + interact: pickLook('terminal:Dracula'), + render: () => +}) + +export const pickedPluginLight = defineFixture({ + width: 1036, + setup: seedPluginThemes, + interact: pickLook('plugin:stablyai.orca-github-vscode-theme/github-light-default'), + render: () => +}) diff --git a/src/renderer/src/components/onboarding/ThemeStep.tsx b/src/renderer/src/components/onboarding/ThemeStep.tsx index 6e6dfd1e25d..5b91e447d94 100644 --- a/src/renderer/src/components/onboarding/ThemeStep.tsx +++ b/src/renderer/src/components/onboarding/ThemeStep.tsx @@ -1,26 +1,33 @@ import { useEffect, useState } from 'react' -import { Check, Monitor, Moon, Settings2, Sun } from 'lucide-react' +import { ExternalLink, Puzzle, Settings2 } from 'lucide-react' import { Trans } from 'react-i18next' import { toast } from 'sonner' -import { cn } from '@/lib/utils' import { track } from '@/lib/telemetry' import { useMountedRef } from '@/hooks/useMountedRef' import { GhosttyDiscoveryRow } from './GhosttyDiscoveryRow' import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { DiscoveryStatusEmitted } from '../../../../shared/onboarding-state-types' import { i18n, translate } from '@/i18n/i18n' -import { ChromePreview } from './theme-chrome-preview' +import { ThemeSlotControl, useThemeLooks } from './ThemeLookGallery' +import { ThemeLookCarousel, openThemePluginDirectory } from './ThemeLookCarousel' +import { ThemeModeTiles } from './ThemeModeTiles' +import type { ThemeLook, ThemeLookBase } from './onboarding-theme-looks' +import { themeLookSettingsUpdate } from './onboarding-theme-look-settings' import { terminalConfigImportSettingsUpdate, type TerminalConfigImportPreview } from '../../../../shared/terminal/appearance/terminal-config-import-preview' import { getSystemPrefersDark } from '@/lib/terminal-theme' +import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-prefers-dark' type ThemeStepProps = { theme: GlobalSettings['theme'] onThemeChange: (theme: GlobalSettings['theme']) => void settings: GlobalSettings | null updateSettings: (updates: Partial) => Promise + /** Settings as the step found them; each pick replaces only its own mode's slot from here. */ + lookBaseline: GlobalSettings | null + onLookBaselineChange: (settings: GlobalSettings) => void } export function applyOnboardingThemeSelection( @@ -62,7 +69,14 @@ function fieldGroupCountBucket(count: number): '0' | '1-3' | '4-7' | '8+' { return '8+' } -export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: ThemeStepProps) { +export function ThemeStep({ + theme, + onThemeChange, + settings, + updateSettings, + lookBaseline, + onLookBaselineChange +}: ThemeStepProps) { const [importing, setImporting] = useState(false) const [discovery, setDiscovery] = useState({ status: 'idle' }) const mountedRef = useMountedRef() @@ -133,9 +147,16 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th track('onboarding_ghostty_import_failed', { reason: 'empty_diff' }) return } - await updateSettings( - terminalConfigImportSettingsUpdate(settings, resolved, getSystemPrefersDark()) + const imported = terminalConfigImportSettingsUpdate( + settings, + resolved, + getSystemPrefersDark() ) + await updateSettings(imported) + // Why: later look picks start from the imported terminal, not the one it replaced. + if (mountedRef.current) { + onLookBaselineChange({ ...(lookBaseline ?? settings), ...imported }) + } // Sync the controller’s local theme so Continue cannot overwrite the imported theme. if (resolved.diff.theme && mountedRef.current) { onThemeChange(resolved.diff.theme) @@ -168,65 +189,78 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th } } - const themes: { - id: GlobalSettings['theme'] - label: string - hint: string - icon: typeof Monitor - }[] = [ - { - id: 'system', - label: translate('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'), - hint: translate('components.onboarding.theme.hints.system', 'Match OS'), - icon: Monitor - }, - { - id: 'dark', - label: translate('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'), - hint: translate('components.onboarding.theme.hints.dark', 'Easy on the eyes'), - icon: Moon - }, - { - id: 'light', - label: translate('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'), - hint: translate('components.onboarding.theme.hints.light', 'Bright & crisp'), - icon: Sun + const systemPrefersDark = useSystemPrefersDark() + const osBase: ThemeLookBase = systemPrefersDark ? 'dark' : 'light' + const [systemSlot, setSystemSlot] = useState(null) + const { looks, selected, pluginTerminalThemes } = useThemeLooks(settings, lookBaseline) + // Why a slot picker under System: both looks persist, and only the OS chooses which one shows. + const base: ThemeLookBase = theme === 'system' ? (systemSlot ?? osBase) : theme + + const pickLook = (look: ThemeLook): void => { + if (settings && lookBaseline) { + void updateSettings( + themeLookSettingsUpdate(look, settings, lookBaseline, pluginTerminalThemes) + ) } - ] + } return (
-
- {themes.map(({ id, label, hint, icon: Icon }) => { - const selected = theme === id - return ( - - ) - })} -
+ )} +
+ } + /> -
- - - Settings → Terminal' +
+
+ + + {translate( + 'components.onboarding.theme.looks.pluginsHint', + 'More themes are available as plugins.' )} - components={{ settings: }} - /> - + + +
+
+ + + Settings → Terminal' + )} + components={{ settings: }} + /> + +
) @@ -294,3 +350,18 @@ function humanFields(diff: Partial): string[] { ] return groups.filter(({ keys }) => keys.some((k) => k in diff)).map(({ label }) => label) } + +function ThemeSectionHeading({ + title, + caption +}: { + title: string + caption: string +}): React.JSX.Element { + return ( +

+ {title} + {caption} +

+ ) +} diff --git a/src/renderer/src/components/onboarding/onboarding-flow-state.ts b/src/renderer/src/components/onboarding/onboarding-flow-state.ts index a606192c1f2..84b0800b112 100644 --- a/src/renderer/src/components/onboarding/onboarding-flow-state.ts +++ b/src/renderer/src/components/onboarding/onboarding-flow-state.ts @@ -10,6 +10,7 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { OnboardingState } from '../../../../shared/onboarding-state-types' import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' import { STEPS } from './use-onboarding-flow-types' +import type { ThemeLookSettings } from './onboarding-theme-look-settings' type TaskSourcesSnapshotProps = EventProps<'onboarding_task_sources_snapshot'> type TaskSourcesGithubStatus = TaskSourcesSnapshotProps['github_status'] @@ -123,7 +124,8 @@ export function remapOpenOnboardingLastCompletedStep({ type SkippedOnboardingPreferenceOptions = { currentStepId: OnboardingStepId - themeBeforePreview: GlobalSettings['theme'] | null + /** Look settings as the theme step found them; null before it was reached. */ + lookBeforePreview: ThemeLookSettings | null settingsTheme: GlobalSettings['theme'] | undefined selectedAgent: TuiAgent | null setTheme: (theme: GlobalSettings['theme']) => void @@ -134,7 +136,7 @@ type SkippedOnboardingPreferenceOptions = { export async function prepareSkippedOnboardingPreferences({ currentStepId, - themeBeforePreview, + lookBeforePreview, settingsTheme, selectedAgent, setTheme, @@ -145,11 +147,11 @@ export async function prepareSkippedOnboardingPreferences({ try { // Why: theme tiles save immediately for a stable preview, but skip must not keep this step's choice. if (currentStepId === 'theme') { - const themeToRestore = themeBeforePreview ?? settingsTheme + const themeToRestore = lookBeforePreview?.theme ?? settingsTheme if (themeToRestore) { setTheme(themeToRestore) applyTheme(themeToRestore) - await updateSettings({ theme: themeToRestore }) + await updateSettings({ ...lookBeforePreview, theme: themeToRestore }) } } // Why: skipping bypasses step persistence, so save the visible agent choice before closing. diff --git a/src/renderer/src/components/onboarding/onboarding-theme-fixture-themes.ts b/src/renderer/src/components/onboarding/onboarding-theme-fixture-themes.ts new file mode 100644 index 00000000000..8649ae6594d --- /dev/null +++ b/src/renderer/src/components/onboarding/onboarding-theme-fixture-themes.ts @@ -0,0 +1,55 @@ +import { usePluginTerminalThemeStore, usePluginThemeStore } from '@/store/plugin-themes' +import type { PluginThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-theme-artifact' +import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact' + +// Why real artifacts: the gallery should be judged against the themes Orca actually bundles. +const appThemeFiles = import.meta.glob<{ base: 'dark' | 'light'; tokens: Record }>( + '../../../../../resources/plugins/launch/stablyai.orca-{github-vscode-theme,vscode-default-themes}/themes/*.json', + { eager: true, import: 'default' } +) +const terminalThemeFiles = import.meta.glob( + '../../../../../resources/plugins/launch/stablyai.orca-github-vscode-theme/terminal/*.json', + { eager: true, import: 'terminal' } +) + +const fileId = (path: string): string => + path + .split('/') + .pop()! + .replace(/\.json$/, '') +const pluginKey = (path: string): string => path.match(/launch\/([^/]+)\//)![1] +const manifests = import.meta.glob<{ contributes: { themes: { id: string; title: string }[] } }>( + '../../../../../resources/plugins/launch/stablyai.orca-{github-vscode-theme,vscode-default-themes}/orca-plugin.json', + { eager: true, import: 'default' } +) +const titles = new Map( + Object.values(manifests).flatMap((manifest) => + manifest.contributes.themes.map((theme) => [theme.id, theme.title] as const) + ) +) +const titleCase = (id: string): string => titles.get(id) ?? id + +const appThemes: PluginThemeRegistration[] = Object.entries(appThemeFiles).map(([path, file]) => ({ + id: `plugin:${pluginKey(path)}/${fileId(path)}`, + base: file.base, + tokens: file.tokens, + revision: '1', + pluginKey: pluginKey(path), + pluginName: pluginKey(path).includes('github') ? 'GitHub Themes' : 'VS Code Themes', + label: titleCase(fileId(path)) +})) +const terminalThemes: PluginTerminalThemeRegistration[] = Object.entries(terminalThemeFiles).map( + ([path, terminal]) => ({ + id: `plugin:${pluginKey(path)}/${fileId(path)}`, + pluginKey: pluginKey(path), + pluginName: 'GitHub Themes', + label: titleCase(fileId(path)), + mode: fileId(path).includes('light') ? 'light' : 'dark', + terminal + }) +) + +export function seedPluginThemes(): void { + usePluginThemeStore.setState({ entries: appThemes, loaded: true }) + usePluginTerminalThemeStore.setState({ entries: terminalThemes, loaded: true }) +} diff --git a/src/renderer/src/components/onboarding/onboarding-theme-look-settings.test.ts b/src/renderer/src/components/onboarding/onboarding-theme-look-settings.test.ts new file mode 100644 index 00000000000..5c63666a9e7 --- /dev/null +++ b/src/renderer/src/components/onboarding/onboarding-theme-look-settings.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' +import { MATCH_TERMINAL_APP_THEME } from '../../../../shared/plugin-theme-settings' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { PluginThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-theme-artifact' +import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact' +import { buildThemeLooks, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks' +import { selectedThemeLookId, themeLookSettingsUpdate } from './onboarding-theme-look-settings' + +const labels = { orca: 'Orca', community: 'Terminal palette', builtIn: (b: ThemeLookBase) => b } +const appTheme: PluginThemeRegistration = { + id: 'plugin:gh/github-dark-default', + base: 'dark', + tokens: { '--background': '#0d1117', '--foreground': '#e6edf3' }, + revision: '1', + pluginKey: 'gh', + pluginName: 'GitHub Themes', + label: 'GitHub Dark Default' +} +const terminalTheme: PluginTerminalThemeRegistration = { + id: 'plugin:gh/github-dark-default', + pluginKey: 'gh', + pluginName: 'GitHub Themes', + label: 'GitHub Dark Default', + mode: 'dark', + terminal: { background: '#010409', foreground: '#e6edf3', red: '#ff7b72' } +} + +function setup(overrides: Partial = {}) { + const settings: GlobalSettings = { ...getDefaultSettings('/home/test'), ...overrides } + const looks = (base: ThemeLookBase): ThemeLook[] => + buildThemeLooks({ + base, + pluginThemes: [appTheme], + pluginTerminalThemes: [terminalTheme], + baseline: settings, + labels + }) + const look = (base: ThemeLookBase, id: string): ThemeLook => + looks(base).find((candidate) => candidate.id === id)! + const pick = (current: GlobalSettings, chosen: ThemeLook): GlobalSettings => ({ + ...current, + ...themeLookSettingsUpdate(chosen, current, settings, [terminalTheme]) + }) + return { settings, looks, look, pick } +} + +describe('themeLookSettingsUpdate', () => { + it('paints a terminal palette app-wide and drops overrides that would mask it', () => { + const { settings, look, looks, pick } = setup({ + terminalColorOverrides: { background: '#123456' } + }) + const next = pick(settings, look('dark', 'terminal:Dracula')) + + expect(next.pluginAppThemeDark).toBe(MATCH_TERMINAL_APP_THEME) + expect(next.terminalThemeDark).toBe('Dracula') + expect(next.terminalColorOverrides).toBeUndefined() + expect(selectedThemeLookId(next, 'dark', looks('dark'))).toBe('terminal:Dracula') + }) + + it('replaces an earlier pick in the same mode with the baseline before applying', () => { + const { settings, look, looks, pick } = setup() + const afterPalette = pick(settings, look('dark', 'terminal:Dracula')) + const next = pick(afterPalette, look('dark', appTheme.id)) + + expect(next.pluginAppThemeDark).toBe(appTheme.id) + // The plugin's own terminal palette replaces Dracula rather than layering on it. + expect(next.terminalThemeDark).not.toBe('Dracula') + expect(selectedThemeLookId(next, 'dark', looks('dark'))).toBe(appTheme.id) + + const reset = pick(next, look('dark', 'built-in:dark')) + expect(reset.pluginAppThemeDark).toBeNull() + expect(reset.terminalThemeDark).toBe(settings.terminalThemeDark) + }) + + it('leaves the other mode untouched and keeps light palettes off the dark terminal', () => { + const { settings, look, pick } = setup({ terminalUseSeparateLightTheme: false }) + const dark = pick(settings, look('dark', 'terminal:Dracula')) + const next = pick(dark, look('light', 'terminal:Catppuccin Latte')) + + expect(next.pluginAppThemeDark).toBe(MATCH_TERMINAL_APP_THEME) + expect(next.terminalThemeDark).toBe('Dracula') + expect(next.terminalThemeLight).toBe('Catppuccin Latte') + expect(next.terminalUseSeparateLightTheme).toBe(true) + }) +}) diff --git a/src/renderer/src/components/onboarding/onboarding-theme-look-settings.ts b/src/renderer/src/components/onboarding/onboarding-theme-look-settings.ts new file mode 100644 index 00000000000..5b9dc1088da --- /dev/null +++ b/src/renderer/src/components/onboarding/onboarding-theme-look-settings.ts @@ -0,0 +1,118 @@ +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact' +import { + MATCH_TERMINAL_APP_THEME, + pluginAppThemeSlotUpdate +} from '../../../../shared/plugin-theme-settings' +import { + hasTerminalColorOverrides, + TERMINAL_COLOR_OVERRIDES_SETTING +} from '../../../../shared/terminal-themes/terminal-color-overrides' +import { withTerminalPluginPalettes } from '@/lib/terminal-plugin-palettes' +import { terminalThemeUpdate } from '../plugins/state/plugin-theme-apply' +import { builtInThemeLookId, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks' + +/** Settings a look writes; the rest of GlobalSettings is never touched by a pick. */ +export type ThemeLookSettings = Pick< + GlobalSettings, + | 'theme' + | 'pluginAppThemeDark' + | 'pluginAppThemeLight' + | 'terminalThemeDark' + | 'terminalThemeLight' + | 'terminalUseSeparateLightTheme' + | 'terminalColorOverrides' + | 'terminalColorOverridesLight' + | 'terminalPluginPalettes' +> + +function slotTerminalTheme(settings: ThemeLookSettings, base: ThemeLookBase): string { + return base === 'dark' || !settings.terminalUseSeparateLightTheme + ? settings.terminalThemeDark + : settings.terminalThemeLight +} + +/** The look a mode's slot holds now, or null when it holds something the step doesn't offer. */ +export function selectedThemeLookId( + settings: ThemeLookSettings, + base: ThemeLookBase, + looks: readonly ThemeLook[] +): string | null { + const app = base === 'dark' ? settings.pluginAppThemeDark : settings.pluginAppThemeLight + if (!app) { + return builtInThemeLookId(base) + } + if (app === MATCH_TERMINAL_APP_THEME) { + const name = slotTerminalTheme(settings, base) + return ( + looks.find((look) => look.kind === 'terminal' && look.terminalThemeName === name)?.id ?? null + ) + } + return looks.find((look) => look.kind === 'plugin' && look.id === app)?.id ?? null +} + +/** One mode's look settings as they were, so a new pick replaces the last one cleanly. */ +function slotBaseline(baseline: ThemeLookSettings, base: ThemeLookBase): Partial { + return base === 'dark' + ? { + pluginAppThemeDark: baseline.pluginAppThemeDark ?? null, + terminalThemeDark: baseline.terminalThemeDark, + terminalColorOverrides: baseline.terminalColorOverrides + } + : { + pluginAppThemeLight: baseline.pluginAppThemeLight ?? null, + terminalThemeLight: baseline.terminalThemeLight, + terminalUseSeparateLightTheme: baseline.terminalUseSeparateLightTheme, + terminalColorOverridesLight: baseline.terminalColorOverridesLight + } +} + +/** The settings write for picking a look: that mode's slot only, starting from the baseline. */ +export function themeLookSettingsUpdate( + look: ThemeLook, + settings: GlobalSettings, + baseline: ThemeLookSettings, + pluginTerminalThemes: readonly PluginTerminalThemeRegistration[] +): Partial { + const dark = look.base === 'dark' + let next: Partial = slotBaseline(baseline, look.base) + if (look.kind === 'built-in') { + next = { ...next, ...pluginAppThemeSlotUpdate(look.base, null) } + } else if (look.kind === 'plugin') { + next = { ...next, ...pluginAppThemeSlotUpdate(look.base, look.theme.id) } + if (look.terminal) { + next = { + ...next, + ...terminalThemeUpdate({ ...settings, ...next }, look.terminal, dark, pluginTerminalThemes) + } + } + } else { + const merged = { ...settings, ...next } + next = { + ...next, + ...pluginAppThemeSlotUpdate(look.base, MATCH_TERMINAL_APP_THEME), + ...(dark + ? { terminalThemeDark: look.terminalThemeName } + : { terminalThemeLight: look.terminalThemeName, terminalUseSeparateLightTheme: true }), + // A picked palette must replace its slot's overrides or they mask its colors. + ...(hasTerminalColorOverrides(merged, look.base) + ? { [TERMINAL_COLOR_OVERRIDES_SETTING[look.base]]: undefined } + : {}) + } + } + return withTerminalPluginPalettes({ ...settings, ...next }, next, pluginTerminalThemes) +} + +export function captureThemeLookSettings(settings: GlobalSettings): ThemeLookSettings { + return { + theme: settings.theme, + pluginAppThemeDark: settings.pluginAppThemeDark ?? null, + pluginAppThemeLight: settings.pluginAppThemeLight ?? null, + terminalThemeDark: settings.terminalThemeDark, + terminalThemeLight: settings.terminalThemeLight, + terminalUseSeparateLightTheme: settings.terminalUseSeparateLightTheme, + terminalColorOverrides: settings.terminalColorOverrides, + terminalColorOverridesLight: settings.terminalColorOverridesLight, + terminalPluginPalettes: settings.terminalPluginPalettes ?? [] + } +} diff --git a/src/renderer/src/components/onboarding/onboarding-theme-looks.ts b/src/renderer/src/components/onboarding/onboarding-theme-looks.ts new file mode 100644 index 00000000000..933ed6333b8 --- /dev/null +++ b/src/renderer/src/components/onboarding/onboarding-theme-looks.ts @@ -0,0 +1,234 @@ +import type { ITheme } from '@xterm/xterm' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { + completePluginAppThemeTokens, + type PluginThemeRegistration +} from '../../../../shared/plugins/artifacts/plugin-theme-artifact' +import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact' +import { resolveTerminalPalette } from '../../../../shared/terminal-themes/terminal-theme-selection' +import { TERMINAL_THEME_CATALOG } from '../../../../shared/terminal-themes/terminal-theme-catalog' + +export type ThemeLookBase = 'light' | 'dark' + +export type ThemeLookPalette = { + background: string + sidebar: string + foreground: string + mutedForeground: string + border: string + accent: string + primary: string + terminal: { + background: string + foreground: string + muted: string + red: string + green: string + yellow: string + blue: string + magenta: string + cyan: string + } +} + +export type ThemeLook = { + id: string + label: string + /** Who made it, shown as the card's caption. */ + source: string + base: ThemeLookBase + palette: ThemeLookPalette +} & ( + | { kind: 'built-in' } + | { + kind: 'plugin' + theme: PluginThemeRegistration + terminal: PluginTerminalThemeRegistration | null + } + | { kind: 'terminal'; terminalThemeName: string } +) + +// Popular community palettes Orca ships; Match Terminal turns each into a whole-app theme. +const TERMINAL_LOOKS: Record = { + dark: [ + 'Catppuccin Mocha', + 'Tokyo Night', + 'Dracula', + 'Rose Pine', + 'Gruvbox Dark', + 'One Dark', + 'Everforest Dark', + 'Night Owl', + 'Solarized Dark' + ], + light: [ + 'Catppuccin Latte', + 'Rose Pine Dawn', + 'Tokyo Night Light', + 'Gruvbox Light', + 'One Light', + 'Everforest Light', + 'Solarized Light' + ] +} + +// Why literal colors: previews show main.css's built-in tokens while the other mode is painted. +const BUILT_IN_CHROME: Record> = { + dark: { + background: '#0a0a0a', + sidebar: '#171717', + foreground: '#f0f0f0', + mutedForeground: '#acacac', + border: 'rgb(255 255 255 / 0.07)', + accent: '#2a2a2a', + primary: '#e5e5e5' + }, + light: { + background: '#ffffff', + sidebar: '#fafafa', + foreground: '#0a0a0a', + mutedForeground: '#696969', + border: '#e5e5e5', + accent: '#f0f0f0', + primary: '#171717' + } +} + +export function builtInThemeLookId(base: ThemeLookBase): string { + return `built-in:${base}` +} + +function terminalLookId(name: string): string { + return `terminal:${name}` +} + +function mixInto(color: string, background: string, percent: number): string { + return `color-mix(in srgb, ${color} ${percent}%, ${background})` +} + +function terminalPalette(theme: ITheme | null | undefined): ThemeLookPalette['terminal'] | null { + if (!theme?.background || !theme.foreground) { + return null + } + const { background, foreground } = theme + return { + background, + foreground, + muted: theme.brightBlack ?? mixInto(foreground, background, 55), + red: theme.red ?? foreground, + green: theme.green ?? foreground, + yellow: theme.yellow ?? foreground, + blue: theme.blue ?? foreground, + magenta: theme.magenta ?? foreground, + cyan: theme.cyan ?? foreground + } +} + +/** The terminal a slot shows today, so built-in looks preview the user's own terminal colors. */ +function currentTerminal( + settings: GlobalSettings, + base: ThemeLookBase +): ThemeLookPalette['terminal'] { + const fallback = + TERMINAL_THEME_CATALOG[base === 'dark' ? 'Ghostty Default Style Dark' : 'Builtin Tango Light'] + return ( + terminalPalette(resolveTerminalPalette({ ...settings, theme: base }, base === 'dark').theme) ?? + terminalPalette(fallback)! + ) +} + +function pluginTerminalFor( + theme: PluginThemeRegistration, + terminals: readonly PluginTerminalThemeRegistration[] +): PluginTerminalThemeRegistration | null { + const own = terminals.filter( + (terminal) => + terminal.pluginKey === theme.pluginKey && + (terminal.mode === theme.base || terminal.mode === 'unknown') + ) + return ( + own.find((terminal) => terminal.label === theme.label) ?? (own.length === 1 ? own[0] : null) + ) +} + +function pluginLook( + theme: PluginThemeRegistration, + terminals: readonly PluginTerminalThemeRegistration[], + baseline: GlobalSettings +): ThemeLook { + const tokens = completePluginAppThemeTokens(theme.tokens) + const terminal = pluginTerminalFor(theme, terminals) + return { + kind: 'plugin', + id: theme.id, + label: theme.label, + source: theme.pluginName, + base: theme.base, + theme, + terminal, + palette: { + background: tokens['--background'], + sidebar: tokens['--worktree-sidebar'] ?? tokens['--sidebar'] ?? tokens['--background'], + foreground: tokens['--foreground'], + mutedForeground: tokens['--muted-foreground'] ?? tokens['--foreground'], + border: tokens['--border'] ?? mixInto(tokens['--foreground'], tokens['--background'], 16), + accent: tokens['--accent'] ?? mixInto(tokens['--foreground'], tokens['--background'], 12), + primary: tokens['--primary'] ?? tokens['--foreground'], + terminal: terminalPalette(terminal?.terminal) ?? currentTerminal(baseline, theme.base) + } + } +} + +function terminalLook(name: string, base: ThemeLookBase, source: string): ThemeLook | null { + const terminal = terminalPalette(TERMINAL_THEME_CATALOG[name]) + if (!terminal) { + return null + } + const { background, foreground } = terminal + return { + kind: 'terminal', + id: terminalLookId(name), + label: name, + source, + base, + terminalThemeName: name, + palette: { + background, + // Why equal: Match Terminal paints the sidebar with the terminal background. + sidebar: background, + foreground, + mutedForeground: mixInto(foreground, background, 70), + border: mixInto(foreground, background, 10), + accent: mixInto(foreground, background, 12), + primary: terminal.blue, + terminal + } + } +} + +/** Every look the theme step offers for one mode: Orca's own first, then installed plugin + * themes, then popular terminal palettes painted through Match Terminal. */ +export function buildThemeLooks(options: { + base: ThemeLookBase + pluginThemes: readonly PluginThemeRegistration[] + pluginTerminalThemes: readonly PluginTerminalThemeRegistration[] + baseline: GlobalSettings + labels: { orca: string; builtIn: (base: ThemeLookBase) => string; community: string } +}): ThemeLook[] { + const { base, pluginThemes, pluginTerminalThemes, baseline, labels } = options + const builtIn: ThemeLook = { + kind: 'built-in', + id: builtInThemeLookId(base), + label: labels.builtIn(base), + source: labels.orca, + base, + palette: { ...BUILT_IN_CHROME[base], terminal: currentTerminal(baseline, base) } + } + const plugins = pluginThemes + .filter((theme) => theme.base === base) + .map((theme) => pluginLook(theme, pluginTerminalThemes, baseline)) + const terminals = TERMINAL_LOOKS[base].flatMap( + (name) => terminalLook(name, base, labels.community) ?? [] + ) + return [builtIn, ...plugins, ...terminals] +} diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-actions.ts b/src/renderer/src/components/onboarding/use-onboarding-flow-actions.ts index 2ac4404f1a8..7e24e206d5f 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-actions.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-actions.ts @@ -10,6 +10,7 @@ import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent import { buildWindowsTerminalSnapshotPayload } from './windows-terminal-onboarding-telemetry' import { persistStep, type PersistCurrentStepResult } from './use-onboarding-flow-persistence' import { STEPS, type StepNumber } from './use-onboarding-flow-types' +import { captureThemeLookSettings } from './onboarding-theme-look-settings' import { prepareSkippedOnboardingPreferences, resolveStepIndex, @@ -44,7 +45,7 @@ type OnboardingFlowActionsArgs = { stepIndex: number setStepIndex: Dispatch> selectedAgent: TuiAgent | null - themeStepEntryThemeRef: { current: GlobalSettings['theme'] | null } + themeLookBaseline: GlobalSettings | null setTheme: Dispatch> updateSettings: (updates: Partial) => Promise | void skipOptions: OnboardingStepSkipOptions @@ -66,7 +67,7 @@ export function useOnboardingFlowActions({ stepIndex, setStepIndex, selectedAgent, - themeStepEntryThemeRef, + themeLookBaseline, setTheme, updateSettings, skipOptions @@ -181,7 +182,7 @@ export function useOnboardingFlowActions({ const durationMs = consumeStepDurationMs() const preferencesSaved = await prepareSkippedOnboardingPreferences({ currentStepId: currentStep.id, - themeBeforePreview: themeStepEntryThemeRef.current, + lookBeforePreview: themeLookBaseline ? captureThemeLookSettings(themeLookBaseline) : null, settingsTheme: settings?.theme, selectedAgent, setTheme, @@ -242,7 +243,7 @@ export function useOnboardingFlowActions({ setBusyLabel, setError, setTheme, - themeStepEntryThemeRef + themeLookBaseline ]) const dismissOnboarding = useCallback( diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.test.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.test.ts index 4930c1c59b9..2fc0fb0cced 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.test.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.test.ts @@ -24,7 +24,17 @@ describe('prepareSkippedOnboardingPreferences', () => { await expect( prepareSkippedOnboardingPreferences({ currentStepId: 'theme', - themeBeforePreview: 'dark', + lookBeforePreview: { + theme: 'dark', + pluginAppThemeDark: null, + pluginAppThemeLight: null, + terminalThemeDark: 'Ghostty Default Style Dark', + terminalThemeLight: 'Builtin Tango Light', + terminalUseSeparateLightTheme: true, + terminalColorOverrides: undefined, + terminalColorOverridesLight: undefined, + terminalPluginPalettes: [] + }, settingsTheme: 'light', selectedAgent: null, setTheme, @@ -36,7 +46,14 @@ describe('prepareSkippedOnboardingPreferences', () => { expect(setTheme).toHaveBeenCalledWith('dark') expect(applyTheme).toHaveBeenCalledWith('dark') - expect(updateSettings).toHaveBeenCalledWith({ theme: 'dark' }) + // Restores the app and terminal slots a picked look replaced, not just the mode. + expect(updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ + theme: 'dark', + pluginAppThemeDark: null, + terminalThemeDark: 'Ghostty Default Style Dark' + }) + ) expect(setError).toHaveBeenCalledWith('settings IPC failed') expect(toast.error).toHaveBeenCalledWith('Could not save progress', { description: 'settings IPC failed' @@ -52,7 +69,7 @@ describe('prepareSkippedOnboardingPreferences', () => { await expect( prepareSkippedOnboardingPreferences({ currentStepId: 'agent', - themeBeforePreview: null, + lookBeforePreview: null, settingsTheme: 'light', selectedAgent: 'codex', setTheme, diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.ts index ac66fb3bdda..11baf9dc157 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.ts @@ -161,20 +161,14 @@ export function useOnboardingFlow( useEffect(() => { paintedThemeRef.current = paintedTheme }, [paintedTheme]) - const themeStepEntryThemeRef = useRef(null) - const themeStepEntryCapturedRef = useRef(false) - useEffect(() => { - if (currentStep.id !== 'theme') { - themeStepEntryCapturedRef.current = false - return - } - if (!settings || themeStepEntryCapturedRef.current) { - return - } - // Why: capture entry theme so "Skip to project setup" keeps the preference the user arrived with. - themeStepEntryCapturedRef.current = true - themeStepEntryThemeRef.current = settings.theme - }, [currentStep.id, settings]) + // Why per entry: "Skip to project setup" keeps the look the user arrived with, and each pick + // replaces only its own mode's slot from here. + const [themeLookBaseline, setThemeLookBaseline] = useState(null) + if (currentStep.id !== 'theme' && themeLookBaseline) { + setThemeLookBaseline(null) + } else if (currentStep.id === 'theme' && settings && !themeLookBaseline) { + setThemeLookBaseline(settings) + } const previewTheme = resolvePaintedTheme(settings, theme) useEffect(() => { @@ -286,7 +280,7 @@ export function useOnboardingFlow( stepIndex, setStepIndex, selectedAgent, - themeStepEntryThemeRef, + themeLookBaseline, setTheme, updateSettings, skipOptions @@ -305,6 +299,8 @@ export function useOnboardingFlow( setYoloPermissions: setYoloPermissionsInteractive, theme, setTheme: setThemeInteractive, + themeLookBaseline, + setThemeLookBaseline, busyLabel, detectedSet, isDetectingAgents, diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json index 8c4989d9765..9ce3be72485 100644 --- a/src/renderer/src/i18n/locales/de.json +++ b/src/renderer/src/i18n/locales/de.json @@ -15062,7 +15062,6 @@ "ae3b00ca82": "GitHub-Aufgaben einrichten", "ff92d15436": "Orca zeigt Benachrichtigungen an, wenn Agenten fertig sind oder Hilfe benötigen.", "b054332836": "Benachrichtigungen einrichten", - "04ae28d8ca": "Ein Erscheinungsbild auswählen, das auch nach Stunden noch gefällt.", "f396db9f20": "Vertraute Umgebung einrichten", "322fc50a18": "Orca funktioniert mit jedem CLI-Agenten. Den bevorzugten Agenten auswählen. Ein Wechsel ist jederzeit möglich.", "198b148b3c": "Standardagenten auswählen", @@ -19078,7 +19077,32 @@ "system": "An Betriebssystem anpassen", "dark": "Angenehm für die Augen", "light": "Hell & klar" - } + }, + "looks": { + "activeNow": "jetzt", + "browsePlugins": "Plugins durchsuchen", + "browseThemePlugins": "Design-Plugins durchsuchen", + "colorMode": "Farbmodus", + "colorModeCaption": "Hell, dunkel oder wie dein System.", + "darkLook": "Dunkler Look", + "darkLooks": "Dunkle Looks", + "lightLook": "Heller Look", + "lightLooks": "Helle Looks", + "moreThemes": "Weitere Designs", + "next": "Weitere Looks", + "orcaDark": "Orca Dunkel", + "orcaLight": "Orca Hell", + "pluginsHint": "Weitere Designs gibt es als Plugins.", + "previous": "Vorherige Looks", + "showsWhenDark": "Wird angezeigt, wenn dein System dunkel ist", + "showsWhenLight": "Wird angezeigt, wenn dein System hell ist", + "slotLabel": "Zu bearbeitender Look", + "sourceBuiltIn": "Integriert", + "sourceCommunity": "Terminal-Palette", + "themes": "Design", + "themesCaption": "Optional. Geh weiter mit App-Designs und Terminal-Paletten." + }, + "subtitle": "Wähle hell oder dunkel und optional ein Design. Beides kannst du jederzeit in den Einstellungen ändern." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a9a8ea06a57..dc82327dcff 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -15301,7 +15301,6 @@ "ae3b00ca82": "Set up GitHub tasks", "ff92d15436": "Orca will notify you when agents are done or need help.", "b054332836": "Set up notifications", - "04ae28d8ca": "Pick the look you want to stare at for hours.", "f396db9f20": "Make it feel like home", "322fc50a18": "Orca works with every CLI agent. Choose the one you'll reach for most. Switch any time.", "198b148b3c": "Pick your default agent", @@ -19376,7 +19375,32 @@ "system": "Match OS", "dark": "Easy on the eyes", "light": "Bright & crisp" - } + }, + "looks": { + "activeNow": "now", + "browsePlugins": "Browse plugins", + "browseThemePlugins": "Browse theme plugins", + "colorMode": "Color mode", + "colorModeCaption": "Light, dark, or follow your OS.", + "darkLook": "Dark look", + "darkLooks": "Dark looks", + "lightLook": "Light look", + "lightLooks": "Light looks", + "moreThemes": "More themes", + "next": "More looks", + "orcaDark": "Orca Dark", + "orcaLight": "Orca Light", + "pluginsHint": "More themes are available as plugins.", + "previous": "Previous looks", + "showsWhenDark": "Shows when your system is dark", + "showsWhenLight": "Shows when your system is light", + "slotLabel": "Look to edit", + "sourceBuiltIn": "Built in", + "sourceCommunity": "Terminal palette", + "themes": "Theme", + "themesCaption": "Optional. Go further with app themes and terminal palettes." + }, + "subtitle": "Choose light or dark, then optionally a theme. Change either any time in Settings." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index da31ce1694a..6b7c3acc416 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -14777,7 +14777,6 @@ "ae3b00ca82": "Configurar tareas de GitHub", "ff92d15436": "Orca te notificará cuando los agentes terminen o necesiten ayuda.", "b054332836": "Configurar notificaciones", - "04ae28d8ca": "Elige la apariencia que quieras ver durante horas.", "f396db9f20": "Haz que te sientas como en casa", "322fc50a18": "Orca funciona con cualquier agente CLI. Elige el que más usarás. Puedes cambiarlo en cualquier momento.", "198b148b3c": "Elija su agente predeterminado", @@ -20341,7 +20340,32 @@ "system": "Igual que el SO", "dark": "Suave a la vista", "light": "Brillante y nítido" - } + }, + "looks": { + "activeNow": "ahora", + "browsePlugins": "Explorar plugins", + "browseThemePlugins": "Explorar plugins de temas", + "colorMode": "Modo de color", + "colorModeCaption": "Claro, oscuro o según tu sistema.", + "darkLook": "Estilo oscuro", + "darkLooks": "Estilos oscuros", + "lightLook": "Estilo claro", + "lightLooks": "Estilos claros", + "moreThemes": "Más temas", + "next": "Más estilos", + "orcaDark": "Orca oscuro", + "orcaLight": "Orca claro", + "pluginsHint": "Hay más temas disponibles como plugins.", + "previous": "Estilos anteriores", + "showsWhenDark": "Se muestra cuando tu sistema está en modo oscuro", + "showsWhenLight": "Se muestra cuando tu sistema está en modo claro", + "slotLabel": "Estilo a editar", + "sourceBuiltIn": "Integrado", + "sourceCommunity": "Paleta de terminal", + "themes": "Tema", + "themesCaption": "Opcional. Ve más allá con temas de la app y paletas de terminal." + }, + "subtitle": "Elige claro u oscuro y, si quieres, un tema. Puedes cambiar ambos cuando quieras en Ajustes." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 4d44ba5ba8a..ca72a8b894b 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -15001,7 +15001,6 @@ "ae3b00ca82": "Configurer les tâches GitHub", "ff92d15436": "Orca vous préviendra quand les agents auront terminé ou auront besoin d'aide.", "b054332836": "Configurer les notifications", - "04ae28d8ca": "Choisissez le look que vous allez contempler pendant des heures.", "f396db9f20": "Donnez-lui vos couleurs", "322fc50a18": "Orca fonctionne avec tous les agents CLI. Choisissez celui que vous solliciterez le plus. Changez quand vous voulez.", "198b148b3c": "Choisissez votre agent par défaut", @@ -20510,7 +20509,32 @@ "system": "Faire correspondre le système d'exploitation", "dark": "Agréable pour les yeux", "light": "Lumineux et net" - } + }, + "looks": { + "activeNow": "actuel", + "browsePlugins": "Parcourir les plugins", + "browseThemePlugins": "Parcourir les plugins de thèmes", + "colorMode": "Mode de couleur", + "colorModeCaption": "Clair, sombre ou selon votre système.", + "darkLook": "Style sombre", + "darkLooks": "Styles sombres", + "lightLook": "Style clair", + "lightLooks": "Styles clairs", + "moreThemes": "Plus de thèmes", + "next": "Plus de styles", + "orcaDark": "Orca sombre", + "orcaLight": "Orca clair", + "pluginsHint": "D’autres thèmes sont disponibles sous forme de plugins.", + "previous": "Styles précédents", + "showsWhenDark": "Affiché quand votre système est en mode sombre", + "showsWhenLight": "Affiché quand votre système est en mode clair", + "slotLabel": "Style à modifier", + "sourceBuiltIn": "Intégré", + "sourceCommunity": "Palette de terminal", + "themes": "Thème", + "themesCaption": "Facultatif. Allez plus loin avec des thèmes d’app et des palettes de terminal." + }, + "subtitle": "Choisissez clair ou sombre, puis un thème si vous le souhaitez. Modifiable à tout moment dans les Réglages." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index fe5c5da4c71..b1a737076fc 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -14402,7 +14402,6 @@ "ae3b00ca82": "GitHub タスクをセットアップする", "ff92d15436": "Orca は、Agent の作業が完了したとき、またはサポートが必要になったときに通知します。", "b054332836": "通知を設定する", - "04ae28d8ca": "何時間も眺めていたくなるテーマを選んでください。", "f396db9f20": "まるで家にいるような気分にさせてくれる", "322fc50a18": "Orca はすべての CLI Agent で動作します。最もよく使うものを選択してください。いつでも切り替えられます。", "198b148b3c": "デフォルトの Agent を選択", @@ -18420,7 +18419,32 @@ "system": "OS に合わせる", "dark": "目にやさしい", "light": "明るく鮮明" - } + }, + "looks": { + "activeNow": "現在", + "browsePlugins": "プラグインを見る", + "browseThemePlugins": "テーマプラグインを見る", + "colorMode": "カラーモード", + "colorModeCaption": "ライト、ダーク、または OS に合わせる。", + "darkLook": "ダークルック", + "darkLooks": "ダークルック", + "lightLook": "ライトルック", + "lightLooks": "ライトルック", + "moreThemes": "その他のテーマ", + "next": "その他のルック", + "orcaDark": "Orca ダーク", + "orcaLight": "Orca ライト", + "pluginsHint": "その他のテーマはプラグインとして入手できます。", + "previous": "前のルック", + "showsWhenDark": "システムがダークのときに表示", + "showsWhenLight": "システムがライトのときに表示", + "slotLabel": "編集するルック", + "sourceBuiltIn": "組み込み", + "sourceCommunity": "ターミナルパレット", + "themes": "テーマ", + "themesCaption": "任意。アプリテーマやターミナルパレットでさらにカスタマイズできます。" + }, + "subtitle": "ライトかダークを選び、必要ならテーマも選びましょう。どちらも設定からいつでも変更できます。" }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 8cdbe380225..8f5ae39f2ad 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -14402,7 +14402,6 @@ "ae3b00ca82": "GitHub 작업 설정", "ff92d15436": "agents 작업이 완료되거나 도움이 필요할 때 Orca가 알려 드립니다.", "b054332836": "알림 설정", - "04ae28d8ca": "몇 시간 내내 보고 싶은 테마를 선택하세요.", "f396db9f20": "집처럼 느껴지도록 하세요", "322fc50a18": "Orca는 모든 CLI agent와 함께 작동합니다. 가장 자주 사용할 agent를 선택하세요. 언제든지 전환할 수 있습니다.", "198b148b3c": "기본 agent를 선택하세요", @@ -18420,7 +18419,32 @@ "system": "OS 설정에 맞춤", "dark": "눈이 편안한 화면", "light": "밝고 선명한 화면" - } + }, + "looks": { + "activeNow": "현재", + "browsePlugins": "플러그인 둘러보기", + "browseThemePlugins": "테마 플러그인 둘러보기", + "colorMode": "색상 모드", + "colorModeCaption": "라이트, 다크 또는 OS 설정 따르기.", + "darkLook": "다크 룩", + "darkLooks": "다크 룩", + "lightLook": "라이트 룩", + "lightLooks": "라이트 룩", + "moreThemes": "더 많은 테마", + "next": "더 많은 룩", + "orcaDark": "Orca 다크", + "orcaLight": "Orca 라이트", + "pluginsHint": "더 많은 테마를 플러그인으로 사용할 수 있습니다.", + "previous": "이전 룩", + "showsWhenDark": "시스템이 다크 모드일 때 표시", + "showsWhenLight": "시스템이 라이트 모드일 때 표시", + "slotLabel": "편집할 룩", + "sourceBuiltIn": "기본 제공", + "sourceCommunity": "터미널 팔레트", + "themes": "테마", + "themesCaption": "선택 사항. 앱 테마와 터미널 팔레트로 더 꾸며 보세요." + }, + "subtitle": "라이트 또는 다크를 고르고, 원하면 테마도 선택하세요. 둘 다 설정에서 언제든 바꿀 수 있습니다." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index c90d103060e..a5929c5cb47 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -2879,7 +2879,6 @@ "ae3b00ca82": "Configurar tarefas do GitHub", "ff92d15436": "O Orca vai avisá-lo quando os agentes terminarem ou precisarem de ajuda.", "b054332836": "Configurar notificações", - "04ae28d8ca": "Escolha a aparência que você vai encarar por horas.", "f396db9f20": "Deixe com a sua cara", "322fc50a18": "O Orca funciona com todo agente de CLI. Escolha o que você vai usar mais. Troque quando quiser.", "198b148b3c": "Escolha seu agente padrão", @@ -19992,7 +19991,32 @@ "system": "Seguir o sistema", "dark": "Suave para os olhos", "light": "Claro e nítido" - } + }, + "looks": { + "activeNow": "agora", + "browsePlugins": "Explorar plugins", + "browseThemePlugins": "Explorar plugins de temas", + "colorMode": "Modo de cor", + "colorModeCaption": "Claro, escuro ou igual ao sistema.", + "darkLook": "Visual escuro", + "darkLooks": "Visuais escuros", + "lightLook": "Visual claro", + "lightLooks": "Visuais claros", + "moreThemes": "Mais temas", + "next": "Mais visuais", + "orcaDark": "Orca escuro", + "orcaLight": "Orca claro", + "pluginsHint": "Há mais temas disponíveis como plugins.", + "previous": "Visuais anteriores", + "showsWhenDark": "Aparece quando o sistema está no modo escuro", + "showsWhenLight": "Aparece quando o sistema está no modo claro", + "slotLabel": "Visual a editar", + "sourceBuiltIn": "Integrado", + "sourceCommunity": "Paleta do terminal", + "themes": "Tema", + "themesCaption": "Opcional. Vá além com temas do app e paletas do terminal." + }, + "subtitle": "Escolha claro ou escuro e, se quiser, um tema. Altere ambos quando quiser em Configurações." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json index dccc41ba77b..81350b7a328 100644 --- a/src/renderer/src/i18n/locales/ru.json +++ b/src/renderer/src/i18n/locales/ru.json @@ -15531,7 +15531,6 @@ "97c42cda00": "Установите GitHub CLI, чтобы:", "ae3b00ca82": "Настройте задачи GitHub", "b054332836": "Настройте уведомления", - "04ae28d8ca": "Выберите оформление, с которым приятно работать часами.", "f396db9f20": "Устройтесь поудобнее", "322fc50a18": "Orca работает с любым CLI-агентом. Выберите того, которым пользуетесь чаще всего. Выбор можно изменить в любой момент.", "198b148b3c": "Выберите агента по умолчанию", @@ -19652,7 +19651,32 @@ "system": "Как в ОС", "dark": "Комфортная для глаз", "light": "Светлая и чёткая" - } + }, + "looks": { + "activeNow": "сейчас", + "browsePlugins": "Обзор плагинов", + "browseThemePlugins": "Обзор плагинов тем", + "colorMode": "Цветовой режим", + "colorModeCaption": "Светлый, тёмный или как в системе.", + "darkLook": "Тёмный стиль", + "darkLooks": "Тёмные стили", + "lightLook": "Светлый стиль", + "lightLooks": "Светлые стили", + "moreThemes": "Ещё темы", + "next": "Ещё стили", + "orcaDark": "Orca Тёмная", + "orcaLight": "Orca Светлая", + "pluginsHint": "Больше тем доступно в виде плагинов.", + "previous": "Предыдущие стили", + "showsWhenDark": "Показывается, когда система в тёмном режиме", + "showsWhenLight": "Показывается, когда система в светлом режиме", + "slotLabel": "Изменяемый стиль", + "sourceBuiltIn": "Встроенная", + "sourceCommunity": "Палитра терминала", + "themes": "Тема", + "themesCaption": "Необязательно. Темы приложения и палитры терминала для большей настройки." + }, + "subtitle": "Выберите светлый или тёмный режим и при желании тему. И то и другое можно изменить в настройках." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json index 5cd5433f1b4..7b81800c60b 100644 --- a/src/renderer/src/i18n/locales/tr.json +++ b/src/renderer/src/i18n/locales/tr.json @@ -15048,7 +15048,6 @@ "ae3b00ca82": "GitHub görevlerini ayarla", "ff92d15436": "Orca, ajanlar işlerini bitirdiğinde veya yardıma ihtiyaç duyduğunda size bildirim gönderecek.", "b054332836": "Bildirimleri ayarla", - "04ae28d8ca": "Saatlerce bakacağınız görünümü seçin.", "f396db9f20": "Kendi evinizdeymiş gibi hissettirin", "322fc50a18": "Orca her CLI ajanıyla çalışır. En sık kullanacağınızı seçin. İstediğiniz zaman değiştirin.", "198b148b3c": "Varsayılan ajanınızı seçin", @@ -19064,7 +19063,32 @@ "system": "İşletim sistemiyle eşleştir", "dark": "Gözü yormaz", "light": "Parlak ve net" - } + }, + "looks": { + "activeNow": "şimdi", + "browsePlugins": "Eklentilere göz at", + "browseThemePlugins": "Tema eklentilerine göz at", + "colorMode": "Renk modu", + "colorModeCaption": "Açık, koyu veya sisteminize uygun.", + "darkLook": "Koyu görünüm", + "darkLooks": "Koyu görünümler", + "lightLook": "Açık görünüm", + "lightLooks": "Açık görünümler", + "moreThemes": "Daha fazla tema", + "next": "Daha fazla görünüm", + "orcaDark": "Orca Koyu", + "orcaLight": "Orca Açık", + "pluginsHint": "Daha fazla tema eklenti olarak sunulur.", + "previous": "Önceki görünümler", + "showsWhenDark": "Sisteminiz koyu olduğunda gösterilir", + "showsWhenLight": "Sisteminiz açık olduğunda gösterilir", + "slotLabel": "Düzenlenecek görünüm", + "sourceBuiltIn": "Yerleşik", + "sourceCommunity": "Terminal paleti", + "themes": "Tema", + "themesCaption": "İsteğe bağlı. Uygulama temaları ve terminal paletleriyle daha ileri gidin." + }, + "subtitle": "Açık veya koyu seçin, isterseniz bir tema da seçin. İkisini de istediğiniz zaman Ayarlar’dan değiştirebilirsiniz." }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json index 92139955fbb..a163494bb89 100644 --- a/src/renderer/src/i18n/locales/zh-Hant.json +++ b/src/renderer/src/i18n/locales/zh-Hant.json @@ -14850,7 +14850,6 @@ "ae3b00ca82": "設定 GitHub 任務", "ff92d15436": "Orca 會在代理程式完成工作或需要協助時發出通知。", "b054332836": "設定通知", - "04ae28d8ca": "選擇你想盯著看幾個小時的主題。", "f396db9f20": "讓這裡有家的感覺", "322fc50a18": "Orca 支援所有 CLI 代理程式。選擇你最常用的一個,隨時可切換。", "198b148b3c": "選擇預設代理程式", @@ -18819,7 +18818,32 @@ "system": "與作業系統一致", "dark": "柔和護眼", "light": "明亮清晰" - } + }, + "looks": { + "activeNow": "目前", + "browsePlugins": "瀏覽外掛程式", + "browseThemePlugins": "瀏覽主題外掛程式", + "colorMode": "色彩模式", + "colorModeCaption": "淺色、深色或跟隨系統。", + "darkLook": "深色外觀", + "darkLooks": "深色外觀", + "lightLook": "淺色外觀", + "lightLooks": "淺色外觀", + "moreThemes": "更多主題", + "next": "更多外觀", + "orcaDark": "Orca 深色", + "orcaLight": "Orca 淺色", + "pluginsHint": "更多主題以外掛程式形式提供。", + "previous": "上一組外觀", + "showsWhenDark": "系統為深色時顯示", + "showsWhenLight": "系統為淺色時顯示", + "slotLabel": "要編輯的外觀", + "sourceBuiltIn": "內建", + "sourceCommunity": "終端機調色盤", + "themes": "主題", + "themesCaption": "選用。使用 App 主題和終端機調色盤進一步自訂。" + }, + "subtitle": "選擇淺色或深色,也可以再選一個主題。兩者都能隨時在設定中變更。" }, "integrations": { "capabilities": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index ef0f84806de..2c3d3cf28fa 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -14402,7 +14402,6 @@ "ae3b00ca82": "设置 GitHub 任务", "ff92d15436": "Orca 会在智能体完成工作或需要帮助时通知你。", "b054332836": "设置通知", - "04ae28d8ca": "选择你想盯着看几个小时的主题。", "f396db9f20": "让这里有家的感觉", "322fc50a18": "Orca 支持所有 CLI 智能体。选择你最常用的一个,随时可切换。", "198b148b3c": "选择默认智能体", @@ -19700,7 +19699,32 @@ "system": "匹配操作系统", "dark": "轻松护眼", "light": "明亮、清脆" - } + }, + "looks": { + "activeNow": "当前", + "browsePlugins": "浏览插件", + "browseThemePlugins": "浏览主题插件", + "colorMode": "颜色模式", + "colorModeCaption": "浅色、深色或跟随系统。", + "darkLook": "深色外观", + "darkLooks": "深色外观", + "lightLook": "浅色外观", + "lightLooks": "浅色外观", + "moreThemes": "更多主题", + "next": "更多外观", + "orcaDark": "Orca 深色", + "orcaLight": "Orca 浅色", + "pluginsHint": "更多主题以插件形式提供。", + "previous": "上一组外观", + "showsWhenDark": "系统为深色时显示", + "showsWhenLight": "系统为浅色时显示", + "slotLabel": "要编辑的外观", + "sourceBuiltIn": "内置", + "sourceCommunity": "终端调色板", + "themes": "主题", + "themesCaption": "可选。使用应用主题和终端调色板进一步自定义。" + }, + "subtitle": "选择浅色或深色,也可以再选一个主题。两者都可以随时在设置中更改。" }, "integrations": { "capabilities": {