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:
@@ -4,13 +4,16 @@
|
||||
* four child slots (declaration = exclusive render authority), seats the
|
||||
* layout store (panel geometry), and wires the panel-action service face.
|
||||
* ctx.layout is the cross-plugin panel-action seam; navigation state lives
|
||||
* with the runtime sessions service.
|
||||
* with the runtime sessions service. A second effect seats the theme
|
||||
* presenter, which projects ctx.theme snapshots onto document.body.
|
||||
*/
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
import type { PanelActions } from './service.ts'
|
||||
import { AppFrame } from './AppFrame.tsx'
|
||||
import { createLayoutStore } from './stores.ts'
|
||||
import { LayoutService } from './service.ts'
|
||||
import { ThemePresenter } from './theme-presenter.ts'
|
||||
|
||||
// Contract surface only (export-convergence rule: cross-package consumers
|
||||
// keep a symbol exported; test-only/package-internal symbols live off /src).
|
||||
@@ -62,7 +65,7 @@ export interface DetailsOwnerProps {}
|
||||
export interface EmptyOwnerProps { children?: never }
|
||||
|
||||
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
|
||||
export const inject = ['slots']
|
||||
export const inject = ['slots', 'theme']
|
||||
|
||||
/**
|
||||
* Client plugin body: provide ctx.layout, then one register() call — AppFrame
|
||||
@@ -98,4 +101,16 @@ export function apply(ctx: ClientContext): void {
|
||||
void disposeService()
|
||||
}
|
||||
}, 'ui-layout: service + root registration')
|
||||
|
||||
// Theme presentation: pure DOM writes from resolved snapshots — initial
|
||||
// state through the getter once, then event-driven only; no React path.
|
||||
ctx.effect(() => {
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(ctx.theme.getTheme())
|
||||
const off = ctx.on('theme/change', snapshot => { presenter.apply(snapshot) })
|
||||
return () => {
|
||||
off()
|
||||
presenter.dispose()
|
||||
}
|
||||
}, 'ui-layout: theme presenter')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Global theme DOM applier: projects the resolved ThemeSnapshot onto
|
||||
* document.body — the `data-ds-dark-theme` palette switch plus the active
|
||||
* theme's alias-token overrides as inline CSS variables. Pure DOM writes, no
|
||||
* React involvement; the presenter only ever retracts what it wrote itself,
|
||||
* so foreign body attributes and inline styles survive apply/dispose.
|
||||
*/
|
||||
import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
|
||||
/** Body attribute selecting the dark base palette in the token stylesheets. */
|
||||
export const DARK_ATTRIBUTE = 'data-ds-dark-theme'
|
||||
|
||||
/** Applies theme snapshots to document.body; one instance per plugin fiber. */
|
||||
export class ThemePresenter {
|
||||
/** Token names this presenter wrote in the last apply (its retraction set). */
|
||||
private appliedTokens: string[] = []
|
||||
|
||||
/**
|
||||
* Project a snapshot onto the body: switch the palette attribute from
|
||||
* `active.colorScheme` (never the id — `system` is resolved upstream) and
|
||||
* replace the previously applied token variables with `active.tokens`.
|
||||
* @param snapshot - resolved theme snapshot from ctx.theme.
|
||||
*/
|
||||
apply(snapshot: ThemeSnapshot): void {
|
||||
const body = document.body
|
||||
if (snapshot.active.colorScheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '')
|
||||
else body.removeAttribute(DARK_ATTRIBUTE)
|
||||
for (const name of this.appliedTokens) body.style.removeProperty(name)
|
||||
this.appliedTokens = []
|
||||
for (const [name, value] of Object.entries(snapshot.active.tokens)) {
|
||||
body.style.setProperty(name, value)
|
||||
this.appliedTokens.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
/** Retract everything this presenter wrote: the palette attribute and all applied token variables. */
|
||||
dispose(): void {
|
||||
const body = document.body
|
||||
body.removeAttribute(DARK_ATTRIBUTE)
|
||||
for (const name of this.appliedTokens) body.style.removeProperty(name)
|
||||
this.appliedTokens = []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user