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).
149 lines
5.8 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
}
|