diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index 6aeda0e04b..26e909b964 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar is fixed-width (only details shrinks, then auto-closes); a closed sidebar retains a 56px control rail while details closes to zero width. The package also seats the theme presenter: it consumes resolved `ctx.theme` snapshots and projects them onto `document.body` (`data-ds-dark-theme` from the active color scheme plus the theme's alias tokens as inline variables). +Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar is fixed-width (only details shrinks, then auto-closes); a closed sidebar retains a 56px control rail while details closes to zero width. The package also seats the theme presenter: it consumes resolved `ctx.theme` snapshots and projects them onto the document (`html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, plus the theme's alias tokens as inline variables on body). AppFrame reads the runtime Session projection: `baselinesReady` selects loading, a page-local `SessionListState.intent` selects the empty composer, and a connected Session renders through `SessionProvider`. The conversation and empty-state owner shares are empty; each registrant obtains business data from standard hooks and actions from its own inject face. The sidebar owner share contains only `collapsed` and `width`; navigation actions belong to sidebar's own injected service face. diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index 9fbc2ed839..2e5799fd32 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏宽度固定(只会收缩详情栏,然后将其自动关闭);关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。该包还提供主题呈现器:它消费解析后的 `ctx.theme` 快照,并将其投影到 `document.body`(依据当前配色方案设置 `data-ds-dark-theme`,并将主题的别名 token 设为内联变量)。 +外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏宽度固定(只会收缩详情栏,然后将其自动关闭);关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。该包还提供主题呈现器:它消费解析后的 `ctx.theme` 快照,并将其投影到 document(用 `html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,并将主题的别名 token 设为 body 上的内联变量)。 AppFrame 读取运行时 Session 投影:`baselinesReady` 选择加载状态,页面局部的 `SessionListState.intent` 选择空白编辑器,已连接 Session 则通过 `SessionProvider` 渲染。会话及空状态的 owner share 为空;每个注册方通过标准 hook 获取业务数据,并从自身的 inject 表层获取操作。侧边栏 owner share 只包含 `collapsed` 和 `width`;导航操作属于侧边栏自身注入的服务表层。 diff --git a/packages/client/ui-layout/src/client/theme-presenter.ts b/packages/client/ui-layout/src/client/theme-presenter.ts index 958f2fd93e..07dc663c54 100644 --- a/packages/client/ui-layout/src/client/theme-presenter.ts +++ b/packages/client/ui-layout/src/client/theme-presenter.ts @@ -1,29 +1,33 @@ /** - * 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. + * Global theme DOM applier: projects the resolved ThemeSnapshot onto the + * document — `html { color-scheme }` for native UA chrome (scrollbars, form + * controls), `body[data-ds-dark-theme]` for the token palette, and the active + * theme's alias-token overrides as inline CSS variables on body. Pure DOM + * writes, no React involvement; the presenter only ever retracts what it wrote + * itself, so foreign 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. */ +/** Applies theme snapshots to the document; 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`. + * Project a snapshot onto the document: set root `color-scheme` and the body + * palette attribute from `active.colorScheme` (never the id — `system` is + * resolved upstream), then replace the previously applied token variables + * with `active.tokens`. * @param snapshot - resolved theme snapshot from ctx.theme. */ apply(snapshot: ThemeSnapshot): void { + const scheme = snapshot.active.colorScheme + document.documentElement.style.colorScheme = scheme const body = document.body - if (snapshot.active.colorScheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '') + if (scheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '') else body.removeAttribute(DARK_ATTRIBUTE) for (const name of this.appliedTokens) body.style.removeProperty(name) this.appliedTokens = [] @@ -33,8 +37,9 @@ export class ThemePresenter { } } - /** Retract everything this presenter wrote: the palette attribute and all applied token variables. */ + /** Retract everything this presenter wrote: root color-scheme, the palette attribute, and all applied token variables. */ dispose(): void { + document.documentElement.style.removeProperty('color-scheme') const body = document.body body.removeAttribute(DARK_ATTRIBUTE) for (const name of this.appliedTokens) body.style.removeProperty(name) diff --git a/packages/client/ui-layout/tests/apply.spec.ts b/packages/client/ui-layout/tests/apply.spec.ts index 1382f5160d..903591163c 100644 --- a/packages/client/ui-layout/tests/apply.spec.ts +++ b/packages/client/ui-layout/tests/apply.spec.ts @@ -63,15 +63,19 @@ describe('ui-layout client apply', () => { const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() // Initial getter application: jsdom has no matchMedia, system resolves light. + expect(document.documentElement.style.colorScheme).toBe('light') expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false) const theme = ctx.get('theme') as ThemeService theme.setTheme('dark') + expect(document.documentElement.style.colorScheme).toBe('dark') expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(true) await fiber.dispose() + expect(document.documentElement.style.colorScheme).toBe('') expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false) - // Listener is off: further theme changes no longer reach the body. + // Listener is off: further theme changes no longer reach the document. theme.setTheme('light') theme.setTheme('dark') + expect(document.documentElement.style.colorScheme).toBe('') expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false) }) diff --git a/packages/client/ui-layout/tests/theme-presenter.spec.ts b/packages/client/ui-layout/tests/theme-presenter.spec.ts index ced83a379e..a14d781e5f 100644 --- a/packages/client/ui-layout/tests/theme-presenter.spec.ts +++ b/packages/client/ui-layout/tests/theme-presenter.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom -// ThemePresenter behavior account: the palette attribute follows -// active.colorScheme only, token variables replace the previous apply's set, -// and dispose retracts everything the presenter wrote. +// ThemePresenter behavior account: root color-scheme and the palette attribute +// follow active.colorScheme only, token variables replace the previous apply's +// set, and dispose retracts everything the presenter wrote. import { beforeEach, describe, expect, it } from 'vitest' import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' @@ -14,22 +14,26 @@ function snapshot(colorScheme: 'light' | 'dark', tokens: Record } beforeEach(() => { + document.documentElement.style.removeProperty('color-scheme') document.body.removeAttribute(DARK_ATTRIBUTE) document.body.removeAttribute('style') }) describe('ThemePresenter', () => { - it('light scheme leaves the dark attribute absent', () => { + it('light scheme sets root color-scheme and leaves the dark attribute absent', () => { const presenter = new ThemePresenter() presenter.apply(snapshot('light')) + expect(document.documentElement.style.colorScheme).toBe('light') expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false) }) - it('dark scheme sets the attribute; switching back to light removes it', () => { + it('dark scheme sets root color-scheme and the attribute; switching to light clears both', () => { const presenter = new ThemePresenter() presenter.apply(snapshot('dark')) + expect(document.documentElement.style.colorScheme).toBe('dark') expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true) presenter.apply(snapshot('light')) + expect(document.documentElement.style.colorScheme).toBe('light') expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false) }) @@ -44,11 +48,12 @@ describe('ThemePresenter', () => { expect(document.body.style.getPropertyValue('--dsw-alias-fg')).toBe('') }) - it('dispose removes the attribute and every applied variable, sparing foreign inline styles', () => { + it('dispose removes color-scheme, the attribute, and every applied variable, sparing foreign inline styles', () => { document.body.style.setProperty('--foreign', 'kept') const presenter = new ThemePresenter() presenter.apply(snapshot('dark', { '--dsw-alias-bg': '#111' })) presenter.dispose() + expect(document.documentElement.style.colorScheme).toBe('') expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false) expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('') expect(document.body.style.getPropertyValue('--foreign')).toBe('kept') diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index 5c9794d4f5..1227df357c 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Theme plugin: ThemeService over the --dsw-* token base stylesheets (static scale + alias semantic layers). The service owns the theme preference (`light`/`dark`/`system`, persisted under `dsh.theme`), resolves `system` through `prefers-color-scheme`, and publishes immutable `ThemeSnapshot`s on the `theme/change` event; it never touches the DOM — ui-layout's presenter applies the resolved snapshot (`body[data-ds-dark-theme]` + inline alias tokens). Contract: api-contracts v3 §8. +Theme plugin: ThemeService over the --dsw-* token base stylesheets (static scale + alias semantic layers). The service owns the theme preference (`light`/`dark`/`system`, persisted under `dsh.theme`), resolves `system` through `prefers-color-scheme`, and publishes immutable `ThemeSnapshot`s on the `theme/change` event; it never touches the DOM — ui-layout's presenter applies the resolved snapshot (`html { color-scheme }`, `body[data-ds-dark-theme]`, and inline alias tokens). Contract: api-contracts v3 §8. ## Model Experience diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index 2e0f76133e..cd87ede726 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的 ThemeService。该服务拥有主题偏好(`light`/`dark`/`system`,以 `dsh.theme` 为键持久化),将 `system` 通过 `prefers-color-scheme` 解析为实际主题,并发布不可变的 `ThemeSnapshot`,通过 `theme/change` 事件通知变化;它绝不接触 DOM:ui-layout 的呈现器会应用解析后的快照(依据当前配色方案设置 `body[data-ds-dark-theme]`,并将主题的别名 token 设为内联变量)。契约:api-contracts v3 §8。 +主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的 ThemeService。该服务拥有主题偏好(`light`/`dark`/`system`,以 `dsh.theme` 为键持久化),将 `system` 通过 `prefers-color-scheme` 解析为实际主题,并发布不可变的 `ThemeSnapshot`,通过 `theme/change` 事件通知变化;它绝不接触 DOM:ui-layout 的呈现器会应用解析后的快照(`html { color-scheme }`、`body[data-ds-dark-theme]`,以及主题的别名 token 内联变量)。契约:api-contracts v3 §8。 ## 模型体验