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:
@@ -0,0 +1,111 @@
|
||||
/* General section rows (figma 501:29983 'Options'): stacked groups, 16px
|
||||
* vertical padding each, hairline separator under all but the last child
|
||||
* (feature-contributed rows carry their own row chrome and separators; the
|
||||
* :last-child rule strips the trailing one wherever the column ends). */
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section > :last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Title + trailing control row (figma 'Setting-Cell': gap 8, pad 16/0). */
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
/* Title + full-width body group (figma 'Frame 2117131229': column, gap 8). */
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
/* Leading text column (figma 'Frame 2036083120': gap 4, pad-right 48). */
|
||||
.rowText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* Selector pill (figma 'Selector': h36 r18, fill #F5F6F7, pad 0/14, gap 12). */
|
||||
.selector {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* Tool Call mode cubes share an 8px gap. */
|
||||
.cubeRow {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Tool Call mode cube (figma '.Selector Cube' 418w r16; horizontal inset =
|
||||
* outer pad 4 + inner .Menu_cell pad 10, vertical = inner pad 8). */
|
||||
.modeCube {
|
||||
box-sizing: border-box;
|
||||
width: 418px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Selected cube: #F5F6F7 fill + #ADB2B8 border (static token — the bluish-400
|
||||
* step has no alias-layer name). */
|
||||
.selected {
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
border-color: var(--dsw-static-neutral-bluish-400);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* The General section (figma 501:29983 'Options'): Permission and Tool Call
|
||||
* skeleton rows, then the feature-contributed preference rows from the
|
||||
* `settings.general.item` slot (locale → Language, ui-theme → Appearance).
|
||||
* The section column stacks rows; each row draws its own internals and
|
||||
* separator.
|
||||
*/
|
||||
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import css from './GeneralSection.module.css'
|
||||
|
||||
/** Injected face of the General section: the settings-namespace translate. */
|
||||
export interface GeneralSectionInjected {
|
||||
/** Translate a `settings` dictionary key to the active-locale text. */
|
||||
t: (key: string) => string
|
||||
}
|
||||
|
||||
/** Full component props: section owner share + item render share + inject face. */
|
||||
export type GeneralSectionComponentProps =
|
||||
PropsRuntime<'settings.section'> & PropsRenderSlots<'settings.general.item'> & GeneralSectionInjected
|
||||
|
||||
/**
|
||||
* Render the General section content column.
|
||||
* @param props - composed slot props (contract/slots.ts).
|
||||
* @returns the section element tree.
|
||||
*/
|
||||
export function GeneralSection({ t, renderSlot }: GeneralSectionComponentProps) {
|
||||
return (
|
||||
<div className={css.section}>
|
||||
{/* Permission (skeleton): disabled selector pill. */}
|
||||
<div className={css.row}>
|
||||
<div className={css.rowText}>
|
||||
<div className={css.title}>{t('permission.title')}</div>
|
||||
<div className={css.desc}>{t('permission.desc')}</div>
|
||||
</div>
|
||||
<button type="button" className={css.selector} disabled>
|
||||
{t('permission.value')}
|
||||
<IconChevronDownOutline14 className={css.chevron} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */}
|
||||
<div className={css.group}>
|
||||
<div className={css.title}>{t('toolcall.title')}</div>
|
||||
<div className={css.cubeRow}>
|
||||
<div className={`${css.modeCube} ${css.selected}`}>
|
||||
<div className={css.title}>{t('toolcall.schema.title')}</div>
|
||||
<div className={css.desc}>{t('toolcall.schema.desc')}</div>
|
||||
</div>
|
||||
<div className={css.modeCube}>
|
||||
<div className={css.title}>{t('toolcall.code.title')}</div>
|
||||
<div className={css.desc}>{t('toolcall.code.desc')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature-owned preference rows (Language, Appearance, …). */}
|
||||
{renderSlot('settings.general.item', {})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Trigger row label (the shell's button provides layout/colors; the label
|
||||
* only guards against overflow during the sidebar collapse crossfade). */
|
||||
|
||||
.triggerLabel {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
56
packages/client/ui-settings-general/src/client/chrome.tsx
Normal file
56
packages/client/ui-settings-general/src/client/chrome.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Shell chrome content registered into the shell's trigger/header seats: the
|
||||
* trigger row icon + label (figma sidebar foot) and the panel title text.
|
||||
* The shell renders the surrounding chrome (button, nav heading row) and
|
||||
* reads each entry's `label` option for aria text.
|
||||
*/
|
||||
import { IconSettingsOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import css from './chrome.module.css'
|
||||
|
||||
/** Injected face of both chrome seats: the settings-namespace translate. */
|
||||
export interface ChromeInjected {
|
||||
/** Translate a `settings` dictionary key to the active-locale text. */
|
||||
t: (key: string) => string
|
||||
}
|
||||
|
||||
/** Trigger content props: the sidebar column state + translate. */
|
||||
export type TriggerContentProps = PropsRuntime<'settings.trigger'> & ChromeInjected
|
||||
|
||||
/** Header content props: translate only. */
|
||||
export type HeaderContentProps = PropsRuntime<'settings.header'> & ChromeInjected
|
||||
|
||||
/**
|
||||
* Render the trigger row content (icon; label only in the wide column).
|
||||
* @param props - composed slot props.
|
||||
* @returns the trigger content fragment.
|
||||
*/
|
||||
export function TriggerContent({ wide, t }: TriggerContentProps) {
|
||||
return (
|
||||
<>
|
||||
<IconSettingsOutline14 size={wide ? 14 : 18} />
|
||||
{wide && <span className={css.triggerLabel}>{t('trigger')}</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the panel title text.
|
||||
* @param props - composed slot props.
|
||||
* @returns the title text node.
|
||||
*/
|
||||
export function HeaderContent({ t }: HeaderContentProps) {
|
||||
return <>{t('title')}</>
|
||||
}
|
||||
|
||||
/** Close-button label text props: translate only. */
|
||||
export type CloseLabelProps = PropsRuntime<'settings.close'> & ChromeInjected
|
||||
|
||||
/**
|
||||
* Render the close button's visually-hidden label text.
|
||||
* @param props - composed slot props.
|
||||
* @returns the label text node.
|
||||
*/
|
||||
export function CloseLabel({ t }: CloseLabelProps) {
|
||||
return <>{t('close')}</>
|
||||
}
|
||||
87
packages/client/ui-settings-general/src/client/index.ts
Normal file
87
packages/client/ui-settings-general/src/client/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Settings ownerless-copy plugin, browser half: registers everything on the
|
||||
* Settings surface that belongs to no single feature — the trigger/header
|
||||
* chrome content, the General section (skeleton rows + the
|
||||
* `settings.general.item` slot declaration), and the `settings`
|
||||
* dictionaries. Feature-owned rows and sections stay with their features.
|
||||
* Export discipline: packages/client/AGENTS.md.
|
||||
*/
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: pulls the shell's SlotMap merges (trigger/header/section/item).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { ChromeInjected } from './chrome.tsx'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
|
||||
import type { GeneralSectionInjected } from './GeneralSection.tsx'
|
||||
import { GeneralSection } from './GeneralSection.tsx'
|
||||
import { en, zh } from './locales.ts'
|
||||
|
||||
export type {
|
||||
ChromeInjected, CloseLabelProps, HeaderContentProps, TriggerContentProps,
|
||||
} from './chrome.tsx'
|
||||
export type {
|
||||
GeneralSectionComponentProps, GeneralSectionInjected,
|
||||
} from './GeneralSection.tsx'
|
||||
|
||||
/** Dictionary namespace owned by this plugin (shell chrome + General copy). */
|
||||
const NS = 'settings'
|
||||
|
||||
/**
|
||||
* Required services (cordis fiber inject). The target slots are declared by
|
||||
* ui-settings' apply, whose activation order relative to this one is NOT
|
||||
* constrained; registration goes through declaration-aware deferral.
|
||||
*/
|
||||
export const inject = ['slots', 'locale']
|
||||
|
||||
/**
|
||||
* Register the `settings` dictionaries, the chrome content, and the General
|
||||
* section, each once its slot declaration is on the ledger.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => {
|
||||
const disposers = [
|
||||
ctx.locale.register(NS, 'zh', zh),
|
||||
ctx.locale.register(NS, 'en', en),
|
||||
]
|
||||
return () => { for (const dispose of disposers) dispose() }
|
||||
}, 'ui-settings-general: dictionaries')
|
||||
|
||||
const t = ctx.locale.bind(NS)
|
||||
const chromeInjected = (): ChromeInjected => ({ t })
|
||||
const generalInjected = (): GeneralSectionInjected => ({ t })
|
||||
|
||||
// All four seats refresh on locale change: re-registration bumps each
|
||||
// slot's ledger version, which re-renders the outlets through their own
|
||||
// subscriptions (outlet memoization would swallow a parent-only render).
|
||||
ctx.effect(() => {
|
||||
const trigger = deferRegistration(ctx.slots, 'settings.trigger', TriggerContent, () =>
|
||||
ctx.slots.register({ name: 'settings.trigger', inject: chromeInjected }, TriggerContent))
|
||||
const header = deferRegistration(ctx.slots, 'settings.header', HeaderContent, () =>
|
||||
ctx.slots.register({ name: 'settings.header', inject: chromeInjected }, HeaderContent))
|
||||
const close = deferRegistration(ctx.slots, 'settings.close', CloseLabel, () =>
|
||||
ctx.slots.register({ name: 'settings.close', inject: chromeInjected }, CloseLabel))
|
||||
const general = deferRegistration(ctx.slots, 'settings.section', GeneralSection, () =>
|
||||
ctx.slots.register({
|
||||
name: 'settings.section',
|
||||
id: 'general',
|
||||
order: 0,
|
||||
label: t('general.nav'),
|
||||
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
|
||||
inject: generalInjected,
|
||||
}, GeneralSection))
|
||||
const offLocale = ctx.on('locale/change', () => {
|
||||
trigger.refresh()
|
||||
header.refresh()
|
||||
close.refresh()
|
||||
general.refresh()
|
||||
})
|
||||
return () => {
|
||||
offLocale()
|
||||
trigger.dispose()
|
||||
header.dispose()
|
||||
close.dispose()
|
||||
general.dispose()
|
||||
}
|
||||
}, 'ui-settings-general: chrome and section registrations')
|
||||
}
|
||||
40
packages/client/ui-settings-general/src/client/locales.ts
Normal file
40
packages/client/ui-settings-general/src/client/locales.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* `settings` namespace dictionaries: shell chrome plus the shell-owned
|
||||
* General section (nav label, skeleton rows). Skeleton-row technical copy
|
||||
* (Read only / Schema mode / Code mode and their descriptions) is shared
|
||||
* verbatim across locales per the Figma design. Feature-owned rows
|
||||
* (Language, Appearance) ship their copy in their own packages.
|
||||
*/
|
||||
import type { LocaleDict } from '@deepseek-ai/dsh-client-locale/client'
|
||||
|
||||
const SHARED = {
|
||||
'permission.value': 'Read only',
|
||||
'toolcall.schema.title': 'Schema mode',
|
||||
'toolcall.schema.desc': 'Traditional function calling — invoke tools one at a time',
|
||||
'toolcall.code.title': 'Code mode',
|
||||
'toolcall.code.desc': 'Chain multiple tools with code — multi-step orchestration',
|
||||
} satisfies LocaleDict
|
||||
|
||||
/** Simplified Chinese dictionary. */
|
||||
export const zh: LocaleDict = {
|
||||
...SHARED,
|
||||
'trigger': '设置',
|
||||
'title': '设置',
|
||||
'close': '关闭',
|
||||
'general.nav': '通用设置',
|
||||
'permission.title': '权限',
|
||||
'permission.desc': '选择默认权限模式',
|
||||
'toolcall.title': '工具调用',
|
||||
}
|
||||
|
||||
/** English dictionary. */
|
||||
export const en: LocaleDict = {
|
||||
...SHARED,
|
||||
'trigger': 'Settings',
|
||||
'title': 'Settings',
|
||||
'close': 'Close',
|
||||
'general.nav': 'General',
|
||||
'permission.title': 'Permission',
|
||||
'permission.desc': 'Choose default permission mode',
|
||||
'toolcall.title': 'Tool Call',
|
||||
}
|
||||
6
packages/client/ui-settings-general/src/css-modules.d.ts
vendored
Normal file
6
packages/client/ui-settings-general/src/css-modules.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.css'
|
||||
4
packages/client/ui-settings-general/src/index.ts
Normal file
4
packages/client/ui-settings-general/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/** Host loader entry for the browser implementation exported from `./client`. */
|
||||
|
||||
/** Host plugin body — no host-side behavior for the general settings plugin. */
|
||||
export function apply(): void {}
|
||||
32
packages/client/ui-settings-general/src/invariant.ts
Normal file
32
packages/client/ui-settings-general/src/invariant.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-settings-general`.
|
||||
* @module @deepseek-ai/dsh-client-ui-settings-general/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-settings-general'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'client-ui-settings-general-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: a copy-owning registrant contributing chrome content
|
||||
* and the General section into shell-declared slots — it emits no cordis
|
||||
* events and owns no cross-plugin mutable relation; slot conflicts already
|
||||
* fail loud in the slot core at load time.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
Reference in New Issue
Block a user