mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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(
|
||||
<ThemeLookCarousel
|
||||
looks={looks}
|
||||
selectedId={looks[0].id}
|
||||
onPick={onPick}
|
||||
ariaLabel="Dark looks"
|
||||
header={null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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<HTMLElement>('[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))
|
||||
})
|
||||
})
|
||||
@@ -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<HTMLDivElement>(null)
|
||||
const [edges, setEdges] = useState<ScrollEdges>({ 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<HTMLElement>(`[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<HTMLDivElement>): void => {
|
||||
const moves: Record<string, (index: number) => 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<HTMLElement>(`[data-theme-look="${CSS.escape(next.id)}"]`)
|
||||
?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-2.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="min-w-0 flex-1">{header}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={edges.start}
|
||||
aria-label={translate('components.onboarding.theme.looks.previous', 'Previous looks')}
|
||||
onClick={() => page(-1)}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={edges.end}
|
||||
aria-label={translate('components.onboarding.theme.looks.next', 'More looks')}
|
||||
onClick={() => page(1)}
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
ref={trackRef}
|
||||
role="group"
|
||||
aria-label={ariaLabel}
|
||||
onKeyDown={onKeyDown}
|
||||
onScroll={(event) => 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 (
|
||||
<button
|
||||
key={look.id}
|
||||
type="button"
|
||||
aria-pressed={selected}
|
||||
tabIndex={look.id === tabStopId ? 0 : -1}
|
||||
data-theme-look={look.id}
|
||||
title={`${look.label} · ${look.source}`}
|
||||
className={cn(
|
||||
'w-40 shrink-0 snap-start rounded-lg border p-1.5 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
|
||||
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
|
||||
)}
|
||||
onClick={() => onPick(look)}
|
||||
>
|
||||
<div className="relative h-[4.5rem] overflow-hidden rounded-md border border-border">
|
||||
<ThemeLookCardPreview palette={look.palette} />
|
||||
{selected && (
|
||||
<div className="absolute right-1 top-1 grid size-4 place-items-center rounded-full bg-foreground text-background shadow-sm">
|
||||
<Check className="size-2.5" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate px-0.5 pt-1.5 text-xs font-medium text-foreground">
|
||||
{look.label}
|
||||
</div>
|
||||
<div className="truncate px-0.5 text-2xs text-muted-foreground">{look.source}</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-40 shrink-0 snap-start flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed border-border p-1.5 text-center text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||
onClick={openThemePluginDirectory}
|
||||
>
|
||||
<Puzzle className="size-4" />
|
||||
<span className="text-xs font-medium">
|
||||
{translate('components.onboarding.theme.looks.moreThemes', 'More themes')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-2xs">
|
||||
{translate('components.onboarding.theme.looks.browsePlugins', 'Browse plugins')}
|
||||
<ExternalLink className="size-2.5" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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<ThemeLookBase, ThemeLook[]>
|
||||
|
||||
/** 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<ThemeLookBase, string | null>
|
||||
pluginTerminalThemes: ReturnType<typeof usePluginTerminalThemes>
|
||||
} {
|
||||
const pluginThemes = usePluginThemes()
|
||||
const pluginTerminalThemes = usePluginTerminalThemes()
|
||||
const languageRevision = useUiLanguageRevision()
|
||||
const looks = useMemo<ThemeLooksByBase>(() => {
|
||||
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 => (
|
||||
<span className="flex items-center gap-1.5 whitespace-nowrap">
|
||||
{base === 'dark'
|
||||
? translate('components.onboarding.theme.looks.darkLook', 'Dark look')
|
||||
: translate('components.onboarding.theme.looks.lightLook', 'Light look')}
|
||||
{base === osBase && (
|
||||
<span className="text-2xs font-normal text-muted-foreground">
|
||||
{translate('components.onboarding.theme.looks.activeNow', 'now')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel={translate('components.onboarding.theme.looks.slotLabel', 'Look to edit')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
equalWidth
|
||||
size="sm"
|
||||
options={[
|
||||
{ value: 'dark', label: label('dark') },
|
||||
{ value: 'light', label: label('light') }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 => (
|
||||
<div className="flex h-[3px] gap-[3px]">
|
||||
{parts.map(([color, width], index) => (
|
||||
<span key={index} className="rounded-full" style={{ background: color, width }} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div aria-hidden="true" className="flex size-full" style={{ background: palette.background }}>
|
||||
<div
|
||||
className="flex w-[28%] shrink-0 flex-col gap-[5px] px-1.5 py-2"
|
||||
style={{ background: palette.sidebar, borderRight: `1px solid ${palette.border}` }}
|
||||
>
|
||||
{[t.green, t.yellow, palette.mutedForeground].map((dot, index) => (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
<span className="size-[4px] shrink-0 rounded-full" style={{ background: dot }} />
|
||||
<span
|
||||
className="h-[3px] rounded-full"
|
||||
style={{
|
||||
background: index === 0 ? palette.foreground : palette.mutedForeground,
|
||||
width: `${70 - index * 15}%`,
|
||||
opacity: index === 0 ? 0.85 : 0.5
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div
|
||||
className="flex h-3 shrink-0 items-end gap-1 px-1.5"
|
||||
style={{ borderBottom: `1px solid ${palette.border}` }}
|
||||
>
|
||||
<span className="h-[2px] w-5 rounded-t" style={{ background: palette.primary }} />
|
||||
<span
|
||||
className="mb-[3px] h-[2px] w-4 rounded-full"
|
||||
style={{ background: palette.border }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-1 flex-col gap-[5px] px-2 py-2"
|
||||
style={{ background: t.background }}
|
||||
>
|
||||
{line([
|
||||
[t.magenta, '8%'],
|
||||
[t.foreground, '55%']
|
||||
])}
|
||||
{line([
|
||||
[t.blue, '8%'],
|
||||
[t.muted, '30%'],
|
||||
[t.cyan, '28%']
|
||||
])}
|
||||
{line([
|
||||
[t.green, '42%'],
|
||||
[t.green, '18%']
|
||||
])}
|
||||
{line([
|
||||
[t.red, '36%'],
|
||||
[t.red, '14%']
|
||||
])}
|
||||
{line([
|
||||
[t.yellow, '8%'],
|
||||
[t.foreground, '32%']
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{themes.map(({ id, label, hint, icon: Icon }) => {
|
||||
const selected = value === id
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
className={cn(
|
||||
'group overflow-hidden rounded-xl border p-3 text-left transition-colors',
|
||||
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
|
||||
)}
|
||||
aria-pressed={selected}
|
||||
onClick={() => onChange(id)}
|
||||
>
|
||||
<div className="relative mb-3 h-24 overflow-hidden rounded-lg border border-border">
|
||||
<ChromePreview variant={id} />
|
||||
{selected && (
|
||||
<div className="absolute right-1.5 top-1.5 grid size-5 place-items-center rounded-full bg-foreground text-background shadow-sm">
|
||||
<Check className="size-3" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Icon className="size-3.5 text-muted-foreground" />
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">{hint}</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<GlobalSettings>(() => ({
|
||||
...getDefaultSettings('/Users/preview'),
|
||||
theme: initialTheme
|
||||
}))
|
||||
const [theme, setTheme] = useState(initialTheme)
|
||||
const [baseline, setBaseline] = useState<GlobalSettings | null>(settings)
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ThemeStep
|
||||
theme={theme}
|
||||
onThemeChange={setTheme}
|
||||
settings={settings}
|
||||
updateSettings={async (updates) => setSettings((current) => ({ ...current, ...updates }))}
|
||||
lookBaseline={baseline}
|
||||
onLookBaselineChange={setBaseline}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const darkMode = defineFixture({
|
||||
width: 1036,
|
||||
setup: seedPluginThemes,
|
||||
render: () => <ThemeStepHarness initialTheme="dark" />
|
||||
})
|
||||
|
||||
export const systemNarrow = defineFixture({
|
||||
width: 760,
|
||||
setup: seedPluginThemes,
|
||||
render: () => <ThemeStepHarness initialTheme="system" />
|
||||
})
|
||||
|
||||
const pickLook = (id: string) => (frame: HTMLElement) => {
|
||||
frame.querySelector<HTMLElement>(`[data-theme-look="${id}"]`)?.click()
|
||||
}
|
||||
|
||||
export const pickedPalette = defineFixture({
|
||||
width: 1036,
|
||||
setup: seedPluginThemes,
|
||||
interact: pickLook('terminal:Dracula'),
|
||||
render: () => <ThemeStepHarness initialTheme="dark" />
|
||||
})
|
||||
|
||||
export const pickedPluginLight = defineFixture({
|
||||
width: 1036,
|
||||
setup: seedPluginThemes,
|
||||
interact: pickLook('plugin:stablyai.orca-github-vscode-theme/github-light-default'),
|
||||
render: () => <ThemeStepHarness initialTheme="light" />
|
||||
})
|
||||
@@ -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<GlobalSettings>) => Promise<void>
|
||||
/** 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<DiscoveryState>({ 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<ThemeLookBase | null>(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 (
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{themes.map(({ id, label, hint, icon: Icon }) => {
|
||||
const selected = theme === id
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
className={cn(
|
||||
'group overflow-hidden rounded-xl border p-3 text-left transition-colors',
|
||||
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
|
||||
)}
|
||||
onClick={() => applyOnboardingThemeSelection(id, onThemeChange, updateSettings)}
|
||||
>
|
||||
<div className="relative mb-3 h-24 overflow-hidden rounded-lg border border-border">
|
||||
<ChromePreview variant={id} />
|
||||
{selected && (
|
||||
<div className="absolute right-1.5 top-1.5 grid size-5 place-items-center rounded-full bg-foreground text-background shadow-sm">
|
||||
<Check className="size-3" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
<section className="space-y-2.5">
|
||||
<ThemeSectionHeading
|
||||
title={translate('components.onboarding.theme.looks.colorMode', 'Color mode')}
|
||||
caption={translate(
|
||||
'components.onboarding.theme.looks.colorModeCaption',
|
||||
'Light, dark, or follow your OS.'
|
||||
)}
|
||||
/>
|
||||
<ThemeModeTiles
|
||||
value={theme}
|
||||
onChange={(id) => {
|
||||
setSystemSlot(null)
|
||||
applyOnboardingThemeSelection(id, onThemeChange, updateSettings)
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<ThemeLookCarousel
|
||||
looks={looks[base]}
|
||||
selectedId={selected[base]}
|
||||
onPick={pickLook}
|
||||
ariaLabel={
|
||||
base === 'dark'
|
||||
? translate('components.onboarding.theme.looks.darkLooks', 'Dark looks')
|
||||
: translate('components.onboarding.theme.looks.lightLooks', 'Light looks')
|
||||
}
|
||||
header={
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
|
||||
<ThemeSectionHeading
|
||||
title={translate('components.onboarding.theme.looks.themes', 'Theme')}
|
||||
caption={
|
||||
theme === 'system' && base !== osBase
|
||||
? base === 'dark'
|
||||
? translate(
|
||||
'components.onboarding.theme.looks.showsWhenDark',
|
||||
'Shows when your system is dark'
|
||||
)
|
||||
: translate(
|
||||
'components.onboarding.theme.looks.showsWhenLight',
|
||||
'Shows when your system is light'
|
||||
)
|
||||
: translate(
|
||||
'components.onboarding.theme.looks.themesCaption',
|
||||
'Optional. Go further with app themes and terminal palettes.'
|
||||
)
|
||||
}
|
||||
/>
|
||||
{theme === 'system' && (
|
||||
<div className="w-60 shrink-0">
|
||||
<ThemeSlotControl value={base} osBase={osBase} onChange={setSystemSlot} />
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<Icon className="size-3.5 text-muted-foreground" />
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">{hint}</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<GhosttyDiscoveryRow
|
||||
discovery={discovery}
|
||||
@@ -235,18 +269,40 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th
|
||||
onImport={importGhostty}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2 px-1 text-[12px] text-muted-foreground">
|
||||
<Settings2 className="size-3.5" />
|
||||
<span>
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
defaults={translate(
|
||||
'auto.components.onboarding.ThemeStep.moreTerminalOptions',
|
||||
'More terminal options, including font, cursor, and palette, in <settings>Settings → Terminal</settings>'
|
||||
<div className="space-y-1.5 px-1 text-[12px] text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Puzzle className="size-3.5" />
|
||||
<span>
|
||||
{translate(
|
||||
'components.onboarding.theme.looks.pluginsHint',
|
||||
'More themes are available as plugins.'
|
||||
)}
|
||||
components={{ settings: <span className="font-medium text-foreground" /> }}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
|
||||
onClick={openThemePluginDirectory}
|
||||
>
|
||||
{translate(
|
||||
'components.onboarding.theme.looks.browseThemePlugins',
|
||||
'Browse theme plugins'
|
||||
)}
|
||||
<ExternalLink className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="size-3.5" />
|
||||
<span>
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
defaults={translate(
|
||||
'auto.components.onboarding.ThemeStep.moreTerminalOptions',
|
||||
'More terminal options, including font, cursor, and palette, in <settings>Settings → Terminal</settings>'
|
||||
)}
|
||||
components={{ settings: <span className="font-medium text-foreground" /> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -294,3 +350,18 @@ function humanFields(diff: Partial<GlobalSettings>): 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 (
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{title}
|
||||
<span className="ml-2 text-xs font-normal text-muted-foreground">{caption}</span>
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, string> }>(
|
||||
'../../../../../resources/plugins/launch/stablyai.orca-{github-vscode-theme,vscode-default-themes}/themes/*.json',
|
||||
{ eager: true, import: 'default' }
|
||||
)
|
||||
const terminalThemeFiles = import.meta.glob<PluginTerminalThemeRegistration['terminal']>(
|
||||
'../../../../../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 })
|
||||
}
|
||||
@@ -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<GlobalSettings> = {}) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -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<GlobalSettings> {
|
||||
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<GlobalSettings> {
|
||||
const dark = look.base === 'dark'
|
||||
let next: Partial<GlobalSettings> = 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 ?? []
|
||||
}
|
||||
}
|
||||
@@ -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<ThemeLookBase, readonly string[]> = {
|
||||
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<ThemeLookBase, Omit<ThemeLookPalette, 'terminal'>> = {
|
||||
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]
|
||||
}
|
||||
@@ -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<SetStateAction<number>>
|
||||
selectedAgent: TuiAgent | null
|
||||
themeStepEntryThemeRef: { current: GlobalSettings['theme'] | null }
|
||||
themeLookBaseline: GlobalSettings | null
|
||||
setTheme: Dispatch<SetStateAction<GlobalSettings['theme']>>
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void> | 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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -161,20 +161,14 @@ export function useOnboardingFlow(
|
||||
useEffect(() => {
|
||||
paintedThemeRef.current = paintedTheme
|
||||
}, [paintedTheme])
|
||||
const themeStepEntryThemeRef = useRef<GlobalSettings['theme'] | null>(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<GlobalSettings | null>(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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user