diff --git a/src/client/categories.ts b/src/client/categories.ts index 51b4b91..86c0b2f 100644 --- a/src/client/categories.ts +++ b/src/client/categories.ts @@ -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) }] } diff --git a/src/client/components/agentGraph.tsx b/src/client/components/agentGraph.tsx index 445ce2b..422df2a 100644 --- a/src/client/components/agentGraph.tsx +++ b/src/client/components/agentGraph.tsx @@ -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)" /> ))} diff --git a/src/client/components/donut.tsx b/src/client/components/donut.tsx index 5a7445f..4180a2b 100644 --- a/src/client/components/donut.tsx +++ b/src/client/components/donut.tsx @@ -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) }} /> ))} diff --git a/src/client/components/statsTiming.tsx b/src/client/components/statsTiming.tsx index 9003532..2ffb6ac 100644 --- a/src/client/components/statsTiming.tsx +++ b/src/client/components/statsTiming.tsx @@ -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: { diff --git a/src/client/components/trendChart.tsx b/src/client/components/trendChart.tsx index e01b59f..ac70dee 100644 --- a/src/client/components/trendChart.tsx +++ b/src/client/components/trendChart.tsx @@ -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 diff --git a/src/client/styles/agentGraph.css b/src/client/styles/agentGraph.css index 6420141..b71ba6b 100644 --- a/src/client/styles/agentGraph.css +++ b/src/client/styles/agentGraph.css @@ -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; } diff --git a/src/client/styles/events.css b/src/client/styles/events.css index 41c36d4..cbfa95b 100644 --- a/src/client/styles/events.css +++ b/src/client/styles/events.css @@ -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; } diff --git a/src/client/styles/requestDetail.css b/src/client/styles/requestDetail.css index 6ab0627..e823bc6 100644 --- a/src/client/styles/requestDetail.css +++ b/src/client/styles/requestDetail.css @@ -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; } diff --git a/src/client/styles/stackedBar.css b/src/client/styles/stackedBar.css index 237b5b2..3a642e8 100644 --- a/src/client/styles/stackedBar.css +++ b/src/client/styles/stackedBar.css @@ -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. */ diff --git a/src/client/styles/stats.css b/src/client/styles/stats.css index ec98fbd..8942e58 100644 --- a/src/client/styles/stats.css +++ b/src/client/styles/stats.css @@ -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. */ diff --git a/src/client/styles/trendChart.css b/src/client/styles/trendChart.css index 565cc61..039dd10 100644 --- a/src/client/styles/trendChart.css +++ b/src/client/styles/trendChart.css @@ -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%; } /* diff --git a/tests/client/categories.spec.ts b/tests/client/categories.spec.ts index e1c58ea..64fca65 100644 --- a/tests/client/categories.spec.ts +++ b/tests/client/categories.spec.ts @@ -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], ]) }) diff --git a/tests/client/components/agentGraph.spec.ts b/tests/client/components/agentGraph.spec.ts index 4db4ee3..965e37e 100644 --- a/tests/client/components/agentGraph.spec.ts +++ b/tests/client/components/agentGraph.spec.ts @@ -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)') }) }) diff --git a/tests/client/components/donut.spec.ts b/tests/client/components/donut.spec.ts index d7d36b1..2accda9 100644 --- a/tests/client/components/donut.spec.ts +++ b/tests/client/components/donut.spec.ts @@ -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() }) diff --git a/tests/client/components/trendChart.spec.ts b/tests/client/components/trendChart.spec.ts index 0345262..1a0c25a 100644 --- a/tests/client/components/trendChart.spec.ts +++ b/tests/client/components/trendChart.spec.ts @@ -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() })