refactor(gui): copy-free settings shell; ui-settings-general owns ownerless copy
The shell is now a pure composition face: no dictionaries, no locale dependency, and three new chrome content seats (settings.trigger / settings.header / settings.close) whose slot content also carries the accessible names (trigger text, dialog aria-labelledby, visually hidden close label). ui-settings-general returns as the owner of copy that belongs to no single feature: chrome content, the General section with its item slot, and the settings dictionaries. Slot types split homes — trigger/header/close/section live in the shell contract; the settings.general.item entry moves to the locale package (the common dependency of every item registrant), with ui-theme consuming it through a re-export seam; the verbatim duplicate merges are gone and the dependency graph is a clean DAG.
This commit is contained in:
@@ -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: 0cfa244f7e9856aede3d01a80404a794374e16be
|
||||
2026-07-25-client-settings-locale-theme.zh.md: 7caa79d5409b2a11078efb8e52273dc70f89f085
|
||||
2026-07-25-client-settings-locale-theme.md: b6a127037c50066fe9aa501bb73005b9c56869fa
|
||||
2026-07-25-client-settings-locale-theme.zh.md: a871f06945fb420016df95b23167e72f59c3e5c5
|
||||
|
||||
@@ -10,9 +10,9 @@ The browser client's existing Settings is written directly inside the Sidebar, a
|
||||
|
||||
## Proposal
|
||||
|
||||
**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell provides only the composition surface (the top-level section list plus the item list inside General) and neither imports nor enumerates any feature; for a feature to appear in Settings, its own plugin registers into the corresponding slot — locale registers the Language row, ui-theme registers the Appearance row, ui-models registers the Models top-level panel. No separate `ui-settings-*` package is created for "a feature's settings page": the settings surface belongs to the feature package itself (shipping the Theme feature means Theme's settings choices ship with ui-theme). The only content the shell carries itself is the first top-level directory, General (skeleton rows plus the item slot declaration), because it belongs to no single feature.
|
||||
**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell is a pure composition surface: it only declares slots and renders the chrome structure — zero copy, no locale dependency, and neither importing nor enumerating any feature; for a feature to appear in Settings, its own plugin registers into the corresponding slot — locale registers the Language row, ui-theme registers the Appearance row, ui-models registers the Models top-level panel. No separate `ui-settings-*` package is created for "a feature's settings page": the settings surface belongs to the feature package itself (shipping the Theme feature means Theme's settings choices ship with ui-theme). Content that belongs to no single feature (the trigger/title/close chrome copy, the General directory with its skeleton rows, the `settings` dictionary) is owned by `ui-settings-general` — the owner of the ownerless copy, not a feature satellite package.
|
||||
|
||||
The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares the `settings.section` list slot. Each section is contributed by a feature plugin; the Settings shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`. General is registered by the shell itself (order 0) and declares the `settings.general.item` list slot, into which the feature plugins' preference rows slot by order.
|
||||
The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares four slots: `settings.trigger` / `settings.header` / `settings.close` (chrome content seats, single) and `settings.section` (top-level pages, list). Accessible names all resolve from slot content: the trigger's accessible name is its text content, the dialog points at the header content node via aria-labelledby, and close is a visually hidden text seat. Each section is contributed by a feature plugin; the shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`. General is registered by `ui-settings-general` (order 0) and declares the `settings.general.item` list slot, into which the feature plugins' preference rows slot by order.
|
||||
|
||||
The Settings entry is the Settings row in the sidebar Foot; clicking it directly opens a 1080×700 centered overlay (black 24% mask); the close button, a mask click, and ESC all close it. There is no intermediate menu form of any kind.
|
||||
|
||||
@@ -28,13 +28,14 @@ The theme service never touches the DOM. `ui-layout` reads the Theme getter init
|
||||
|
||||
| Registration surface | Owning plugin | First-phase content |
|
||||
|---|---|---|
|
||||
| General section (order 0) | built into the `ui-settings` shell | Permission and Tool Call visual skeletons (no write operations) plus the `settings.general.item` slot declaration |
|
||||
| chrome content (trigger/header/close) | `ui-settings-general` | Settings entry-row icon and copy, panel title, close hidden text |
|
||||
| General section (order 0) | `ui-settings-general` | Permission and Tool Call visual skeletons (no write operations) plus the `settings.general.item` slot declaration |
|
||||
| Language row (item order 0) | `locale` | Selector dropdown; 中文/English genuinely switch |
|
||||
| Appearance row (item order 10) | `ui-theme` | Light/Dark/System three cubes genuinely switch (the selected state reflects preference) |
|
||||
| Models section (order 10) | `ui-models` | Navigation item only, with an empty content area; later model-management features land in that package |
|
||||
| Plugin | none | Not built this phase, and the navigation does not show the item (once a later plugin feature package registers the section it appears automatically) |
|
||||
|
||||
The first phase localizes only the copy inside the Settings overlay; dictionaries stay close to their owners — shell copy (the chrome plus the General skeletons) lives in the `settings` namespace, and feature-row copy lives in each feature package (`settings.locale`, `settings.theme`, `settings.models`).
|
||||
The first phase localizes only the copy inside the Settings overlay; dictionaries stay close to their owners — the chrome plus the General skeletons live in `ui-settings-general`'s `settings` namespace, and feature-row copy lives in each feature package (`settings.locale`, `settings.theme`, `settings.models`).
|
||||
|
||||
### Slot topology
|
||||
|
||||
@@ -42,16 +43,19 @@ The first phase localizes only the copy inside the Settings overlay; dictionarie
|
||||
root
|
||||
└─ sidebar
|
||||
└─ sidebar.settings single/root
|
||||
└─ ui-settings(壳)
|
||||
└─ ui-settings(壳,零文案)
|
||||
├─ settings.trigger single/root ui-settings-general 注册
|
||||
├─ settings.header single/root ui-settings-general 注册
|
||||
├─ settings.close single/root ui-settings-general 注册
|
||||
└─ settings.section list/root
|
||||
├─ general (order 0) ui-settings 壳自带
|
||||
├─ general (order 0) ui-settings-general 注册
|
||||
│ └─ settings.general.item list/root
|
||||
│ ├─ language (0) locale 注册
|
||||
│ └─ appearance (10) ui-theme 注册
|
||||
└─ models (order 10) ui-models 注册
|
||||
```
|
||||
|
||||
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, `refresh()` for localized labels, one-call disposal) and do not depend on the client manifest's apply order. The `settings.general.item` SlotMap entry's canonical home is the ui-settings contract; locale/ui-theme, because of the reference cycle (the shell consumes ctx.locale), consume that entry as verbatim duplicated merges, with declaration merging guaranteeing the copies agree.
|
||||
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, `refresh()` for localized labels, one-call disposal) and do not depend on the client manifest's apply order. The SlotMap types split homes: trigger/header/close/section have their canonical home in the ui-settings contract (the consumers, general and models, both depend on the shell — no cycle); `settings.general.item`'s canonical home is the locale package — it is the lowest common dependency of all item registrants (a settings row always carries copy), while the declarer general's contract is unreachable from locale/ui-theme (it would form a cycle); ui-theme consumes it through a re-export seam.
|
||||
|
||||
### Future work: promote slot declarations to first-class injectable waits
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ Status: proposed
|
||||
|
||||
## Proposal
|
||||
|
||||
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳只提供组合面(一级 section 列表 + General 内的 item 列表),不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应坑位注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。壳自带的唯一内容是第一个一级目录 General(骨架行 + item 坑位声明),因为它不属于任何单一功能。
|
||||
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳是纯组合面:只声明坑位、渲染 chrome 结构,零文案、不依赖 locale、不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应坑位注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。不属于任何单一功能的内容(trigger/标题/close 的 chrome 文案、General 目录与骨架行、`settings` 字典)由 `ui-settings-general` 拥有——它是「无主文案」的属主,不是功能卫星包。
|
||||
|
||||
Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明 `settings.section` list 坑位。每个 section 由功能插件贡献;Settings 壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由壳自己注册(order 0)并声明 `settings.general.item` list 坑位,功能插件的偏好行按 order 排入。
|
||||
Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明四个坑:`settings.trigger` / `settings.header` / `settings.close`(chrome 内容座,single)与 `settings.section`(一级页面,list)。无障碍名全部解析自坑内容:trigger 的可达名即其文本内容,dialog 经 aria-labelledby 指向 header 内容节点,close 是视觉隐藏文本座。每个 section 由功能插件贡献;壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由 `ui-settings-general` 注册(order 0)并声明 `settings.general.item` list 坑位,功能插件的偏好行按 order 排入。
|
||||
|
||||
Settings 入口是 sidebar Foot 的 Settings 行,点击直接打开 1080×700 居中浮层(黑 24% 遮罩);close 按钮、点击遮罩、ESC 均关闭。无任何中间菜单形态。
|
||||
|
||||
@@ -28,13 +28,14 @@ Theme service 不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订
|
||||
|
||||
| 注册面 | 属主插件 | 首期内容 |
|
||||
|---|---|---|
|
||||
| General section(order 0)| `ui-settings` 壳自带 | Permission、Tool Call 视觉骨架(无写操作)+ `settings.general.item` 坑位声明 |
|
||||
| chrome 内容(trigger/header/close)| `ui-settings-general` | 设置入口行图标+文案、面板标题、close 隐藏文本 |
|
||||
| General section(order 0)| `ui-settings-general` | Permission、Tool Call 视觉骨架(无写操作)+ `settings.general.item` 坑位声明 |
|
||||
| Language 行(item order 0)| `locale` | Selector 下拉,中文/English 真实可切 |
|
||||
| Appearance 行(item order 10)| `ui-theme` | Light/Dark/System 三 cube 真实可切(选中态看 preference) |
|
||||
| Models section(order 10)| `ui-models` | 仅导航项,内容区为空;后续模型管理功能落在该包 |
|
||||
| Plugin | 无 | 首期不做,导航不出现该项(后续插件功能包注册 section 即自动出现) |
|
||||
|
||||
首期只翻译 Settings 浮层内文案;字典就近——壳文案(chrome + General 骨架)归 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
|
||||
首期只翻译 Settings 浮层内文案;字典就近——chrome + General 骨架归 `ui-settings-general` 的 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
|
||||
|
||||
### Slot topology
|
||||
|
||||
@@ -42,16 +43,19 @@ Theme service 不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订
|
||||
root
|
||||
└─ sidebar
|
||||
└─ sidebar.settings single/root
|
||||
└─ ui-settings(壳)
|
||||
└─ ui-settings(壳,零文案)
|
||||
├─ settings.trigger single/root ui-settings-general 注册
|
||||
├─ settings.header single/root ui-settings-general 注册
|
||||
├─ settings.close single/root ui-settings-general 注册
|
||||
└─ settings.section list/root
|
||||
├─ general (order 0) ui-settings 壳自带
|
||||
├─ general (order 0) ui-settings-general 注册
|
||||
│ └─ settings.general.item list/root
|
||||
│ ├─ language (0) locale 注册
|
||||
│ └─ appearance (10) ui-theme 注册
|
||||
└─ models (order 10) ui-models 注册
|
||||
```
|
||||
|
||||
section/item contribution 均使用 declaration-aware deferral(ui-slots 的 `deferRegistration()`:ledger 判在位、`refresh()` 换本地化 label、一键 dispose),不依赖 client manifest 的 apply 顺序。`settings.general.item` 的 SlotMap 条目正家在 ui-settings contract;locale/ui-theme 因引用环(壳消费 ctx.locale)以逐字重复合并的方式消费该条目,declaration merging 保证副本一致。
|
||||
section/item contribution 均使用 declaration-aware deferral(ui-slots 的 `deferRegistration()`:ledger 判在位、`refresh()` 换本地化 label、一键 dispose),不依赖 client manifest 的 apply 顺序。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
|
||||
|
||||
### Future work:坑位声明升格为可 inject 的一等等待物
|
||||
|
||||
|
||||
Reference in New Issue
Block a user