refactor(client): adopt Tailwind color tokens for the plugin's palette

Replace the hand-picked hex literals with the nearest standard Tailwind
tokens now emitted by the CSS channel: the category/timing/ring accents
(indigo, amber, green, purple, blue, teal, pink, violet, red, slate-400)
and the neutral chart tracks and zebra fills, recast as color-mix over
--color-neutral-500 at the original alphas.

Donut and agent-graph ring segments now carry their stroke as an inline
style instead of the presentation attribute: the colors are CSS variables
and var() is unusable in an attribute. Free segments keep their class
rule by omitting the inline stroke. Visual output is unchanged (the v4
values are the nearest equivalents of the former v3 hexes); harness
--dsw-* semantic tokens, the logo fills and the language identity colors
stay as-is.
This commit is contained in:
Bowen Liang committed 2026-09-13 09:18:40 +08:00
1 parent f1ab3a18c6
commit 75fada4fb3
15 files changed
+58 -49

No files matched your search

+7 -7
View File
@@ -27,12 +27,12 @@ export interface PartsPart {
}
export const CATS: { key: Category | 'system' | 'tools'; color: string }[] = [
{ key: 'system', color: '#6366f1' },
{ key: 'tools', color: '#f59e0b' },
{ key: 'user', color: '#22c55e' },
{ key: 'inject', color: '#a855f7' },
{ key: 'assistant', color: '#3b82f6' },
{ key: 'tool', color: '#14b8a6' },
{ key: 'system', color: 'var(--color-indigo-500)' },
{ key: 'tools', color: 'var(--color-amber-500)' },
{ key: 'user', color: 'var(--color-green-500)' },
{ key: 'inject', color: 'var(--color-purple-500)' },
{ key: 'assistant', color: 'var(--color-blue-500)' },
{ key: 'tool', color: 'var(--color-teal-500)' },
]
/** Category key → bar color, for per-item bands (the browser's DNA mode) that bypass the CATS-order part builders. */
@@ -130,5 +130,5 @@ export function billedParts(
const prompt = input > 0
? anchoredParts(estimated, input)
: estimated.map(p => ({ ...p, value: 0 }))
return [...prompt, { key: 'output', color: '#ec4899', value: Math.max(0, usage.outputTokens) }]
return [...prompt, { key: 'output', color: 'var(--color-pink-500)', value: Math.max(0, usage.outputTokens) }]
}
+7 -4
View File
@@ -40,9 +40,9 @@ const CAPTION_H = 60
/** Fallback arc color for pressure-only nodes (no composition data), by fill ratio. */
export function ringColorOf(pct: number | null): string {
if (pct === null) return 'var(--dsw-alias-border-l1)'
if (pct >= 90) return '#ef4444'
if (pct >= 70) return '#f59e0b'
return '#22c55e'
if (pct >= 90) return 'var(--color-red-500)'
if (pct >= 70) return 'var(--color-amber-500)'
return 'var(--color-green-500)'
}
export function makeAgentGraph(
@@ -269,9 +269,12 @@ function AgentNodeView(props: NodeViewProps): ReactElement {
key={seg.key}
className={'lc-agent-seg' + (seg.free ? ' lc-agent-free' : '')}
r={AGENT_RING_R}
stroke={seg.free ? undefined : seg.color}
strokeDasharray={`${seg.len} ${ring - seg.len}`}
strokeDashoffset={-seg.offset}
// Inline style, not the stroke attribute: segment colors are CSS variables
// (var() is unusable in a presentation attribute). Free segments carry no
// inline stroke so the .lc-agent-free class rule keeps painting the remainder.
style={{ stroke: seg.free ? undefined : seg.color }}
transform="rotate(-90)"
/>
))}
+3 -2
View File
@@ -95,13 +95,14 @@ export function makeDonut(kit: ViewKit): (props: DonutProps) => ReactElement {
cy="21"
r="15.9155"
fill="none"
stroke={a.color}
strokeWidth="4"
strokeDasharray={`${a.len} ${100 - a.len}`}
strokeDashoffset={a.offset}
// Sweep-in stagger slot (stats.css animates stroke-dasharray from 0 100 up to these attribute
// values, so the slices build clockwise from 12 o'clock, one after another).
style={{ '--lc-i': i } as CSSProperties}
// The stroke rides inline because the colors are CSS variables and SVG
// presentation attributes cannot carry var().
style={{ '--lc-i': i, stroke: a.color } as CSSProperties}
onMouseEnter={() => { if (props.onHoverKey !== undefined) props.onHoverKey(a.key) }}
/>
))}
+6 -6
View File
@@ -38,12 +38,12 @@ interface Slice {
}
const COLOR = {
ttft: '#3b82f6',
reasoning: '#8b5cf6',
text: '#ec4899',
toolarg: '#f59e0b',
tools: '#14b8a6',
other: '#94a3b8',
ttft: 'var(--color-blue-500)',
reasoning: 'var(--color-violet-500)',
text: 'var(--color-pink-500)',
toolarg: 'var(--color-amber-500)',
tools: 'var(--color-teal-500)',
other: 'var(--color-slate-400)',
} as const
export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => ReactElement): (props: {
+4 -1
View File
@@ -107,7 +107,10 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactE
const STAGGER_CAP = 20
// Neutral zebra, deliberately DISJOINT from the category palette — the strip must read as a partition layer, not a bottom segment of the
// composition bars.
const TURN_FILLS = ['rgba(128,128,128,0.12)', 'rgba(128,128,128,0.26)']
const TURN_FILLS = [
'color-mix(in srgb, var(--color-neutral-500) 12%, transparent)',
'color-mix(in srgb, var(--color-neutral-500) 26%, transparent)',
]
// Turn labels render at natural width (a 2-digit "12" is wider than a 14px turn bar) and overflow their block.
// Every label must stay on the single line, so the strip shrinks ALL labels to one font size — the largest at
// which the tightest adjacent pair still clears the gap (analytic widths below, no measurement) — and the
+1 -1
View File
@@ -40,7 +40,7 @@
.lc-agent-self-badge { margin-left: 5px; vertical-align: 1px; }
.lc-agents-badge-on { color: var(--dsw-alias-state-success-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent); }
.lc-agents-inspector-stats { color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-agents-inspector-open { margin-left: auto; flex: none; color: var(--dsw-alias-brand-primary, #3b82f6); }
.lc-agents-inspector-open { margin-left: auto; flex: none; color: var(--dsw-alias-brand-primary, var(--color-blue-500)); }
.lc-agents-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 8px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
.lc-agents-legend-item { display: inline-flex; align-items: center; gap: 5px; }
.lc-agents-legend-item i { width: 8px; height: 8px; border-radius: 2px; }
+2 -2
View File
@@ -1,7 +1,7 @@
/* The event log: list rows and kind tint pills (events.tsx). */
.lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }
.lc-event-icon.lc-event-inject { color: #a855f7; }
.lc-event-icon.lc-event-inject { color: var(--color-purple-500); }
.lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }
.lc-event-icon.lc-event-mode { color: var(--dsw-alias-label-secondary); }
/*
@@ -10,7 +10,7 @@
.lc-kind { flex: none; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }
.lc-kind-inject { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }
.lc-kind-compaction { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); color: var(--dsw-alias-state-error-primary); }
.lc-kind-prune { background: color-mix(in srgb, #8b5cf6 15%, transparent); color: #8b5cf6; }
.lc-kind-prune { background: color-mix(in srgb, var(--color-violet-500) 15%, transparent); color: var(--color-violet-500); }
.lc-kind-model { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent); color: var(--dsw-alias-brand-primary); }
.lc-kind-mode { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 15%, transparent); color: var(--dsw-alias-label-secondary); }
.lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+2 -2
View File
@@ -52,10 +52,10 @@ button.lc-brief-row-link { cursor: pointer; }
.lc-detail-row-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }
.lc-detail-row-on .lc-detail-label { font-weight: 600; }
.lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }
.lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; position: relative; }
.lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--color-neutral-500) 18%, transparent); overflow: hidden; display: block; position: relative; }
.lc-bar-fill { display: block; height: 100%; border-radius: 3px; }
/* Delta rows: a hairline zero at the middle, growth fills right of it, shrinkage left. */
.lc-bar-zero { position: absolute; left: 50%; top: 0; bottom: 0; border-left: 1px solid rgba(128,128,128,0.45); }
.lc-bar-zero { position: absolute; left: 50%; top: 0; bottom: 0; border-left: 1px solid color-mix(in srgb, var(--color-neutral-500) 45%, transparent); }
.lc-bar-fill-up, .lc-bar-fill-down { position: absolute; top: 0; height: 100%; display: block; }
.lc-bar-fill-up { left: 50%; border-radius: 0 3px 3px 0; }
.lc-bar-fill-down { right: 50%; border-radius: 3px 0 0 3px; }
+1 -1
View File
@@ -1,6 +1,6 @@
/* The composition stacked bar with occupied-window frame, reserve band, legend chips and hover tip (stackedBar.tsx). */
.lc-stacked-wrap { position: relative; width: 100%; }
.lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }
.lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: color-mix(in srgb, var(--color-neutral-500) 18%, transparent); position: relative; }
/*
Hover reference frame around the OCCUPIED region of the composition bar: a solid box from the left edge to the used/window boundary so the legend/tooltip 'share of used' percentages visibly map to the boxed part; pointer-events: none keeps hover on the segments/free; the free track echoes it dashed so the two read as one system.
*/
+1 -1
View File
@@ -45,7 +45,7 @@
.lc-donut-row .lc-sl { flex: 1 1 auto; }
.lc-donut { position: relative; flex: none; }
.lc-donut svg { display: block; }
.lc-donut-track { stroke: rgba(128,128,128,0.18); }
.lc-donut-track { stroke: color-mix(in srgb, var(--color-neutral-500) 18%, transparent); }
/* The legend row ↔ segment hover link: hovering either side dims the ring's
other slices to 0.35 (the stacked bars' idiom) while the active one stays
solid; the center figures stay at rest. */
+2 -2
View File
@@ -30,7 +30,7 @@
.lc-grid-q1 { top: 102px; }
/* The SOLID zero baseline in BOTH modes — inline-positioned off the up-arm in delta, the chart floor
(top 130px, right under the bars) under the '0' label in total; painted after the dashed guides. */
.lc-grid-zero { top: 130px; border-top-style: solid; border-top-color: rgba(128,128,128,0.5); }
.lc-grid-zero { top: 130px; border-top-style: solid; border-top-color: color-mix(in srgb, var(--color-neutral-500) 50%, transparent); }
.lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; outline: 1px dashed transparent; outline-offset: 1px; transition: opacity 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), outline-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-chart-dim .lc-bar { opacity: 0.35; }
.lc-chart-dim .lc-bar-in-turn { opacity: 1; }
@@ -55,7 +55,7 @@
*/
.lc-bar-hovered { outline-color: var(--dsw-alias-brand-primary); }
.lc-bar-selected { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 1px; }
.lc-bar-in-turn { background: rgba(128,128,128,0.14); }
.lc-bar-in-turn { background: color-mix(in srgb, var(--color-neutral-500) 14%, transparent); }
.lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }
.lc-bar-stack > div { width: 100%; }
/*
+13 -13
View File
@@ -24,12 +24,12 @@ describe('partsOf', () => {
test('reads all six categories in CATS order with their colors', () => {
const parts = partsOf(current({ system: 1, tools: 2, user: 3, inject: 4, assistant: 5, tool: 6 }))
assert.deepEqual(parts.map(p => [p.key, p.color, p.value]), [
['system', '#6366f1', 1],
['tools', '#f59e0b', 2],
['user', '#22c55e', 3],
['inject', '#a855f7', 4],
['assistant', '#3b82f6', 5],
['tool', '#14b8a6', 6],
['system', 'var(--color-indigo-500)', 1],
['tools', 'var(--color-amber-500)', 2],
['user', 'var(--color-green-500)', 3],
['inject', 'var(--color-purple-500)', 4],
['assistant', 'var(--color-blue-500)', 5],
['tool', 'var(--color-teal-500)', 6],
])
})
@@ -131,13 +131,13 @@ describe('billedParts', () => {
// Input 400 equals the estimated total, so the ratios carry over as-is;
// the seven parts sum to the chat line's 460 by construction.
assert.deepEqual(parts.map(p => [p.key, p.color, p.value]), [
['system', '#6366f1', 50],
['tools', '#f59e0b', 50],
['user', '#22c55e', 100],
['inject', '#a855f7', 0],
['assistant', '#3b82f6', 100],
['tool', '#14b8a6', 100],
['output', '#ec4899', 60],
['system', 'var(--color-indigo-500)', 50],
['tools', 'var(--color-amber-500)', 50],
['user', 'var(--color-green-500)', 100],
['inject', 'var(--color-purple-500)', 0],
['assistant', 'var(--color-blue-500)', 100],
['tool', 'var(--color-teal-500)', 100],
['output', 'var(--color-pink-500)', 60],
])
})
+3 -3
View File
@@ -393,8 +393,8 @@ describe('AgentGraph — the family tree', () => {
describe('ringColorOf', () => {
test('occupancy thresholds', () => {
assert.equal(ringColorOf(null), 'var(--dsw-alias-border-l1)')
assert.equal(ringColorOf(95), '#ef4444')
assert.equal(ringColorOf(70), '#f59e0b')
assert.equal(ringColorOf(12), '#22c55e')
assert.equal(ringColorOf(95), 'var(--color-red-500)')
assert.equal(ringColorOf(70), 'var(--color-amber-500)')
assert.equal(ringColorOf(12), 'var(--color-green-500)')
})
})
+3 -1
View File
@@ -53,7 +53,9 @@ describe('Donut', () => {
}))
const circles = queryAll(m.container, 'circle')
assert.equal(circles.length, 1)
assert.equal(circles[0].getAttribute('stroke'), '#ffffff')
// The color rides inline style (var() values can't live in a stroke attribute);
// jsdom reports the style keyword in its normalized rgb() spelling.
assert.equal(circles[0].style.stroke, 'rgb(255, 255, 255)')
await m.unmount()
})
+3 -3
View File
@@ -179,7 +179,7 @@ describe('TrendChart step granularity, total mode', () => {
// Zero-value categories are skipped entirely (r3.user = 0 → five segments, no user-green segment).
const segs3 = queryAll(bs[2], '.lc-bar-stack > div')
assert.equal(segs3.length, 5)
assert.ok(![...segs3].some(s => s.style.background.includes('34, 197, 94') || s.style.background === '#22c55e'))
assert.ok(![...segs3].some(s => s.style.background.includes('var(--color-green-500)')))
// Total-mode axis: full quartile graduation — max, ¾, ½, ¼, 0.
assert.equal(query(m.container, '.lc-axis-top').textContent, '600')
@@ -194,8 +194,8 @@ describe('TrendChart step granularity, total mode', () => {
assert.equal(turns.length, 2)
assert.equal(turns[0].style.width, '30px')
assert.equal(turns[1].style.width, '14px')
assert.ok(turns[0].style.background.includes('0.12'))
assert.ok(turns[1].style.background.includes('0.26'))
assert.ok(turns[0].style.background.includes('var(--color-neutral-500) 12%'))
assert.ok(turns[1].style.background.includes('var(--color-neutral-500) 26%'))
assert.deepEqual(turns.map(t => t.textContent), ['1', '0'])
await m.unmount()
})