Files
dsh-context/src/client/components/settingsCard.tsx
T
Bowen Liang 17183882be refactor(client): move pure pseudo-state styling to Tailwind state variants
The 20 hover/focus rules that are leaf styles (no state class re-asserts
them) become hover:/focus:/group-hover: variants on the elements' own
class lists, and their sheet rules are deleted: row and button hover
tints, retry/close/gate brightness, input focus borders, underline
hints, the three styled-tip reveals (named group/tip), the plugin-info
row underlines (group/pi), and the agent halo's hover/focus wash
(group/agent — the React hover state keeps driving the inspector;
lc-agent-hover stays as a test anchor).

The hover/lit(-on) cascade pyramids stay hand-written in the sheets —
the gran/rich-seg/rich-copy/fa-chip toggles and the settings card's
open state need ordered overrides over their own hover rules, several
of those state classes double as test anchors and DOM query hooks, and
moving them would mean a chip factory for zero visual change.
Transitions, animations and all harness --dsw-* tokens stay in sheets.

Behavior notes: variant utilities carry (0,2,0) specificity, so they
keep beating the sheet base rules exactly as the old :hover rules did;
Tailwind media-guards hover behind (hover: hover), so touch devices no
longer get sticky hover styling (dsh web is desktop-first).
2026-09-13 09:41:30 +08:00

149 lines
5.8 KiB
TypeScript

/**
* The dsh-context card in Settings → Plugins → Plugin configuration, registered on the framework's `settings.plugin.item` slot keyed on
* the
* Host-served `dsh-context` settings namespace — the section itself supplies nothing; it renders nothing while the namespace is
* unavailable
* (a deployment without the Host half, or a remote browser, shows no trace).
* Mounts expanded when the Plugin Info card's "Open in Settings" jump left a
* fresh expand request (settingsJump.ts), scrolling itself into view.
*/
import { useEffect, useRef, useState, type ReactElement } from 'react'
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import { consumeCardExpand } from '../settingsJump'
import type { SettingsField, SettingsState } from '../settings'
import type { ViewKit } from '../viewkit'
export interface SettingsCardProps {
useContextSettings?: <T>(selector: (state: SettingsState) => T) => T
set?: (field: SettingsField, value: string) => void
}
interface PrefRowProps {
label: string
value: string
options: ReadonlyArray<{ id: string; label: string }>
disabled: boolean
onPick: (id: string) => void
}
function PrefRow(props: PrefRowProps): ReactElement {
const [open, setOpen] = useState(false)
const active = props.options.find(o => o.id === props.value)?.label ?? props.value
return (
<div className="lc-settings-row">
<span className="lc-settings-label">{props.label}</span>
<Menu
open={open}
onClose={() => { setOpen(false) }}
items={props.options}
selectedId={props.value}
onSelect={(id) => { setOpen(false); props.onPick(id) }}
align="end"
portal
anchor={(
<button
type="button"
className="lc-settings-select hover:enabled:bg-(--dsw-alias-interactive-bg-hover) disabled:opacity-50 disabled:cursor-default"
disabled={props.disabled}
aria-haspopup="menu"
aria-expanded={open}
onClick={() => { setOpen(v => !v) }}
>
{active}
<IconChevronDownOutline14 />
</button>
)}
/>
</div>
)
}
export function makeSettingsCard(kit: ViewKit): (props: SettingsCardProps) => ReactElement | null {
const { t } = kit
return function SettingsCard(props: SettingsCardProps): ReactElement | null {
const [open, setOpen] = useState(false)
const itemRef = useRef<HTMLLIElement | null>(null)
// "Open in Settings" jump: consume its fresh expand request once on mount
// and land open; every guard stays local so no host quirk can surface.
useEffect(() => {
if (!consumeCardExpand()) return
setOpen(true)
try {
itemRef.current?.scrollIntoView({ block: 'nearest' })
} catch { /* hosts without scrollIntoView: expanded but unscrolled */ }
}, [])
const state = typeof props.useContextSettings === 'function' ? props.useContextSettings(s => s) : undefined
if (state === undefined || state.status === 'unavailable') return null
const disabled = state.status !== 'ready' || !state.writable
return (
<li ref={itemRef} className={'lc-settings-card' + (open ? ' lc-settings-open' : '')}>
<button
type="button"
className="lc-settings-head"
aria-expanded={open}
aria-label={`${t(open ? 'settings.collapse' : 'settings.expand')}: ${t('settings.title')}`}
onClick={() => { setOpen(!open) }}
>
<span className="lc-settings-headtext">
<span className="lc-settings-name">{t('settings.title')}</span>
<span className="lc-settings-desc">{t('settings.desc')}</span>
</span>
<IconChevronDownOutline14 className="lc-settings-chevron" />
</button>
{open
? (
<div className="lc-settings-body">
{!state.writable && state.status === 'ready'
? <p className="lc-settings-note" role="status">{t('settings.readOnly')}</p>
: null}
<PrefRow
label={t('settings.placement')}
value={state.placement}
disabled={disabled}
options={[
{ id: 'all', label: t('placement.all') },
{ id: 'tab', label: t('placement.tab') },
{ id: 'sidebar', label: t('placement.sidebar') },
]}
onPick={(id) => { props.set?.('defaultPlacement', id) }}
/>
<PrefRow
label={t('settings.gran')}
value={state.granularity}
disabled={disabled}
options={[
{ id: 'step', label: t('gran.step') },
{ id: 'turn', label: t('gran.turn') },
]}
onPick={(id) => { props.set?.('defaultGranularity', id) }}
/>
<PrefRow
label={t('settings.mode')}
value={state.mode}
disabled={disabled}
options={[
{ id: 'total', label: t('gran.total') },
{ id: 'delta', label: t('gran.delta') },
]}
onPick={(id) => { props.set?.('defaultTrendMode', id) }}
/>
<PrefRow
label={t('settings.fileSort')}
value={state.fileSort}
disabled={disabled}
options={[
{ id: 'count', label: t('files.sort.count') },
{ id: 'latest', label: t('files.sort.latest') },
{ id: 'path', label: t('files.sort.path') },
]}
onPick={(id) => { props.set?.('defaultFileSort', id) }}
/>
</div>
)
: null}
</li>
)
}
}