feat(web): open the local settings file

This commit is contained in:
Yichen Jiang
2026-08-04 16:33:35 +08:00
parent 828cb2ae20
commit e31b7221e7
61 changed files with 939 additions and 98 deletions

View File

@@ -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 packages/client/ui-settings/README.md
README.md: 14c78c83467313a6efa7033c31fb9c9b1cd94e0c
README.zh.md: 8831842d03db4572f1dca5547b84c0d8a3be8f2d
README.md: de78d599b7833179339ceeb680fbd665b056bd83
README.zh.md: fdc6ade1413c7d02afba85ed3b69a2b78b401836

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Settings shell plugin: a pure composition face. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, and declares the slots registrants fill: `settings.trigger` / `settings.header` / `settings.close` (chrome content), `settings.section` (one page per feature), and `settings.onboarding` (ordered feature-owned pages in a full-viewport stage). The shell ships no copy of its own — all text arrives from registrants (ui-settings-general owns chrome, General, and the product notice; features own their sections, rows, and conditional onboarding pages). Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency).
Settings shell plugin: a pure composition face. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, and declares the slots registrants fill: `settings.trigger` / `settings.header` / `settings.close` (chrome content), `settings.action` (ordered content-header actions), `settings.section` (one page per feature), and `settings.onboarding` (ordered feature-owned pages in a full-viewport stage). The shell ships no copy of its own — all text arrives from registrants (ui-settings-general owns chrome, General, and the product notice; features own their actions, sections, rows, and conditional onboarding pages). Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency).
The shell projects the onboarding ledger into ascending order and mounts exactly one page at a time in a body-level stage while marking the underlying app root inert. The active registrant receives its id, `complete()`, and an `openSection(id)` callback; completing or skipping transfers ownership to the next entry. Registrants own durable completion, capability readiness, copy, and mutations, so independently registered flows cannot stack and the shell does not become a second configuration fact source.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
设置外壳插件:一个纯组合表层。它以触发控件和模态设置面板占用 `sidebar.settings`,并声明由注册方填充的 slot:`settings.trigger`/`settings.header`/`settings.close`(界面框架内容)、`settings.section`(每项功能一页)和 `settings.onboarding`(由各功能持有、显示在全视口展示层中的有序页面)。外壳不自带文案:所有文本都来自注册方(ui-settings-general 拥有界面框架、「通用」分区和产品声明;各功能拥有各自的分区、行和条件式首次使用引导页面)。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。
设置外壳插件:一个纯组合表层。它以触发控件和模态设置面板占用 `sidebar.settings`,并声明由注册方填充的 slot:`settings.trigger`/`settings.header`/`settings.close`(界面框架内容)、`settings.action`(内容标题栏中的有序操作)、`settings.section`(每项功能一页)和 `settings.onboarding`(由各功能持有、显示在全视口展示层中的有序页面)。外壳不自带文案:所有文本都来自注册方(ui-settings-general 拥有界面框架、「通用」分区和产品声明;各功能拥有各自的操作、分区、行和条件式首次使用引导页面)。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。
外壳将首次使用引导记录按升序投影,在 body 层级的展示层中每次只挂载一个页面,同时将下层应用根节点标记为 `inert`。当前注册方会收到该条目的 id、`complete()` 和 `openSection(id)` 回调;完成或跳过当前页面后,所有权转交给下一项。持久化完成状态、能力就绪状态、文案和变更操作均由注册方持有,因此独立注册的流程无法堆叠,外壳也不会成为第二个配置事实来源。

View File

