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:
@@ -49,7 +49,7 @@
|
||||
.railIn .iconButton,
|
||||
.railIn .newSession,
|
||||
.railIn .searchButton,
|
||||
.railIn .foot {
|
||||
.railIn .footArea {
|
||||
animation: rail-in 150ms var(--ds-ease-in-out) 100ms backwards;
|
||||
}
|
||||
|
||||
@@ -372,45 +372,11 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Foot: settings entry (figma 133:7668, 49 hug): the former 18/10 vertical
|
||||
margins fold into the row so the hover pill spans the full 49px. */
|
||||
.foot {
|
||||
/* Foot seat: pure layout — the flex slot pinning the sidebar.settings slot
|
||||
content to the column bottom. Row visuals belong to the slot occupant
|
||||
(ui-settings). */
|
||||
.footArea {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 49px;
|
||||
margin: 8px 0 0; /* + 49px row + root padBottom 6 keeps the old 57px band */
|
||||
padding: 0 2px 0 6px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.foot:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
/* Rail settings: the same 36x36 circle box as the other rail controls. */
|
||||
.collapsed .foot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 18px 0 10px;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.footLabel {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collapsed .footLabel {
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -419,7 +385,7 @@
|
||||
.railIn .iconButton,
|
||||
.railIn .newSession,
|
||||
.railIn .searchButton,
|
||||
.railIn .foot {
|
||||
.railIn .footArea {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import clsx from 'clsx'
|
||||
import {
|
||||
BrandWordmark, FishLogo,
|
||||
IconCloseFill14, IconNewChatOutline16, IconPanelLeftOutline16, IconPersonalizationOutline16,
|
||||
IconProjectAddOutline16, IconSearchOutline16, IconSettingsOutline14,
|
||||
IconProjectAddOutline16, IconSearchOutline16,
|
||||
Menu, Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -314,9 +314,10 @@ export function SidebarRoot({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
|
||||
<IconSettingsOutline14 size={wide ? 14 : 18} />
|
||||
{wide && <span className={clsx(css.footLabel, css.wide)}>Settings</span>}
|
||||
{/* Foot seat: the flex slot pinning the settings entry to the column
|
||||
bottom; ui-settings occupies it with the trigger row + panel. */}
|
||||
<div className={css.footArea}>
|
||||
{renderSlot('sidebar.settings', { wide })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,9 +20,24 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* is claiming); ui-workspace registers the picker.
|
||||
*/
|
||||
'sidebar.workspace': { kind: 'single'; scope: 'root'; owner: SidebarWorkspaceOwnerProps }
|
||||
/**
|
||||
* The settings seat at the sidebar foot. Declared by this package's
|
||||
* 'sidebar' entry; ui-settings registers its trigger row + modal panel.
|
||||
* The sidebar passes only its column state — it holds no settings state.
|
||||
*/
|
||||
'sidebar.settings': { kind: 'single'; scope: 'root'; owner: SidebarSettingsOwnerProps }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner share of the sidebar settings seat: the column display state the
|
||||
* occupant's trigger row must render against (wide row vs rail icon).
|
||||
*/
|
||||
export interface SidebarSettingsOwnerProps {
|
||||
/** Whether the sidebar renders wide content (false = 56px rail). */
|
||||
wide: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner share of the sidebar workspace hole: popover geometry plus the
|
||||
* sidebar's pick semantics. The picked Host Workspace is already real; the
|
||||
@@ -62,8 +77,9 @@ export type SidebarRootInjected = {
|
||||
|
||||
/**
|
||||
* Full component props: layout owner state/actions plus global useSessions
|
||||
* and useWorkspaces, the declared Workspace picker render share, and this
|
||||
* package's injected callback. No store is registered.
|
||||
* and useWorkspaces, the declared child-slot render shares (Workspace picker
|
||||
* and settings seat), and this package's injected callback. No store is
|
||||
* registered.
|
||||
*/
|
||||
export type SidebarRootComponentProps =
|
||||
PropsRuntime<'sidebar'> & PropsRenderSlots<'sidebar.workspace'> & SidebarRootInjected
|
||||
PropsRuntime<'sidebar'> & PropsRenderSlots<'sidebar.workspace' | 'sidebar.settings'> & SidebarRootInjected
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SidebarRootInjected } from './contract/slots.ts'
|
||||
import { SidebarRoot } from './SidebarRoot.tsx'
|
||||
|
||||
export type { SidebarRootComponentProps, SidebarRootInjected, SidebarWorkspaceOwnerProps } from './contract/slots.ts'
|
||||
export type { SidebarRootComponentProps, SidebarRootInjected, SidebarSettingsOwnerProps, SidebarWorkspaceOwnerProps } from './contract/slots.ts'
|
||||
|
||||
/** Services required by the sidebar plugin. */
|
||||
export const inject = ['slots', 'layout', 'sessions', 'workspaces']
|
||||
@@ -20,9 +20,12 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(
|
||||
() => ctx.slots.register({
|
||||
name: 'sidebar',
|
||||
// SidebarRoot owns this picker site; ui-workspace registers the shared
|
||||
// picker that selects a Host Workspace for a frontend Session Intent.
|
||||
children: { 'sidebar.workspace': { kind: 'single', scope: 'root' } },
|
||||
// SidebarRoot owns these sites; ui-workspace registers the shared
|
||||
// picker, ui-settings registers the settings trigger + panel.
|
||||
children: {
|
||||
'sidebar.workspace': { kind: 'single', scope: 'root' },
|
||||
'sidebar.settings': { kind: 'single', scope: 'root' },
|
||||
},
|
||||
inject: injectProps,
|
||||
}, SidebarRoot),
|
||||
'ui-sidebar: slot registration',
|
||||
|
||||
Reference in New Issue
Block a user