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).
This commit is contained in:
Bowen Liang committed 2026-09-13 09:41:30 +08:00
1 parent 75fada4fb3
commit 17183882be
29 files changed
+57 -71

No files matched your search

+7 -1
View File
@@ -249,6 +249,9 @@ function AgentNodeView(props: NodeViewProps): ReactElement {
+ (node.completed && !node.running ? ' lc-agent-done' : '')
+ (props.hovered ? ' lc-agent-hover' : '')
+ (node.isCurrent ? '' : ' lc-agent-clickable')
// The halo's hover/focus wash rides group variants on the node (the React
// hover state only drives the inspector; lc-agent-hover stays as a test anchor).
+ ' group/agent'
return (
<g
className={cls}
@@ -262,7 +265,10 @@ function AgentNodeView(props: NodeViewProps): ReactElement {
onMouseLeave={() => { props.onHover(null) }}
>
{/* Halo carries the state: wash for self, breathing green while running, faint green for done. */}
<circle className="lc-agent-halo" r={AGENT_NODE_R + 9} />
<circle
className="lc-agent-halo group-hover/agent:fill-[var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))] group-focus-visible/agent:fill-[var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))]"
r={AGENT_NODE_R + 9}
/>
<circle className="lc-agent-track" r={AGENT_NODE_R} />
{segs.map(seg => (
<circle
+4 -4
View File
@@ -249,7 +249,7 @@ function RowToolbar(props: {
return (
<div className="lc-br-toolctl">
<input
className="lc-br-tool-search"
className="lc-br-tool-search focus:border-(--dsw-alias-label-dimmed)"
value={props.value}
placeholder={props.placeholder}
onChange={(ev: ChangeEvent<HTMLInputElement>) => { props.onChange(ev.target.value) }}
@@ -321,7 +321,7 @@ function ToolSchema(props: {
<div className="lc-ts-json">
<button
type="button"
className="lc-ts-json-toggle"
className="lc-ts-json-toggle hover:text-(--dsw-alias-label-primary) hover:underline"
onClick={() => { setJsonOpen(o => !o) }}
>{(jsonOpen ? '▾ ' : '▸ ') + (jsonOpen ? props.labels.hide : props.labels.show)}</button>
{jsonOpen ? <pre className="lc-ts-desc-body lc-br-dim">{schemaJson}</pre> : null}
@@ -820,7 +820,7 @@ export function makeContextBrowser(
const open = openElem === key
return (
<div key={key} className={'lc-br-elem' + (open ? ' lc-br-elem-on' : '')}>
<button type="button" className="lc-br-elem-row" onClick={() => { toggleElem(key) }}>
<button type="button" className="lc-br-elem-row hover:bg-(--dsw-alias-interactive-bg-hover)" onClick={() => { toggleElem(key) }}>
<span className={'lc-br-chev' + (open ? ' lc-br-chev-on' : '')} />
{err ? <span className="lc-br-err-dot" title={t('node.failed')} /> : null}
{tag !== null ? <span className="lc-br-tag">{tag}</span> : null}
@@ -1115,7 +1115,7 @@ export function makeContextBrowser(
<div key={c.key} className={'lc-br-cat' + (openable ? '' : ' lc-br-cat-empty')}>
<button
type="button"
className={'lc-br-cat-row' + (linked && props.hoverKey === c.key ? ' lc-br-cat-on' : '')}
className={'lc-br-cat-row hover:bg-(--dsw-alias-interactive-bg-hover)' + (linked && props.hoverKey === c.key ? ' lc-br-cat-on' : '')}
/* v8 ignore start -- the handlers exist only when linked,
and linked already requires onHoverKey defined (above). */
onMouseEnter={linked ? () => { if (props.onHoverKey !== undefined) props.onHoverKey(c.key) } : undefined}
+1 -1
View File
@@ -67,7 +67,7 @@ export function makeContextJumpButton(kit: ViewKit): (props: ContextJumpProps) =
}
return (
<Tooltip label={t('jump.title')} side="bottom">
<button type="button" className="lc-jump" aria-label={t('jump.title')} onClick={jump}>
<button type="button" className="lc-jump hover:bg-(--dsw-alias-interactive-bg-hover) hover:text-(--dsw-alias-label-secondary)" aria-label={t('jump.title')} onClick={jump}>
<JumpIcon />
</button>
</Tooltip>
+1 -1
View File
@@ -116,7 +116,7 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
<div className="lc-modal-card" onClick={(ev) => { ev.stopPropagation() }}>
<div className="lc-modal-head">
<span className="lc-modal-title">{t('tab')}</span>
<button className="lc-modal-close" aria-label={t('cmd.close')} onClick={close}>×</button>
<button className="lc-modal-close hover:text-(--dsw-alias-label-primary) hover:bg-(--dsw-alias-bg-layer-2)" aria-label={t('cmd.close')} onClick={close}>×</button>
</div>
{data === null || head === null ? (
+1 -1
View File
@@ -30,7 +30,7 @@ export function makeDetailNote(kit: ViewKit): (props: {
return (
<div className={cls}>
{props.onRetry !== undefined
? <button type="button" className="lc-br-retry" onClick={props.onRetry}>{t('detail.loadFailed')}</button>
? <button type="button" className="lc-br-retry hover:brightness-[1.15]" onClick={props.onRetry}>{t('detail.loadFailed')}</button>
: t('detail.loadFailed')}
</div>
)
+1 -1
View File
@@ -29,7 +29,7 @@ export function makeErrorBoundary(t: Translate): ComponentType<{ children?: Reac
<code className="lc-error-msg">{error.message}</code>
<button
type="button"
className="lc-error-retry"
className="lc-error-retry hover:border-(--dsw-alias-label-primary)"
onClick={() => { this.setState({ error: null }) }}
>{t('error.retry')}</button>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@ export function fetchMissNote(
if (state === 'absent') return t('browser.notInLog')
if (state === 'failed') {
return (
<button type="button" className="lc-br-retry" onClick={onRetry}>
<button type="button" className="lc-br-retry hover:brightness-[1.15]" onClick={onRetry}>
{t('browser.loadFailed')}
</button>
)
+6 -6
View File
@@ -173,7 +173,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
))}
</div>
<input
className="lc-fa-search"
className="lc-fa-search focus:border-(--dsw-alias-label-dimmed)"
value={query}
placeholder={t('files.search')}
onChange={(ev: ChangeEvent<HTMLInputElement>) => { setQuery(ev.target.value) }}
@@ -183,9 +183,9 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
<span>{t('files.files', { n: activity.entries.length })}</span>
{activity.totals.added + activity.totals.removed > 0 ? (
/* The one styled tip of the card: it lives OUTSIDE the scrolling list, so the bubble never clips. */
<span className="lc-fa-meta-delta">
<span className="lc-fa-meta-delta group/tip">
<DeltaPair added={activity.totals.added} removed={activity.totals.removed} />
<span className="lc-tip lc-fa-meta-tip" role="tooltip">{t('files.deltaTip')}</span>
<span className="lc-tip lc-fa-meta-tip group-hover/tip:opacity-100" role="tooltip">{t('files.deltaTip')}</span>
</span>
) : null}
<span className="lc-gran lc-fa-sort" role="group" title={t('files.sortTip')}>
@@ -225,7 +225,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
<div key={e.path} className={'lc-fa-item' + (open ? ' lc-fa-item-on' : '')}>
<button
type="button"
className="lc-fa-row"
className="lc-fa-row hover:bg-(--dsw-alias-interactive-bg-hover)"
title={e.path}
onClick={() => { setOpenPath(open ? null : e.path) }}
>
@@ -244,7 +244,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
{openable
? (
<b
className="lc-fa-file"
className="lc-fa-file hover:underline"
title={t(previewable ? 'files.preview' : 'files.open')}
onClick={(ev: MouseEvent) => {
ev.stopPropagation()
@@ -284,7 +284,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
<button
key={key}
type="button"
className="lc-fa-op lc-fa-op-link"
className="lc-fa-op lc-fa-op-link hover:bg-(--dsw-alias-interactive-bg-hover)"
title={t('files.locate')}
onClick={() => { onLocate(op) }}
>
+1 -1
View File
@@ -143,7 +143,7 @@ export function makeImageCard(kit: ViewKit): ImageKit['Card'] {
<>
<button
type="button"
className="lc-att-item"
className="lc-att-item hover:border-(--dsw-alias-label-dimmed) hover:bg-[var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))]"
title={error ? t('attach.loadFailed') : t('attach.open')}
onClick={activate}
>
+5 -5
View File
@@ -14,9 +14,9 @@ export function makePluginInfo(kit: ViewKit): () => ReactElement {
// `title` carries the untruncated value: at narrow card widths the row's
// ellipsis can cut the repo or name short, and the hover text recovers it.
const row = (label: string, value: ReactNode, href: string, hint: string) => (
<a className="lc-pi-row" href={href} target="_blank" rel="noreferrer">
<a className="lc-pi-row group/pi" href={href} target="_blank" rel="noreferrer">
<div className="lc-pi-label">{label}</div>
<div className="lc-pi-value" title={hint}>{value}</div>
<div className="lc-pi-value group-hover/pi:underline" title={hint}>{value}</div>
</a>
)
return function PluginInfo(): ReactElement {
@@ -38,7 +38,7 @@ export function makePluginInfo(kit: ViewKit): () => ReactElement {
<div className="lc-card-title">
<span className="lc-card-title-text">{t('plugin.title')}</span>
{/* The tagline doubles as the repo link: hover underlines it, a click opens GitHub. */}
<a className="lc-card-sub lc-pi-hint" href={PLUGIN_REPO} target="_blank" rel="noreferrer">
<a className="lc-card-sub lc-pi-hint hover:underline" href={PLUGIN_REPO} target="_blank" rel="noreferrer">
{t('plugin.hint')}
</a>
</div>
@@ -46,9 +46,9 @@ export function makePluginInfo(kit: ViewKit): () => ReactElement {
{row(t('plugin.name'), nameValue, PLUGIN_REPO + '/releases', update !== null ? nameText + ' ↑ v' + update : nameText)}
{row(t('plugin.github'), PLUGIN_REPO_SHORT, PLUGIN_REPO, PLUGIN_REPO_SHORT)}
{/* Best-effort jump to this plugin's settings page — openPluginSettings silently no-ops when the host's chrome doesn't match. */}
<button type="button" className="lc-pi-row lc-pi-row-btn" onClick={() => { openPluginSettings() }}>
<button type="button" className="lc-pi-row lc-pi-row-btn group/pi" onClick={() => { openPluginSettings() }}>
<div className="lc-pi-label">{t('plugin.settings')}</div>
<div className="lc-pi-value" title={t('plugin.settingsOpen')}>{t('plugin.settingsOpen')}</div>
<div className="lc-pi-value group-hover/pi:underline" title={t('plugin.settingsOpen')}>{t('plugin.settingsOpen')}</div>
</button>
</div>
</div>
+4 -4
View File
@@ -53,9 +53,9 @@ export function makeRequestDetail(
}): ReactElement {
const inner = (
<>
<span className="lc-brief-tag">
<span className="lc-brief-tag group/tip">
{props.tag}
<span className="lc-tip lc-brief-tip" role="tooltip">{props.tagTip}</span>
<span className="lc-tip lc-brief-tip group-hover/tip:opacity-100" role="tooltip">{props.tagTip}</span>
</span>
{props.children}
</>
@@ -65,7 +65,7 @@ export function makeRequestDetail(
const onLocate = props.onLocate
const locate = (): void => { onLocate(node, props.isResponse) }
return (
<button type="button" className="lc-brief-row lc-brief-row-link" onClick={locate}>
<button type="button" className="lc-brief-row lc-brief-row-link hover:bg-(--dsw-alias-interactive-bg-hover)" onClick={locate}>
{inner}
</button>
)
@@ -160,7 +160,7 @@ export function makeRequestDetail(
return (
<span
key={n.seq}
className={'lc-brief-chip' + (grow ? ' lc-brief-chip-grow' : '') + (onClick !== undefined ? ' lc-brief-chip-link' : '')}
className={'lc-brief-chip' + (grow ? ' lc-brief-chip-grow' : '') + (onClick !== undefined ? ' lc-brief-chip-link hover:bg-(--dsw-alias-interactive-bg-hover)' : '')}
title={factTitle(tag, text) + hint}
onClick={onClick}
>
+1 -1
View File
@@ -44,7 +44,7 @@ function PrefRow(props: PrefRowProps): ReactElement {
anchor={(
<button
type="button"
className="lc-settings-select"
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}
+3 -3
View File
@@ -161,13 +161,13 @@ export function makeStatsContext(kit: ViewKit): (props: {
1,
)
const cell = (label: string, value: string | number, tip?: ReactNode): ReactElement => (
<div className={'lc-stat' + (tip === undefined ? '' : ' lc-stat-tipped')}>
<div className={'lc-stat' + (tip === undefined ? '' : ' lc-stat-tipped group/tip')}>
<span className="lc-stat-label">
{label}
{tip !== undefined && <i className="lc-stat-q" aria-hidden="true">?</i>}
{tip !== undefined && <i className="lc-stat-q group-hover/tip:text-(--dsw-alias-label-primary) group-hover/tip:border-(--dsw-alias-label-primary)" aria-hidden="true">?</i>}
</span>
<b className="lc-stat-value">{typeof value === 'number' ? fmt(value) : value}</b>
{tip !== undefined && <span className="lc-tip lc-stat-tip" role="tooltip">{tip}</span>}
{tip !== undefined && <span className="lc-tip lc-stat-tip group-hover/tip:opacity-100" role="tooltip">{tip}</span>}
</div>
)
return (
+1 -1
View File
@@ -204,7 +204,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactE
const enterStyle = { '--lc-i': Math.min(props.enterIndex, STAGGER_CAP) } as CSSProperties
return (
<div
className={'lc-bar'
className={'lc-bar hover:bg-(--dsw-alias-bg-layer-2)'
+ (props.selected ? ' lc-bar-selected' : '')
+ (props.hovered ? ' lc-bar-hovered' : '')
+ (props.inTurn ? ' lc-bar-in-turn' : '')}
+2 -2
View File
@@ -49,7 +49,7 @@ export function makeUpgradeGate(kit: ViewKit): (props: UpgradeGateProps) => Reac
<div className="lc-modal-card lc-gate-card" onClick={(ev) => { ev.stopPropagation() }}>
<div className="lc-modal-head">
<span className="lc-modal-title">{t('gate.title')}</span>
<button className="lc-modal-close" aria-label={t('cmd.close')} onClick={close}>×</button>
<button className="lc-modal-close hover:text-(--dsw-alias-label-primary) hover:bg-(--dsw-alias-bg-layer-2)" aria-label={t('cmd.close')} onClick={close}>×</button>
</div>
<div className="lc-gate-body">{t('gate.body', { minimum: props.minimum })}</div>
<div className="lc-gate-versions">
@@ -66,7 +66,7 @@ export function makeUpgradeGate(kit: ViewKit): (props: UpgradeGateProps) => Reac
</span>
</div>
<div className="lc-gate-actions">
<button type="button" className="lc-gate-ok" onClick={close}>{t('gate.ok')}</button>
<button type="button" className="lc-gate-ok hover:brightness-110" onClick={close}>{t('gate.ok')}</button>
</div>
</div>
</div>
+2 -2
View File
@@ -16,9 +16,9 @@
@keyframes lc-agent-flow { to { stroke-dashoffset: -9; } }
.lc-agent-node { outline: none; }
.lc-agent-clickable { cursor: pointer; }
/* The halo carries node state: hover wash, brand tint for self, breathing green while running, a faint green rest for done. */
/* The halo carries node state: the hover/focus wash rides `group-hover/agent`/`group-focus-visible/agent`
variants in agentGraph.tsx, brand tint for self, breathing green while running, a faint green rest for done. */
.lc-agent-halo { fill: transparent; transition: fill var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-agent-hover .lc-agent-halo, .lc-agent-node:focus-visible .lc-agent-halo { fill: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); }
.lc-agent-self .lc-agent-halo { fill: var(--dsw-alias-bg-layer-2); }
.lc-agent-running .lc-agent-halo { fill: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); animation: lc-agent-glow 1.8s ease-in-out infinite; }
.lc-agent-done .lc-agent-halo { fill: color-mix(in srgb, var(--dsw-alias-state-success-primary) 7%, transparent); }
-1
View File
@@ -1,7 +1,6 @@
/* Attachment grids and the image lightbox (browser.tsx inline grid + images.tsx lightbox). */
.lc-att-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 8px 10px; }
.lc-att-item { display: flex; gap: 8px; align-items: center; min-width: 0; width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: transparent; font: inherit; text-align: left; cursor: pointer; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-att-item:hover { border-color: var(--dsw-alias-label-dimmed); background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); }
.lc-att-thumb { width: 64px; height: 64px; flex: none; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); }
.lc-att-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lc-att-ph, .lc-att-err { font-size: 10px; color: var(--dsw-alias-label-secondary); text-align: center; padding: 2px; }
+4 -4
View File
@@ -78,9 +78,10 @@
.lc-gran-on .lc-kind-n { opacity: 0.85; }
/*
Shared tooltip-bubble chrome: pair `.lc-tip` with the owner's own tip class,
then position locally (top/bottom anchor, width cap, whitespace, reveal rule).
The component sheets carry the four shippers (.lc-stat-tip / .lc-brief-tip /
.lc-fa-meta-tip / .lc-bar-tip); a new tip never re-declares this chrome.
then position locally (top/bottom anchor, width cap, whitespace); the reveal
rides a `group-hover/tip` variant on the owner. The component sheets carry
the four shippers (.lc-stat-tip / .lc-brief-tip / .lc-fa-meta-tip / .lc-bar-tip);
a new tip never re-declares this chrome.
*/
.lc-tip { position: absolute; z-index: 6; width: max-content; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 6px 10px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-shadow-lv3, 0 2px 8px rgba(0, 0, 0, 0.18)); pointer-events: none; opacity: 0; transition: opacity var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-events, .lc-fa-list { display: flex; flex-direction: column; gap: 2px; height: 320px; overflow-y: auto; }
@@ -127,7 +128,6 @@
.lc-error { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 40px 16px; }
.lc-error-msg { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 4px 8px; max-width: 100%; overflow-wrap: anywhere; }
.lc-error-retry { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 4px 14px; border-radius: 6px; cursor: pointer; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-error-retry:hover { border-color: var(--dsw-alias-label-primary); }
.lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }
[data-conversation-scroll]:has(.lc-root) > [data-composer-seat]:not(:has([data-approval-key], [data-question-key], [data-plan-review-key])) { display: none; }
/*
-3
View File
@@ -14,12 +14,10 @@
.lc-br-meta .lc-est { margin-left: auto; }
.lc-br-note { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
.lc-br-retry { border: 0; background: transparent; padding: 0; color: var(--dsw-alias-brand-primary); font: inherit; font-size: 12px; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; transition: filter var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-br-retry:hover { filter: brightness(1.15); }
.lc-br-cats { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
.lc-br-cat { border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
.lc-br-cat-empty { opacity: 0.55; }
.lc-br-cat-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; padding: 7px 10px; cursor: pointer; text-align: left; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-br-cat-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
/*
Current-composition hover echo: the category row lit by the shared hover link looks hovered — same tint as a physical hover, plus a ring on its color dot.
*/
@@ -48,7 +46,6 @@
.lc-br-elem { border-radius: 6px; }
.lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }
.lc-br-elem-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 5px 6px; cursor: pointer; text-align: left; border-radius: 6px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
/* Failed tool result: the chat's red run-state dot (6px core on a 10px
halo) leads the collapsed row, so the failure scans without expanding. */
.lc-br-err-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-error-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
-1
View File
@@ -48,7 +48,6 @@
.lc-ts-arg-val { color: var(--dsw-alias-label-primary); font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; scrollbar-width: thin; }
.lc-ts-json { display: flex; flex-direction: column; gap: 4px; }
.lc-ts-json-toggle { align-self: flex-start; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; padding: 0; cursor: pointer; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-ts-json-toggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
.lc-rich-seg { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; padding: 1px; }
.lc-rich-seg-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-rich-seg-btn:hover { color: var(--dsw-alias-label-primary); }
-5
View File
@@ -8,19 +8,16 @@
.lc-fa-ctl .lc-gran { margin-left: 0; }
.lc-fa-n { margin-left: 4px; font-weight: 700; opacity: 0.75; }
.lc-fa-search, .lc-br-tool-search { flex: 1; min-width: 80px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; padding: 3px 8px; outline: none; }
.lc-fa-search:focus, .lc-br-tool-search:focus { border-color: var(--dsw-alias-label-dimmed); }
.lc-fa-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 6px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
/* The sort toggle rides the meta strip's right edge — the shared .lc-gran's own auto margin does the push. */
.lc-fa-meta .lc-fa-sort { flex: none; }
/* The aggregate delta pill carries the card's one styled tip (the shared `.lc-tip` chrome) — it sits outside the scrolling list, so the bubble never clips. */
.lc-fa-meta-delta { position: relative; }
.lc-fa-meta-tip { top: calc(100% + 6px); left: 0; max-width: 260px; white-space: normal; line-height: 1.5; }
.lc-fa-meta-delta:hover .lc-fa-meta-tip { opacity: 1; }
/* An expanded file reads as one contained card: layer-2 fill inside a hairline frame. */
.lc-fa-item { border: 1px solid transparent; border-radius: 8px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-fa-item-on { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
.lc-fa-row { display: flex; align-items: center; gap: 7px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 6px 8px; cursor: pointer; text-align: left; border-radius: 7px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-fa-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
.lc-fa-form { flex: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; font-size: 11px; opacity: 0.85; }
.lc-fa-lang { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 13px; padding: 0 2px; border-radius: 4px; font-size: 8px; font-weight: 700; line-height: 1; }
/* The chevron sits close to the icon: only half the row gap stays visible. */
@@ -42,7 +39,6 @@
.lc-fa-ctl .lc-gran { flex-wrap: wrap; }
}
.lc-fa-file { cursor: pointer; }
.lc-fa-file:hover { text-decoration: underline; }
.lc-fa-path em { font-style: normal; color: var(--dsw-alias-label-secondary); }
/* Per-purpose count badges: pill chips, one fixed tint per purpose (read = brand, write = success, search = warn). */
.lc-fa-badge { flex: none; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; padding: 1px 7px; border-radius: 999px; white-space: nowrap; }
@@ -65,7 +61,6 @@
.lc-fa-ops { margin: 0 8px 8px 15px; padding: 2px 0 2px 10px; border-left: 2px solid var(--dsw-alias-border-l1); display: flex; flex-direction: column; gap: 1px; }
.lc-fa-op { display: flex; align-items: center; gap: 8px; min-width: 0; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 11px; padding: 3px 6px; text-align: left; border-radius: 5px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-fa-op-link { cursor: pointer; }
.lc-fa-op-link:hover { background: var(--dsw-alias-interactive-bg-hover); }
.lc-fa-op-time { flex: none; margin-left: auto; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
.lc-fa-op-tool { flex: none; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; padding: 0 6px; border-radius: 999px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); }
.lc-fa-op-detail { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary); }
-1
View File
@@ -2,4 +2,3 @@
/* The chat action-row jump button — mirrors the harness MessageIconActions `.action` chrome so it sits flush beside the shipped icons. */
.lc-jump { display: inline-flex; align-items: center; justify-content: center; width: calc(28px + var(--dsh-content-font-delta, 0px)); height: calc(28px + var(--dsh-content-font-delta, 0px)); padding: 6px; border: 0; border-radius: 28px; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-jump svg { width: calc(16px + var(--dsh-content-font-delta, 0px)); height: calc(16px + var(--dsh-content-font-delta, 0px)); }
.lc-jump:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
-2
View File
@@ -6,7 +6,6 @@
.lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
.lc-modal-title { font-weight: 600; font-size: 14px; }
.lc-modal-close { margin-left: auto; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer; font-family: inherit; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }
/* ---- the baseline-gate dialog (upgradeGate.tsx): narrower card, version rows, one primary action ---- */
.lc-gate-card { width: min(440px, calc(100vw - 48px)); }
@@ -19,4 +18,3 @@
.lc-gate-or-newer { margin-left: 6px; font-family: inherit; font-weight: 400; font-size: 11px; color: var(--dsw-alias-label-secondary); }
.lc-gate-actions { display: flex; justify-content: flex-end; margin-top: 16px; }
.lc-gate-ok { border: 0; border-radius: 6px; padding: 6px 16px; font-size: 12px; font-family: inherit; cursor: pointer; background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); transition: filter var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-gate-ok:hover { filter: brightness(1.1); }
+1 -4
View File
@@ -24,12 +24,10 @@
.lc-brief-row { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 3px 6px; margin-left: -6px; margin-right: -6px; border-radius: 6px; border: 0; background: transparent; font-family: inherit; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-primary); text-align: left; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
/* The link class marks a hoverable ROW (buttons and the chip-carrying inputs row alike); the pointer cursor belongs only to the
actual click targets — button rows and individual chips. */
.lc-brief-row-link:hover { background: var(--dsw-alias-interactive-bg-hover); }
button.lc-brief-row-link { cursor: pointer; }
.lc-brief-tag { flex: none; box-sizing: border-box; min-width: 56px; text-align: center; font-size: 11px; padding: 0 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); position: relative; }
/* The tag's explanation bubble — the shared `.lc-tip` chrome: instant styled reveal on tag hover, never intercepts the pointer. */
/* The tag's explanation bubble — the shared `.lc-tip` chrome: instant styled reveal on tag hover (a `group-hover/tip` variant), never intercepts the pointer. */
.lc-brief-tip { bottom: calc(100% + 6px); left: 0; max-width: 240px; white-space: normal; text-align: left; line-height: 1.5; font-weight: 400; }
.lc-brief-tag:hover .lc-brief-tip { opacity: 1; }
.lc-brief-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The empty-state In row's placeholder: same anatomy as the preview text, muted. */
.lc-brief-empty { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary); }
@@ -42,7 +40,6 @@ button.lc-brief-row-link { cursor: pointer; }
.lc-brief-chip-tag { flex: none; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; padding: 0 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); }
.lc-brief-chip-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-brief-chip-link { cursor: pointer; }
.lc-brief-chip-link:hover { background: var(--dsw-alias-interactive-bg-hover); }
.lc-brief-more { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); }
.lc-detail-row { display: flex; align-items: center; gap: 8px; border-radius: 4px; /* Padding + matching negative margin: the highlight paints past the text while the layout stays put. */ padding: 1px 4px; margin: -1px -4px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
/* Category hover-link: the shared hover lights the matching detail row — same pairing as the legend chips and the
-2
View File
@@ -12,6 +12,4 @@
.lc-settings-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
.lc-settings-label { flex: 1; min-width: 0; font-size: 14px; color: var(--dsw-alias-label-primary); }
.lc-settings-select { 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; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-settings-select:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
.lc-settings-select:disabled { opacity: .5; cursor: default; }
.lc-settings-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 1.5; margin: 12px 0 4px; }
+1 -5
View File
@@ -16,13 +16,11 @@
.lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
.lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
Stats cell tooltip (cost cell): styled DOM bubble because native `title` can't hold the multi-row price breakdown; a '?' signals the affordance and hovering anywhere on the cell reveals the bubble below (the cell is the tip's position frame). Chrome rides the shared `.lc-tip`.
Stats cell tooltip (cost cell): styled DOM bubble because native `title` can't hold the multi-row price breakdown; a '?' signals the affordance and hovering anywhere on the cell reveals the bubble below (the cell is the tip's position frame). Chrome rides the shared `.lc-tip`; the reveal and the badge's brighten ride `group-hover/tip` variants (`group/tip` on the cell).
*/
.lc-stat-tipped { position: relative; }
.lc-stat-q { display: inline-block; margin-left: 4px; width: 11px; height: 11px; line-height: 11px; border-radius: 50%; text-align: center; font-size: 9px; font-weight: 700; font-style: normal; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); cursor: help; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-stat-tipped:hover .lc-stat-q { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-primary); }
.lc-stat-tip { top: calc(100% + 6px); left: 0; max-width: 280px; line-height: 1.5; font-weight: 400; }
.lc-stat-tipped:hover .lc-stat-tip { opacity: 1; }
.lc-stat-tip-prices { display: block; margin-top: 5px; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 5px; }
.lc-stat-tip-head { display: block; font-weight: 600; }
.lc-stat-tip-row { display: block; margin-top: 2px; }
@@ -92,9 +90,7 @@
/* The settings-entry row is a button wearing the link-row chrome — reset the button chrome the shared row style doesn't cover. */
.lc-pi-row-btn { appearance: none; border: 0; background: none; font: inherit; color: inherit; text-align: left; padding: 0; cursor: pointer; width: 100%; }
.lc-pi-row { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }
.lc-pi-row:hover .lc-pi-value { text-decoration: underline; }
.lc-pi-hint { text-decoration: none; }
.lc-pi-hint:hover { text-decoration: underline; }
.lc-pi-update { color: var(--dsw-alias-state-warn-primary); font-size: 11px; margin-left: 6px; white-space: nowrap; }
.lc-pi-label { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
+9 -6
View File
@@ -1,10 +1,13 @@
/*
* Tailwind entry: compiles (tsdown.config.ts, @tailwindcss/node) the layout
* utilities carried by the plugin's JSX class lists — box layout, flex/grid,
* spacing, sizing, positioning, overflow, and the @container responsive
* variants. The sibling sheets keep everything Tailwind does not own here:
* the harness's `--dsw-*` color/typography chrome, hover/focus states,
* contextual selectors, animations.
* Tailwind entry: compiles (tsdown.config.ts, @tailwindcss/node) the utilities
* carried by the plugin's JSX class lists — box layout, flex/grid, spacing,
* sizing, positioning, overflow, the @container responsive variants, and the
* pure `:hover`/`:focus` pseudo-state styling (hover/focus/group-hover
* variants; Tailwind media-guards hover behind `(hover: hover)`). The sibling
* sheets keep everything the variants cannot own here: the harness's
* `--dsw-*` color/typography chrome, the JS-state class systems (the lit/dim/
* selected markers, several of which double as test anchors and DOM query
* hooks), their contextual overrides, and animations.
*
* Deliberate deviations from a stock Tailwind setup, all for cohabitation
* with the harness host page:
-1
View File
@@ -45,7 +45,6 @@
.lc-chart-tip { position: absolute; bottom: calc(100% + 4px); left: 0; z-index: 5; display: flex; flex-direction: column; gap: 2px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }
/* The metric row reads secondary to the identity row. */
.lc-chart-tip span + span { color: var(--dsw-alias-label-secondary); }
.lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }
/*
Hovering previews a bar with a thin dashed frame; a CLICKED (locked)
bar must always show the strong solid frame, so the selected rule is
+1 -1
View File
@@ -879,7 +879,7 @@ describe('ContextBrowser tool schemas', () => {
assert.equal(text(query(rows[0], '.lc-br-preview')), 'write', 'tool name leads')
const chip = chips[0] as HTMLElement
assert.equal(chip.previousElementSibling, query(rows[0], '.lc-br-preview'), 'plugin chip sits right after the tool name')
assert.equal(chip.parentElement!.className, 'lc-br-elem-row', 'plugin chip is a single frame, a direct row child')
assert.ok(chip.parentElement!.classList.contains('lc-br-elem-row'), 'plugin chip is a single frame, a direct row child')
assert.equal(queryAll(chip, '.lc-br-tag').length, 0, 'no nested tag wrapper')
await m.unmount()
})