@@ -167,12 +167,22 @@
flex: none;
display: flex;
align-items: flex-start;
justify-content: flex-end;
justify-content: space-between;
gap: 8px;
height: 54px;
padding: 20px 14px 8px 10px;
box-sizing: border-box;
}
.actions {
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
margin-left: auto;
}
/* Close button (figma .Icon_container 501:29982): 28x28, r28, 14px glyph. */
.close {
display: inline-flex;

View File

@@ -76,6 +76,7 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
</nav>
<div className={css.content}>
<div className={css.header}>
<div className={css.actions}>{renderSlot('settings.action', {})}</div>
<button ref={closeButton} type="button" className={css.close} onClick={onClose}>
<IconCloseOutline16 size={14} />
<span className={css.hiddenLabel}>{renderSlot('settings.close', {})}</span>

View File

@@ -2,8 +2,8 @@
* Settings shell slot contract — the canonical home of every settings slot
* type. The shell is a pure composition face with zero copy of its own: it
* occupies the sidebar-owned `sidebar.settings` hole and declares the slots
* below; ALL text (trigger label, panel title, close aria, section content)
* arrives from registrants. A feature owns its settings surface — adding a
* below; ALL text (trigger label, panel title, header actions, close aria,
* section content) arrives from registrants. A feature owns its settings surface — adding a
* setting never means editing the shell; copy that belongs to no single
* feature (chrome, the General section) is owned by ui-settings-general.
*/
@@ -29,6 +29,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* Absent contribution leaves the heading empty.
*/
'settings.header': { kind: 'single'; scope: 'root'; owner: SettingsHeaderOwnerProps }
/**
* Optional actions rendered in the content-column header before Close.
* Registrants own visibility, behavior, copy, and failure presentation;
* the shell supplies only the ordered render site.
*/
'settings.action': { kind: 'list'; scope: 'root'; owner: SettingsHeaderOwnerProps }
/**
* The close button's visually-hidden label text (the button itself —
* icon, geometry, focus — is shell chrome). Absent contribution leaves
@@ -125,6 +131,11 @@ export type SettingsRootInjected = {
export type SettingsRootComponentProps =
PropsRuntime<'sidebar.settings'>
& PropsRenderSlots<
'settings.trigger' | 'settings.header' | 'settings.close' | 'settings.section' | 'settings.onboarding'
| 'settings.trigger'
| 'settings.header'
| 'settings.action'
| 'settings.close'
| 'settings.section'
| 'settings.onboarding'
>
& InjectFace<SettingsRootInjected>

View File

@@ -103,6 +103,7 @@ export function apply(ctx: ClientContext): void {
children: {
'settings.trigger': { kind: 'single', scope: 'root' },
'settings.header': { kind: 'single', scope: 'root' },
'settings.action': { kind: 'list', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
'settings.onboarding': { kind: 'list', scope: 'root' },

View File

@@ -24,10 +24,11 @@ function injectedOf(slots: SlotsService): SettingsRootInjected {
return (entry.inject as () => SettingsRootInjected)()
}
/** The shell's five child declarations (chrome, sections, and onboarding overlays). */
/** The shell's child declarations (chrome, actions, sections, and onboarding overlays). */
const CHILD_SPECS = {
'settings.trigger': { kind: 'single', scope: 'root' },
'settings.header': { kind: 'single', scope: 'root' },
'settings.action': { kind: 'list', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
'settings.onboarding': { kind: 'list', scope: 'root' },
@@ -38,7 +39,7 @@ describe('ui-settings apply', () => {
expect(inject).toEqual(['slots'])
})
it('registers the shell and declares the five child slots, before or after the declaration', async () => {
it('registers the shell and declares every child slot, before or after the declaration', async () => {
const before = await bench()
declare(before.slots)
await before.ctx.plugin({ inject: [...inject], apply }).await()
@@ -124,7 +125,7 @@ describe('ui-settings apply', () => {
}
})
it('unregisters the shell and collapses all five child slots on teardown', async () => {
it('unregisters the shell and collapses every child slot on teardown', async () => {
const b = await bench()
declare(b.slots)
const fiber = b.ctx.plugin({ inject: [...inject], apply })

View File

@@ -14,6 +14,7 @@ type Step = { id: string; order: number }
const SEAT_CONTENT: Record<string, string> = {
'settings.trigger': 'Settings',
'settings.header': 'Settings Title',
'settings.action': 'Open configuration file',
'settings.close': 'Close',
}
@@ -114,6 +115,13 @@ describe('SettingsPanel chrome seats', () => {
expect(close.hasAttribute('aria-label')).toBe(false)
expect(close.textContent).toContain('Close')
})
it('renders header actions before the shell-owned close control', () => {
const { renderSlot } = mount()
openPanel()
expect(screen.getByText('Open configuration file')).toBeTruthy()
expect(renderSlot).toHaveBeenCalledWith('settings.action', {})
})
})
describe('SettingsPanel close paths', () => {