docs(gui): regenerate graphs and sync the note's English pair

Generated docs follow the ui-models rename and the removed
ui-settings-general package; the English note side picks up the
feature-owner self-registration doctrine.
This commit is contained in:
imccyu
2026-07-26 02:54:29 +08:00
parent 23a60ade67
commit a3ee5dd8a0
11 changed files with 250 additions and 29 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-25-client-settings-locale-theme.md: e1245a9e1fac82fb0feb84af7a59945b17fa1daf
2026-07-25-client-settings-locale-theme.zh.md: 195b2e5ffa3556dd1b8bf2dd6ec8ae84115fbdb2
2026-07-25-client-settings-locale-theme.md: 658e6bd3c2da39a98e476c60f10f3f51ad82e5ea
2026-07-25-client-settings-locale-theme.zh.md: dfe93a0ca1f53380c653886c73fe0c32b08d8443
@@ -119,4 +119,4 @@ Locale ships with 中文 and English built in; `setLocale`/`setTheme` are the on
## Risks
The apply order of slot declarations and contributions is not fixed, so every new section must keep declaration-aware registration and idempotence guards. Service events may fire before a section's first render, so both the General store's init and the controller attach must align to the current snapshot from the getters. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR.
The apply order of slot declarations and contributions is not fixed, so every section/item registrant must keep declaration-aware registration and judge presence by the ledger, not by a local disposer. Service events may fire before a row's first render, so both a feature row store's init and the inject attach must align to the current snapshot from the getter. The duplicated merge copies of `settings.general.item` (locale, ui-theme) must stay verbatim-identical to the ui-settings canonical home — any drift means changing all three together. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR.
+1 -2
View File
@@ -2022,10 +2022,9 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
- `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
- `@deepseek-ai/dsh-client-ui-layout` ([`packages/client/ui-layout/src/index.ts`](../packages/client/ui-layout/src/index.ts))
- `@deepseek-ai/dsh-client-ui-models` ([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts))
- `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-models` ([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts))
- `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
+2 -2
View File
@@ -63,8 +63,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `internal/dispatch` | - | [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `hmr`, `modules`, `webserver` |
| `internal/status` | - | [`agent`](../packages/core/agent) |
| `locale/change` | `locale` (`emit`) | `ui-settings-general`, `ui-settings-models` |
| `locale/change` | `locale` (`emit`) | `locale`, `ui-models`, `ui-settings` |
| `slots/changed` | `runtime` (`emit`) | - |
| `theme/change` | `ui-theme` (`emit`) | `ui-layout`, `ui-settings-general` |
| `theme/change` | `ui-theme` (`emit`) | `ui-layout`, `ui-theme` |
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.
+21 -20
View File
@@ -141,11 +141,10 @@ flowchart TD
pkg_client_runtime["client-runtime"]
pkg_client_ui_conversation["client-ui-conversation"]
pkg_client_ui_layout["client-ui-layout"]
pkg_client_ui_models["client-ui-models"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_question["client-ui-question"]
pkg_client_ui_settings["client-ui-settings"]
pkg_client_ui_settings_general["client-ui-settings-general"]
pkg_client_ui_settings_models["client-ui-settings-models"]
pkg_client_ui_sidebar["client-ui-sidebar"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_theme["client-ui-theme"]
@@ -228,13 +227,11 @@ flowchart TD
pkg_subagent_subprocess --> pkg_invariants
pkg_acp_snapshot --> pkg_invariants
pkg_loader_smoke --> pkg_invariants
pkg_client_locale --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_runtime --> pkg_invariants
pkg_client_ui_primitives --> pkg_invariants
pkg_client_ui_question --> pkg_invariants
pkg_client_ui_slots --> pkg_invariants
pkg_client_ui_theme --> pkg_invariants
pkg_client_ui_trajectory --> pkg_invariants
pkg_client_web --> pkg_invariants
pkg_client_web_react --> pkg_invariants
@@ -250,25 +247,21 @@ flowchart TD
pkg_client_hmr --> pkg_client_modules
pkg_client_hmr --> pkg_host_webserver
pkg_client_hmr --> pkg_invariants
pkg_client_locale --> pkg_client_runtime
pkg_client_locale --> pkg_client_ui_primitives
pkg_client_locale --> pkg_client_ui_slots
pkg_client_locale --> pkg_invariants
pkg_client_ui_conversation --> pkg_client_runtime
pkg_client_ui_conversation --> pkg_client_ui_primitives
pkg_client_ui_conversation --> pkg_client_ui_slots
pkg_client_ui_conversation --> pkg_invariants
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_slots
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_client_ui_models --> pkg_client_runtime
pkg_client_ui_models --> pkg_client_ui_slots
pkg_client_ui_models --> pkg_invariants
pkg_client_ui_settings --> pkg_client_runtime
pkg_client_ui_settings --> pkg_client_ui_primitives
pkg_client_ui_settings --> pkg_client_ui_slots
pkg_client_ui_settings --> pkg_invariants
pkg_client_ui_settings_general --> pkg_client_runtime
pkg_client_ui_settings_general --> pkg_client_ui_primitives
pkg_client_ui_settings_general --> pkg_client_ui_slots
pkg_client_ui_settings_general --> pkg_invariants
pkg_client_ui_settings_models --> pkg_client_runtime
pkg_client_ui_settings_models --> pkg_client_ui_slots
pkg_client_ui_settings_models --> pkg_invariants
pkg_client_ui_sidebar --> pkg_client_runtime
pkg_client_ui_sidebar --> pkg_client_ui_primitives
pkg_client_ui_sidebar --> pkg_client_ui_slots
@@ -303,6 +296,11 @@ flowchart TD
pkg_system_prompt --> pkg_scope
pkg_web --> pkg_invariants
pkg_web --> pkg_llm
pkg_client_ui_theme --> pkg_client_locale
pkg_client_ui_theme --> pkg_client_runtime
pkg_client_ui_theme --> pkg_client_ui_primitives
pkg_client_ui_theme --> pkg_client_ui_slots
pkg_client_ui_theme --> pkg_invariants
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_invariants
pkg_lsp --> pkg_llm
@@ -355,6 +353,10 @@ flowchart TD
pkg_app_boot --> pkg_invariants
pkg_app_boot --> pkg_paths
pkg_app_boot --> pkg_system_prompt
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_slots
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_code_runtime_worker --> pkg_code_runtime
pkg_code_runtime_worker --> pkg_invariants
pkg_code_runtime_worker --> pkg_session
@@ -813,13 +815,11 @@ flowchart TD
| [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | [`invariants`](../packages/support/invariants) |
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`invariants`](../packages/support/invariants) |
| [`client-locale`](../packages/client/locale) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-ui-slots`](../packages/client/ui-slots) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/support/invariants) |
@@ -831,11 +831,10 @@ flowchart TD
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
@@ -848,6 +847,7 @@ flowchart TD
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -865,6 +865,7 @@ flowchart TD
| [`session-title`](../packages/session-title/session-title) | `session-title` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`app-boot`](../packages/ui/app-boot) | `ui` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`timeout`](../packages/util/timeout) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
@@ -0,0 +1,82 @@
// @vitest-environment jsdom
/** LanguageRow behavior: selector pill shows the active locale, the menu
* opens/closes, and selection drives setLocale. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { LanguageRow } from '../src/client/LanguageRow.tsx'
import type { LanguageRowComponentProps } from '../src/client/LanguageRow.tsx'
import { createLanguageRowStore } from '../src/client/settings-store.ts'
afterEach(cleanup)
const OPTIONS = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }]
/** Empty global standard-kit hooks (the row reads neither). */
function emptySessions() {
const store = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' })
return bindSnapshotSelector(store)
}
function emptyWorkspaces() {
const store = createSnapshotStore<WorkspaceListState>({
items: [], intent: undefined, state: 'idle', phase: 'ready', error: null,
baselinesReady: true, recentWorkspaceId: undefined,
})
return bindSnapshotSelector(store)
}
function mount(active = 'en') {
// Real store instance — the sanctioned zero-machinery path for tests.
const store = createLanguageRowStore().create()
store.actions.sync(active, OPTIONS, 0)
const setLocale = vi.fn()
const props: LanguageRowComponentProps = {
useSessions: emptySessions(),
useWorkspaces: emptyWorkspaces(),
useStore: bindSnapshotSelector(store),
actions: store.actions,
t: (key: string) => key === 'language.title' ? 'Language' : key,
setLocale,
}
render(<LanguageRow {...props} />)
return { store, setLocale }
}
describe('LanguageRow', () => {
it('shows the title and the active locale label on the selector pill', () => {
mount('en')
expect(screen.getByText('Language')).toBeDefined()
const trigger = screen.getByRole('button', { name: /English/ })
expect(trigger.getAttribute('aria-expanded')).toBe('false')
})
it('opens the menu, selects a locale, and closes', () => {
const b = mount('en')
const trigger = screen.getByRole('button', { name: /English/ })
fireEvent.click(trigger)
expect(trigger.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(screen.getByRole('menuitem', { name: '中文' }))
expect(b.setLocale).toHaveBeenCalledWith('zh')
expect(trigger.getAttribute('aria-expanded')).toBe('false')
expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull()
})
it('closes on outside pointerdown without selecting', () => {
const b = mount('en')
fireEvent.click(screen.getByRole('button', { name: /English/ }))
expect(screen.getByRole('menuitem', { name: '中文' })).toBeDefined()
fireEvent.pointerDown(document.body)
expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull()
expect(b.setLocale).not.toHaveBeenCalled()
})
it('follows store changes; an unknown active id falls back to the id itself', () => {
const b = mount('en')
act(() => { b.store.actions.sync('zh', OPTIONS, 1) })
expect(screen.getByRole('button', { name: /中文/ })).toBeDefined()
act(() => { b.store.actions.sync('fr', OPTIONS, 2) })
expect(screen.getByRole('button', { name: /fr/ })).toBeDefined()
})
})
@@ -0,0 +1,30 @@
/** Language row store: snapshot-mirror action and the revision guard. */
import { describe, expect, it } from 'vitest'
import { createLanguageRowStore } from '../src/client/settings-store.ts'
const OPTIONS = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }]
describe('createLanguageRowStore', () => {
it('init shape: empty mirror with revision at -1', () => {
const store = createLanguageRowStore().create()
expect(store.getSnapshot()).toEqual({ active: '', options: [], revision: -1 })
})
it('sync mirrors the snapshot and advances the revision', () => {
const store = createLanguageRowStore().create()
store.actions.sync('zh', OPTIONS, 0)
expect(store.getSnapshot()).toEqual({ active: 'zh', options: OPTIONS, revision: 0 })
store.actions.sync('en', OPTIONS, 1)
expect(store.getSnapshot().active).toBe('en')
expect(store.getSnapshot().revision).toBe(1)
})
it('revision guard drops stale and duplicate writes', () => {
const store = createLanguageRowStore().create()
store.actions.sync('en', OPTIONS, 5)
store.actions.sync('zh', OPTIONS, 4)
store.actions.sync('zh', OPTIONS, 5)
expect(store.getSnapshot().active).toBe('en')
expect(store.getSnapshot().revision).toBe(5)
})
})
@@ -82,10 +82,12 @@ describe('ui-settings apply', () => {
// empty once apply settles.
expect(injected.sections()).toEqual([{ id: 'general', order: 0, label: '通用设置' }])
b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null)
b.slots.register({ name: 'settings.section', id: 'a', order: 5 } as never, () => null)
// No order and no label: both projection defaults apply (order 0 ties
// keep registration sequence, so 'a' lands after the General entry).
b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null)
expect(injected.sections()).toEqual([
{ id: 'general', order: 0, label: '通用设置' },
{ id: 'a', order: 5, label: '' },
{ id: 'a', order: 0, label: '' },
{ id: 'z', order: 20, label: 'Z' },
])
expect(injected.sectionsVersion()).toBe(b.slots.getVersion('settings.section'))
@@ -11,7 +11,11 @@ function mount() {
const renderSlot = vi.fn(
((key: string) => <div data-testid={`slot-${key}`} />) as GeneralSectionComponentProps['renderSlot'],
)
// Global standard kit stubs: the section consumes neither hook.
const unusedHook = (() => { throw new Error('unused by GeneralSection') }) as never
const props: GeneralSectionComponentProps = {
useSessions: unusedHook,
useWorkspaces: unusedHook,
t: (key) => en[key] ?? key,
renderSlot,
}
@@ -0,0 +1,75 @@
// @vitest-environment jsdom
/** AppearanceRow behavior: three cubes, selection follows the persisted
* preference, clicks drive setTheme. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { AppearanceRow } from '../src/client/AppearanceRow.tsx'
import type { AppearanceRowComponentProps } from '../src/client/AppearanceRow.tsx'
import { createAppearanceRowStore } from '../src/client/settings-store.ts'
import type { ThemePreference } from '../src/client/index.ts'
afterEach(cleanup)
const COPY: Record<string, string> = {
'appearance.title': 'Appearance',
'appearance.light': 'Light',
'appearance.dark': 'Dark',
'appearance.system': 'System',
}
/** Empty global standard-kit hooks (the row reads neither). */
function emptySessions() {
const store = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' })
return bindSnapshotSelector(store)
}
function emptyWorkspaces() {
const store = createSnapshotStore<WorkspaceListState>({
items: [], intent: undefined, state: 'idle', phase: 'ready', error: null,
baselinesReady: true, recentWorkspaceId: undefined,
})
return bindSnapshotSelector(store)
}
function mount(preference: ThemePreference = 'system') {
// Real store instance — the sanctioned zero-machinery path for tests.
const store = createAppearanceRowStore().create()
store.actions.sync(preference, 0)
const setTheme = vi.fn()
const props: AppearanceRowComponentProps = {
useSessions: emptySessions(),
useWorkspaces: emptyWorkspaces(),
useStore: bindSnapshotSelector(store),
actions: store.actions,
t: (key: string) => COPY[key] ?? key,
setTheme,
}
render(<AppearanceRow {...props} />)
return { store, setTheme }
}
const pressed = (name: RegExp): string | null =>
screen.getByRole('button', { name }).getAttribute('aria-pressed')
describe('AppearanceRow', () => {
it('renders the title and three cubes with the preference cube selected', () => {
mount('dark')
expect(screen.getByText('Appearance')).toBeDefined()
expect(pressed(/Dark/)).toBe('true')
expect(pressed(/Light/)).toBe('false')
expect(pressed(/System/)).toBe('false')
})
it('click drives setTheme; selection follows the store mirror, not the click echo', () => {
const b = mount('dark')
fireEvent.click(screen.getByRole('button', { name: /Light/ }))
expect(b.setTheme).toHaveBeenCalledWith('light')
// No store write yet: selection is unchanged.
expect(pressed(/Dark/)).toBe('true')
act(() => { b.store.actions.sync('light', 1) })
expect(pressed(/Light/)).toBe('true')
expect(pressed(/Dark/)).toBe('false')
})
})
@@ -0,0 +1,28 @@
/** Appearance row store: snapshot-mirror action and the revision guard. */
import { describe, expect, it } from 'vitest'
import { createAppearanceRowStore } from '../src/client/settings-store.ts'
describe('createAppearanceRowStore', () => {
it('init shape: system preference with revision at -1', () => {
const store = createAppearanceRowStore().create()
expect(store.getSnapshot()).toEqual({ preference: 'system', revision: -1 })
})
it('sync mirrors the preference and advances the revision', () => {
const store = createAppearanceRowStore().create()
store.actions.sync('dark', 0)
expect(store.getSnapshot()).toEqual({ preference: 'dark', revision: 0 })
store.actions.sync('light', 2)
expect(store.getSnapshot().preference).toBe('light')
expect(store.getSnapshot().revision).toBe(2)
})
it('revision guard drops stale and duplicate writes', () => {
const store = createAppearanceRowStore().create()
store.actions.sync('dark', 3)
store.actions.sync('system', 2)
store.actions.sync('system', 3)
expect(store.getSnapshot().preference).toBe('dark')
expect(store.getSnapshot().revision).toBe(3)
})
})