feat(gui): settings panel with locale and theme preferences
Add the browser Settings surface as slot-composed plugins over new preference services: - Rename dsh-client-i18n to dsh-client-locale (locale is the domain name); LocaleService adds getLocale()/setLocale(id), immutable snapshots, a locale/change event, and dsh.locale persistence. - ThemeService owns the light/dark/system preference (default system), resolves system via prefers-color-scheme, publishes theme/change snapshots, persists dsh.theme, and no longer touches the DOM; ui-layout's ThemePresenter applies resolved snapshots (body[data-ds-dark-theme] + alias tokens) and cleans up on dispose. - ui-sidebar drops the phase-1 settings dropdown/modal; the foot renders the new sidebar.settings slot with the column state. - New ui-settings shell occupies sidebar.settings: foot trigger row and the centered 1080x700 panel (figma 501:29947) with 24% mask, close button / mask click / Escape all closing, and a 188px nav projected from the settings.section list slot it declares. Nav labels are registrant-localized; sections re-register on locale change, so the ledger version is the shell's only subscription. - ui-settings-general registers the General section: Permission and Tool Call skeletons, live Language (locale menu) and Appearance (Light/Dark/System cubes following the persisted preference); its slot store mirrors both service snapshots via apply-side listeners. - ui-settings-models registers the Models nav entry with an empty content column. - Portaled menus pin z-index above modal overlays (a menu anchored inside the settings dialog rendered underneath it and was unclickable). - theme/data/list-pen icons in ui-primitives; settings copy ships as zh/en dictionaries; fixture manifests gain the settings rows.
This commit is contained in:
@@ -32,16 +32,16 @@ const workspaces: WorkspaceListState = {
|
||||
function mount(sessionState: SessionListState = sessions) {
|
||||
const startSession = vi.fn()
|
||||
const open = vi.fn()
|
||||
let pickerOwner: unknown
|
||||
const owners: Record<string, unknown> = {}
|
||||
const view = render(
|
||||
<SidebarRoot
|
||||
collapsed={false} width={300}
|
||||
useSessions={hook(sessionState)} useWorkspaces={hook(workspaces)}
|
||||
startSession={startSession} open={open} toggleSidebar={vi.fn()}
|
||||
renderSlot={((_key: string, owner: unknown) => { pickerOwner = owner; return null }) as SidebarRootComponentProps['renderSlot']}
|
||||
renderSlot={((key: string, owner: unknown) => { owners[key] = owner; return null }) as SidebarRootComponentProps['renderSlot']}
|
||||
/>,
|
||||
)
|
||||
return { view, startSession, open, pickerOwner: () => pickerOwner }
|
||||
return { view, startSession, open, pickerOwner: () => owners['sidebar.workspace'], settingsOwner: () => owners['sidebar.settings'] }
|
||||
}
|
||||
|
||||
function mountSidebar({
|
||||
@@ -58,14 +58,14 @@ function mountSidebar({
|
||||
const startSession = vi.fn()
|
||||
const open = vi.fn()
|
||||
const toggleSidebar = vi.fn()
|
||||
let pickerOwner: unknown
|
||||
const owners: Record<string, unknown> = {}
|
||||
let current = { sessionState, workspaceState, collapsed, width }
|
||||
const root = () => (
|
||||
<SidebarRoot
|
||||
collapsed={current.collapsed} width={current.width}
|
||||
useSessions={hook(current.sessionState)} useWorkspaces={hook(current.workspaceState)}
|
||||
startSession={startSession} open={open} toggleSidebar={toggleSidebar}
|
||||
renderSlot={((_key: string, owner: unknown) => { pickerOwner = owner; return null }) as SidebarRootComponentProps['renderSlot']}
|
||||
renderSlot={((key: string, owner: unknown) => { owners[key] = owner; return null }) as SidebarRootComponentProps['renderSlot']}
|
||||
/>
|
||||
)
|
||||
const view = render(root())
|
||||
@@ -73,7 +73,8 @@ function mountSidebar({
|
||||
startSession,
|
||||
open,
|
||||
toggleSidebar,
|
||||
pickerOwner: () => pickerOwner,
|
||||
pickerOwner: () => owners['sidebar.workspace'],
|
||||
settingsOwner: () => owners['sidebar.settings'],
|
||||
rerender(next: Partial<typeof current>) {
|
||||
current = { ...current, ...next }
|
||||
view.rerender(root())
|
||||
@@ -246,6 +247,7 @@ describe('SidebarRoot', () => {
|
||||
it('keeps wide content during live collapse, then settles to the rail', () => {
|
||||
vi.useFakeTimers()
|
||||
const b = mountSidebar({ width: 320 })
|
||||
expect((b.settingsOwner() as { wide: boolean }).wide).toBe(true)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }))
|
||||
expect(b.toggleSidebar).toHaveBeenCalledOnce()
|
||||
b.rerender({ collapsed: true, width: 56 })
|
||||
@@ -253,6 +255,8 @@ describe('SidebarRoot', () => {
|
||||
act(() => { vi.advanceTimersByTime(150) })
|
||||
expect(screen.queryByPlaceholderText('Search name, keywords...')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
|
||||
// The settings seat share tracks the settled column state.
|
||||
expect((b.settingsOwner() as { wide: boolean }).wide).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user