/** * Settings shell root: the sidebar-foot trigger row plus the centered modal * panel (figma 501:29947, 1080x700) with the section nav rail. The shell is * a pure composition face — every piece of text (trigger label, panel title, * close label, sections) arrives from registrants through slots; accessible * names resolve to that content (trigger: its own text; dialog: * aria-labelledby the title node; close: visually-hidden slot text). Modal * open state and the active section id are component-local viewing state; * the onboarding coordinator mounts exactly one ordered registrant while the * sessions-derived empty-Hero fact is active. */ import { useCallback, useEffect, useId, useRef, useState } from 'react' import { createPortal } from 'react-dom' import clsx from 'clsx' import { IconCloseOutline16, IconDataOutline16, IconSettingsOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts' import css from './SettingsRoot.module.css' /** Nav glyph by section id; unknown ids fall back to the settings gear. */ function navIcon(id: string) { if (id === 'models') return return } type PanelProps = { rows: readonly SettingsSectionRow[] renderSlot: SettingsRootComponentProps['renderSlot'] activeId: string | undefined onSelect: (id: string) => void onClose: () => void } /** * The modal layer: full-viewport mask + centered panel. Close paths: the * header button, a mask click, and document-level Escape (mounted only while * open, so the listener lifetime is the panel's). */ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelProps) { // Entries can unmount underneath the requested id, so the render-time // projection falls back to the first row when the id is gone. const active = rows.find(r => r.id === activeId)?.id ?? rows[0]?.id const titleId = useId() useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('keydown', onKeyDown) } }, [onClose]) // Baseline focus management: entering the dialog lands on the close button. const closeButton = useRef(null) useEffect(() => { closeButton.current?.focus() }, []) return (
) } /** * Render the settings trigger and panel. * @param props - composed slot props (contract/slots.ts). * @returns the settings shell element tree. */ export function SettingsRoot(props: SettingsRootComponentProps) { const { wide, useSections, useOnboardingSteps, useSessions, renderSlot } = props const [open, setOpen] = useState(false) const [activeId, setActiveId] = useState(undefined) const [completedOnboarding, setCompletedOnboarding] = useState>(() => new Set()) const close = useCallback(() => { setOpen(false) setActiveId(undefined) }, []) const openSection = useCallback((id: string) => { setActiveId(id) setOpen(true) }, []) // The ledger tick keeps the nav rows fresh: registrants re-register with // freshly localized text on locale change, and the trigger/header/close // seats re-render through their own outlets' subscriptions. const rows = useSections(s => s) const onboardingSteps = useOnboardingSteps(s => s) const onboardingActive = useSessions(state => state.phase === 'ready' && (state.current === undefined || state.byId[state.current]?.blank === true)) const onboardingStep = onboardingActive ? onboardingSteps.find(step => !completedOnboarding.has(step.id)) : undefined useEffect(() => { if (onboardingActive) return setCompletedOnboarding(new Set()) }, [onboardingActive]) const completeOnboardingStep = useCallback((id: string) => { setCompletedOnboarding((previous) => { if (previous.has(id)) return previous return new Set([...previous, id]) }) }, []) useEffect(() => { if (onboardingStep === undefined) return const appRoot = document.getElementById('root') if (appRoot === null) return appRoot.inert = true return () => { appRoot.inert = false } }, [onboardingStep]) return ( <> {open && ( )} {onboardingStep !== undefined && createPortal((
), document.body)} ) }