feat(client): lead the trend detail panel with the composition card's chips

The pinned step's per-category rows were a second rendering of the same
categories, drifted from the one above: their own grid, dimmed regular-weight
labels, a fixed figure column. The panel now renders the composition card's
own chip legend — same dot, 600-weight label, right-aligned figures with the
secondary share, same hover highlight and 160px grid reflow — in the shared
CATS order, so a category reads identically in both cards.

Delta mode has no counterpart above, so makeLegend gains a delta variant: the
chips print the signed change colored by sign, and both the share and its
"of used" hint drop out. The panel's chips report hovers back exactly as the
composition legend's do, and the reserved lane mirrors the chip grid so the
pending and loaded panels still measure alike.
This commit is contained in:
bowenliang123 committed 2026-10-15 03:06:24 +08:00
1 parent 408bbc1a29
commit cc0babaf02
9 files changed
+95 -105

No files matched your search

+7 -7
View File
@@ -66,14 +66,13 @@ function TrendReserved(props: { children: ReactNode; catLabel: (cat: Category |
<div className="lc-detail-head" />
<div className="lc-brief" />
<div className="lc-stacked-wrap"><div className="lc-stacked" /></div>
<div className="lc-detail-rows">
<div className="lc-legend">
{CATS.map(c => (
<div key={c.key} className="lc-detail-row">
<span key={c.key} className="lc-chip">
<i style={{ background: c.color }} />
<span className="lc-detail-label">{props.catLabel(c.key)}</span>
<span className="lc-detail-num" />
<span className="lc-detail-pct" />
</div>
<span className="lc-chip-label">{props.catLabel(c.key)}</span>
<span className="lc-chip-nums" />
</span>
))}
</div>
</div>
@@ -94,7 +93,7 @@ export function makeContextView(
const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend)
const TrendChart = makeTrendChart(kit)
const TrendOverview = makeTrendOverview(kit)
const RequestDetail = makeRequestDetail(kit, StackedBar)
const RequestDetail = makeRequestDetail(kit, StackedBar, Legend)
const EventList = makeEventList(kit)
const FileCard = makeFileCard(kit, settings)
// The stats board's subagent-cost cell reads the shared page-scope cold-head cache.
@@ -572,6 +571,7 @@ export function makeContextView(
stepsOf={stepsOf}
onLocate={locateNode}
hoverKey={trendHoverCat}
onHoverKey={setHoverCat}
/>
</div>
)}
+2 -7
View File
@@ -1,14 +1,9 @@
import { type ReactElement } from 'react'
import type { PartsPart } from '../categories'
import type { Headline } from '../headline'
import type { ViewKit } from '../viewkit'
import type { StackedBarProps } from './stackedBar'
import type { StackedBarProps, LegendProps } from './stackedBar'
type LegendFn = (props: {
parts: PartsPart[]
hoverKey?: string | null
onHoverKey?: (key: string | null) => void
}) => ReactElement
type LegendFn = (props: LegendProps) => ReactElement
export interface CurrentCompositionProps {
head: Headline
+9 -23
View File
@@ -5,7 +5,7 @@ import { cacheHitPercent } from '../format'
import type { StepBrief } from '../brief'
import { blockSummaryOf, callNamesOf, callSummaryOf } from '../callSummary'
import type { ConversationNodeLike } from '../services'
import type { StackedBarProps } from './stackedBar'
import type { StackedBarProps, LegendProps } from './stackedBar'
import type { ViewKit } from '../viewkit'
export interface RequestDetailProps {
@@ -24,16 +24,18 @@ export interface RequestDetailProps {
stepsOf?: (turn: number | undefined) => number
/** Reveal a brief row's node in the Context browser; absent = rows render inert. */
onLocate?: (node: SurfaceNode, isResponse: boolean) => void
/** Mirrored category hover (shared with the overview/browser): lights the composition bar's matching
* segment; the bar never reports hovers back. */
/** Category hover shared with the overview/browser and the composition legend: lights the matching bar
* segment and chip, and the chips report hovers back — the bar never does. */
hoverKey?: string | null
onHoverKey?: (key: string | null) => void
}
export function makeRequestDetail(
kit: ViewKit,
StackedBar: (props: StackedBarProps) => ReactElement,
Legend: (props: LegendProps) => ReactElement,
): ComponentType<RequestDetailProps> {
const { t, fmt, fmtTime, catLabel, eventLabel, eventAt } = kit
const { t, fmt, fmtTime, eventLabel, eventAt } = kit
/** One brief row: a fixed-width kind tag plus one glanceable line. The tag carries a styled, instant explanation bubble (the
* shared `.lc-tip` chrome); the content span keeps the native title (preview + locate hint), so the two never stack.
@@ -213,6 +215,8 @@ export function makeRequestDetail(
const parts = delta
? CATS.map((c, i) => ({ key: c.key, color: c.color, value: Math.abs(deltas[i]) }))
: partsOf(req)
// Delta's chips print the signed change; the bar's own parts keep only magnitudes.
const legendParts = delta ? parts.map((p, i) => ({ ...p, raw: deltas[i] })) : parts
return (
<div className="lc-detail">
<div className="lc-detail-head">
@@ -248,25 +252,7 @@ export function makeRequestDetail(
{/* Mirrors the shared category hover with the tip off — a cross-card hover must not float a second tooltip
over a bar the pointer does not rest on (same rule as the browser's own bar). */}
<StackedBar parts={parts} height={10} hoverKey={props.hoverKey} tip={false} />
<div className="lc-detail-rows">
{CATS.map((c, i) => {
const v = delta ? deltas[i] : req[c.key] || 0
return (
<div key={c.key} className={'lc-detail-row' + (props.hoverKey === c.key ? ' lc-detail-row-on' : '')}>
<i style={{ background: c.color }} />
<span className="lc-detail-label">{catLabel(c.key)}</span>
{delta ? (
<span className={'lc-detail-num' + (v > 0 ? ' lc-detail-num-up' : v < 0 ? ' lc-detail-num-down' : '')}>
{(v > 0 ? '+' : '') + fmt(v)}
</span>
) : (
<span className="lc-detail-num">{'≈' + fmt(v)}</span>
)}
<span className="lc-detail-pct">{!delta && req.total > 0 ? `${Math.round(v / req.total * 100)}%` : ''}</span>
</div>
)
})}
</div>
<Legend parts={legendParts} hoverKey={props.hoverKey} onHoverKey={props.onHoverKey} delta={delta} />
</div>
)
})
+14 -11
View File
@@ -289,17 +289,19 @@ export function makeStackedBar(kit: ViewKit): (props: StackedBarProps) => ReactE
}
}
export function makeLegend(kit: ViewKit): (props: {
/** The category chips' interface, shared by the composition card's legend and the trend panel. */
export interface LegendProps {
parts: PartsPart[]
hoverKey?: string | null
onHoverKey?: (key: string | null) => void
}) => ReactElement {
/** Signed figures (the trend panel's Delta mode): the count prints with its sign and the sign's color,
* and the share drops out — a signed change has no share of the used total. */
delta?: boolean
}
export function makeLegend(kit: ViewKit): (props: LegendProps) => ReactElement {
const { t, fmt, catLabel } = kit
return function Legend(props: {
parts: PartsPart[]
hoverKey?: string | null
onHoverKey?: (key: string | null) => void
}): ReactElement {
return function Legend(props: LegendProps): ReactElement {
let total = 0
for (const p of props.parts) total += p.raw ?? p.value
return (
@@ -307,19 +309,20 @@ export function makeLegend(kit: ViewKit): (props: {
{props.parts.map((p) => {
const count = p.raw ?? p.value
const on = props.hoverKey !== undefined && props.hoverKey === p.key
const sign = props.delta === true && count !== 0 ? ' lc-chip-nums-' + (count > 0 ? 'up' : 'down') : ''
return (
<span
key={p.key}
className={'lc-chip' + (on ? ' lc-chip-on' : '')}
title={t('overview.ofUsed')}
title={props.delta === true ? undefined : t('overview.ofUsed')}
onMouseEnter={() => { if (props.onHoverKey !== undefined) props.onHoverKey(p.key) }}
onMouseLeave={() => { if (props.onHoverKey !== undefined) props.onHoverKey(null) }}
>
<i style={{ background: p.color }} />
<span className="lc-chip-label">{catLabel(p.key)}</span>
<span className="lc-chip-nums">
{'≈' + fmt(count)}
{total > 0 ? <em>{`${Math.round(count / total * 100)}%`}</em> : null}
<span className={'lc-chip-nums' + sign}>
{props.delta === true ? (count > 0 ? '+' : '') + fmt(count) : '≈' + fmt(count)}
{props.delta !== true && total > 0 ? <em>{`${Math.round(count / total * 100)}%`}</em> : null}
</span>
</span>
)
-14
View File
@@ -13,8 +13,6 @@
/* Sign semantics mirror the harness diff blocks. */
.lc-detail-metric-up { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); }
.lc-detail-metric-down { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); }
/* auto-fit instead of a fixed pair: a row's min-content is ~196px, below which two columns would spill. */
.lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 4px 24px; }
/* A fixed three-row lane (3 x the 27px row + 2 x the 2px gap): the container exists even when no row resolves, AND
every row is a constant height whether or not it carries a chip — a chip is 2px taller than bare text, so without
the floor the lane measured 79.5px on an input-less step and 81.5px on one with chips, moving the trend card (and
@@ -34,15 +32,3 @@ button.lc-brief-row-link { cursor: pointer; }
.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-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. */
.lc-detail-row i { transition: box-shadow var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-detail-row-on { background: var(--dsw-alias-interactive-bg-hover); }
.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); }
/* The figures pin to the row's right edge, so they form one column across the labels' ragged widths. */
.lc-detail-num { margin-left: auto; width: 52px; text-align: right; font-variant-numeric: tabular-nums; }
.lc-detail-num-up { color: var(--dsw-alias-state-success-primary); }
.lc-detail-num-down { color: var(--dsw-alias-state-error-primary); }
.lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }
+4 -1
View File
@@ -38,8 +38,11 @@
.lc-chip { display: flex; align-items: center; gap: 5px; min-width: 0; color: var(--dsw-alias-label-primary); padding: 1px 6px; border-radius: 6px; cursor: pointer; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-chip-label { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.lc-chip-nums { margin-left: auto; flex: none; display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.lc-chip i, .lc-detail-row i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }
.lc-chip i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }
.lc-chip i { transition: box-shadow var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }
/* Signed figures (the trend panel's Delta mode): the sign semantics mirror the harness diff blocks. */
.lc-chip-nums-up { color: var(--dsw-alias-state-success-primary); }
.lc-chip-nums-down { color: var(--dsw-alias-state-error-primary); }
.lc-chip-on { font-weight: 600; background: var(--dsw-alias-interactive-bg-hover); }
.lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }
@@ -142,7 +142,7 @@ describe('ContextView — interactions', () => {
const detailBar = query(m.container, '.lc-detail .lc-stacked')
assert.ok(detailBar.className.includes('lc-stacked-dim'))
assert.ok(queryAll(detailBar, '.lc-stacked-seg-on').length === 1)
assert.ok(queryAll(m.container, '.lc-detail-row-on').length === 1, 'the matching detail row lights too')
assert.ok(queryAll(m.container, '.lc-detail .lc-chip-on').length === 1, 'the matching detail chip lights too')
await unhover(toolsRow)
assert.equal(query(m.container, '.lc-chart').hasAttribute('data-catdim'), false)
@@ -156,6 +156,20 @@ describe('ContextView — interactions', () => {
await m.unmount()
})
test('trend panel chip hover feeds the shared category state back to the composition legend', async () => {
const m = await mountRich('sv-link-chip')
// The panel's chips are hover SOURCES, exactly as the composition legend's are; "tools" is the second chip.
const chip = queryAll(m.container, '.lc-detail .lc-legend .lc-chip')[1]
await hover(chip)
assert.equal(query(m.container, '.lc-chart').getAttribute('data-catdim'), 'tools')
const comp = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['overview.title']))
assert.ok(comp !== undefined)
assert.equal(queryAll(comp, '.lc-legend .lc-chip-on').length, 1, 'the composition legend chip lights with it')
await unhover(chip)
assert.equal(query(m.container, '.lc-chart').hasAttribute('data-catdim'), false)
await m.unmount()
})
test('expanding a browser category focuses the trend bars on it; collapsing restores all', async () => {
const m = await mountRich('sv-focus')
assert.equal(text(query(m.container, '.lc-axis-top')), '420', 'unfocused: the axis tops at the largest whole-bar total')
@@ -71,8 +71,11 @@ describe('ContextView — the split generation (slim head + detail channel)', ()
// Pending: the trend card stands in the same boxes the loaded panel uses, so its arrival changes no card's height.
assert.equal(queryAll(m.container, '.lc-trend-lane').length, 1, 'the reserved body renders while the read is in flight')
assert.equal(queryAll(m.container, '.lc-trend-lane .lc-detail-rows .lc-detail-row').length, 7,
'the lane reserves one row per category')
assert.equal(queryAll(m.container, '.lc-trend-lane .lc-legend .lc-chip').length, 7,
'the lane reserves one chip per category')
const laneChip = query(m.container, '.lc-trend-lane .lc-legend .lc-chip')
assert.deepEqual(Array.from(laneChip.children).map(el => el.className.split(' ')[0]), ['', 'lc-chip-label', 'lc-chip-nums'],
'the reserved chips wear the legend chip anatomy, so the panels measure alike')
assert.equal(queryAll(m.container, '.lc-trend-lane .lc-detail-metrics').length, 0,
'no fabricated figures in the reserved panel')
+39 -39
View File
@@ -2,14 +2,14 @@ import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { makeRequestDetail } from '../../../src/client/components/requestDetail'
import { makeStackedBar } from '../../../src/client/components/stackedBar'
import { makeStackedBar, makeLegend } from '../../../src/client/components/stackedBar'
import type { RequestRecord, ContextEventRecord, SurfaceNode } from '../../../src/shared/types'
import type { ConversationNodeLike } from '../../../src/client/services'
import type { StepBrief } from '../../../src/client/brief'
import { makeKit, mount, click, queryAll, query, surfaceNode, text } from '../helpers/kit'
const kit = makeKit()
const RequestDetail = makeRequestDetail(kit, makeStackedBar(kit))
const RequestDetail = makeRequestDetail(kit, makeStackedBar(kit), makeLegend(kit))
function req(over: Partial<RequestRecord>): RequestRecord {
return {
@@ -23,12 +23,11 @@ function req(over: Partial<RequestRecord>): RequestRecord {
/** Full-category fixture so every row renders a non-zero figure. */
const FULL = { system: 100, tools: 200, user: 300, inject: 50, assistant: 400, tool: 250, total: 1300 }
/** A detail row's anatomy — dot, label, figure, share — by each child's base class (a delta row's figure adds a
* sign modifier). Pinned structurally, so reintroducing a per-row mini bar fails here in either mode, instead of
* passing an absence check on a deleted class. */
const ROW_ANATOMY = ['', 'lc-detail-label', 'lc-detail-num', 'lc-detail-pct']
/** A category chip's anatomy — dot, label, figures — by each child's base class (a delta chip's figures add a
* sign modifier). Pinned structurally: a child added, removed or reordered fails here in either mode. */
const CHIP_ANATOMY = ['', 'lc-chip-label', 'lc-chip-nums']
const anatomy = (container: HTMLElement): string[][] =>
queryAll(container, '.lc-detail-row').map(r => Array.from(r.children).map(el => el.className.split(' ')[0]))
queryAll(container, '.lc-legend .lc-chip').map(c => Array.from(c.children).map(el => el.className.split(' ')[0]))
describe('RequestDetail header', () => {
test('null request renders nothing', async () => {
@@ -37,7 +36,7 @@ describe('RequestDetail header', () => {
await m.unmount()
})
test('step detail: title, time, usage chips, per-category rows', async () => {
test('step detail: title, time, usage chips, per-category chips', async () => {
const m = await mount(h(RequestDetail, {
request: req({ prompt: 1000, output: 120, cacheRead: 500 }),
}))
@@ -52,13 +51,14 @@ describe('RequestDetail header', () => {
const ms = await mount(h(RequestDetail, { request: req({}), stepsOf: (turn) => (turn === 1 ? 7 : 1) }))
assert.ok(text(query(ms.container, '.lc-detail-head')).includes('Turn 1 · Step 1 of 7'))
await ms.unmount()
const rows = queryAll(m.container, '.lc-detail-row')
assert.equal(rows.length, 7)
assert.ok(text(rows[0]).includes('System Prompt'))
assert.ok(text(rows[0]).includes('≈100'))
const pcts = queryAll(m.container, '.lc-detail-pct').map(el => text(el))
const chips = queryAll(m.container, '.lc-legend .lc-chip')
assert.equal(chips.length, 7)
assert.ok(text(chips[0]).includes('System Prompt'))
assert.ok(text(chips[0]).includes('≈100'))
const pcts = queryAll(m.container, '.lc-chip-nums em').map(el => text(el))
assert.ok(pcts.some(p => p.endsWith('%')), 'percentages rendered when total > 0')
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => ROW_ANATOMY))
assert.equal(pcts[0], '8%', 'the share rides the used total: 100 of 1300')
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => CHIP_ANATOMY))
await m.unmount()
})
@@ -144,7 +144,7 @@ describe('RequestDetail header', () => {
})
describe('RequestDetail delta mode', () => {
test('signed deltas vs the previous record: net chip, signed rows, no usage chips', async () => {
test('signed deltas vs the previous record: net chip, signed chips, no usage chips', async () => {
const request = req({ system: 150, tools: 200, user: 250, inject: 0, assistant: 300, tool: 350, total: 1250, prompt: 900 })
const prev = req({ ...FULL, inject: 0 })
const m = await mount(h(RequestDetail, { request, prev }))
@@ -156,14 +156,14 @@ describe('RequestDetail delta mode', () => {
assert.ok(text(metric).includes('Δ 0'))
assert.ok(!metric.className.includes('lc-detail-metric-up'))
assert.ok(!metric.className.includes('lc-detail-metric-down'))
const nums = queryAll(m.container, '.lc-detail-num')
assert.ok(text(nums[0]).includes('+50'))
assert.ok(nums[0].className.includes('lc-detail-num-up'))
assert.ok(text(nums[2]).includes('-50'))
assert.ok(nums[2].className.includes('lc-detail-num-down'))
assert.ok(!nums[1].className.includes('lc-detail-num-up'), 'zero delta is neutral')
assert.ok(queryAll(m.container, '.lc-detail-pct').every(el => text(el) === ''), 'no percentages in delta mode')
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => ROW_ANATOMY), 'delta rows carry no diverging bar')
const figs = queryAll(m.container, '.lc-chip-nums')
assert.ok(text(figs[0]).includes('+50'))
assert.ok(figs[0].className.includes('lc-chip-nums-up'))
assert.ok(text(figs[2]).includes('-50'))
assert.ok(figs[2].className.includes('lc-chip-nums-down'))
assert.ok(!figs[1].className.includes('lc-chip-nums-up'), 'zero delta is neutral')
assert.equal(queryAll(m.container, '.lc-chip-nums em').length, 0, 'a signed change carries no share')
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => CHIP_ANATOMY), 'delta chips carry no diverging bar')
await m.unmount()
})
@@ -172,8 +172,8 @@ describe('RequestDetail delta mode', () => {
const metric = query(m.container, '.lc-detail-metric')
assert.ok(metric.className.includes('lc-detail-metric-up'))
assert.ok(text(metric).includes('Δ +1.3k'))
assert.equal(queryAll(m.container, '.lc-detail-num-down').length, 0)
assert.equal(queryAll(m.container, '.lc-detail-num-up').length, 6, 'every non-zero category reads up')
assert.equal(queryAll(m.container, '.lc-chip-nums-down').length, 0)
assert.equal(queryAll(m.container, '.lc-chip-nums-up').length, 6, 'every non-zero category reads up')
await m.unmount()
const m2 = await mount(h(RequestDetail, {
@@ -183,25 +183,25 @@ describe('RequestDetail delta mode', () => {
const metric2 = query(m2.container, '.lc-detail-metric')
assert.ok(metric2.className.includes('lc-detail-metric-down'))
assert.ok(text(metric2).includes('Δ -1.3k'))
assert.equal(queryAll(m2.container, '.lc-detail-num-up').length, 0)
assert.equal(queryAll(m2.container, '.lc-detail-num-down').length, 6)
assert.equal(queryAll(m2.container, '.lc-chip-nums-up').length, 0)
assert.equal(queryAll(m2.container, '.lc-chip-nums-down').length, 6)
await m2.unmount()
})
test('cumulative rows with a zero total render zero figures and no percentages', async () => {
test('cumulative chips with a zero total render zero figures and no shares', async () => {
const m = await mount(h(RequestDetail, {
request: req({ system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 }),
}))
const figs = queryAll(m.container, '.lc-detail-num').map(el => text(el))
const figs = queryAll(m.container, '.lc-chip-nums').map(el => text(el))
assert.equal(figs.length, 7)
assert.ok(figs.every(f => f === '≈0'))
assert.ok(queryAll(m.container, '.lc-detail-pct').every(el => text(el) === ''))
assert.equal(queryAll(m.container, '.lc-chip-nums em').length, 0)
await m.unmount()
})
})
describe('RequestDetail composition bar hover-link', () => {
test('the mirrored hoverKey dims the bar, lights the matching segment and detail row; the tip stays off', async () => {
test('the mirrored hoverKey dims the bar, lights the matching segment and chip; the tip stays off', async () => {
const m = await mount(h(RequestDetail, { request: req({}), hoverKey: 'tools' }))
const bar = query(m.container, '.lc-stacked')
assert.ok(bar.className.includes('lc-stacked-dim'), 'a mirrored hover dims the bar')
@@ -209,24 +209,24 @@ describe('RequestDetail composition bar hover-link', () => {
assert.equal(segs.length, 6, 'all six categories render')
assert.ok(segs[1].className.includes('lc-stacked-seg-on'), 'exactly the tools segment (CATS order) lights')
assert.ok(segs.filter(s => s.className.includes('lc-stacked-seg-on')).length === 1)
const rows = queryAll(m.container, '.lc-detail-row')
assert.ok(rows[1].className.includes('lc-detail-row-on'), 'the tools row lights too')
assert.ok(rows.filter(r => r.className.includes('lc-detail-row-on')).length === 1)
const chips = queryAll(m.container, '.lc-legend .lc-chip')
assert.ok(chips[1].className.includes('lc-chip-on'), 'the tools chip lights too')
assert.ok(chips.filter(c => c.className.includes('lc-chip-on')).length === 1)
assert.equal(queryAll(m.container, '.lc-bar-tip').length, 0, 'a mirrored hover floats no tooltip')
const m2 = await mount(h(RequestDetail, { request: req({}) }))
assert.equal(query(m2.container, '.lc-stacked').className, 'lc-stacked')
assert.equal(queryAll(m2.container, '.lc-detail-row-on').length, 0)
assert.equal(queryAll(m2.container, '.lc-chip-on').length, 0)
assert.equal(queryAll(m2.container, '.lc-bar-tip').length, 0)
await m2.unmount()
await m.unmount()
})
test('delta-mode detail rows light the matching category too', async () => {
test('delta-mode detail chips light the matching category too', async () => {
const m = await mount(h(RequestDetail, { request: req({}), prev: req({ ...FULL }), hoverKey: 'user' }))
const rows = queryAll(m.container, '.lc-detail-row')
assert.ok(rows[2].className.includes('lc-detail-row-on'), 'the user row lights in delta mode')
assert.ok(rows.filter(r => r.className.includes('lc-detail-row-on')).length === 1)
const chips = queryAll(m.container, '.lc-legend .lc-chip')
assert.ok(chips[2].className.includes('lc-chip-on'), 'the user chip lights in delta mode')
assert.ok(chips.filter(c => c.className.includes('lc-chip-on')).length === 1)
await m.unmount()
})
})