From 44d58a96a7126e4478704fccfc239628fafd79fb Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Fri, 14 Aug 2026 22:56:27 +0800 Subject: [PATCH] fix(ui): turn bars use a uniform column width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn aggregates no longer stretch to their step span — every bar keeps the fixed 14px column in both granularities, and the turn strip blocks match (uniform in turn mode, step-spanning in step mode), so bars and blocks stay aligned 1:1. --- scripts/test-client.mjs | 10 +++++----- src/client.cts | 29 ++++++++++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/scripts/test-client.mjs b/scripts/test-client.mjs index fec2774..77b2319 100644 --- a/scripts/test-client.mjs +++ b/scripts/test-client.mjs @@ -353,14 +353,14 @@ assert.equal(byClass(tr, 'lc-turn').length, 3, 'turn strip still has one block p const turnOn = byClass(tr, 'lc-gran-on') assert.equal(turnOn[0].args[2], 'Turn', 'turn button is active after switching') -// turn bars span their steps' columns and align with their strip blocks +// turn bars keep the uniform column width and align with their strip blocks const turnBars = byClass(tr, 'lc-bar') const t1Bar = turnBars.find(b => b.args[1].key === 2) assert.ok(t1Bar, 'T1 is aggregated into its last step (seq 2)') -assert.equal(t1Bar.args[1].style.width, '30px', 'T1 bar spans its two step columns') -const t1Block = byClass(tr, 'lc-turn')[0] -assert.equal(t1Block.args[1].style.width, '30px', 'T1 strip block matches the bar width (aligned)') -assert.equal(turnBars[1].args[1].style.width, '14px', 'single-step turn bar keeps the step column width') +for (const b of turnBars) assert.equal(b.args[1].style.width, '14px', 'every turn bar keeps the uniform column width') +const turnBlocks2 = byClass(tr, 'lc-turn') +for (const blk of turnBlocks2) assert.equal(blk.args[1].style.width, '14px', 'every turn block matches the bar width (aligned)') +assert.equal(turnBlocks2[0].args[1].style.width, t1Bar.args[1].style.width, 'T1 bar and block align 1:1') // the turn detail is labeled with the step count and tagged as the last step t1Bar.args[1].onMouseEnter() diff --git a/src/client.cts b/src/client.cts index 65701a8..bac57fa 100644 --- a/src/client.cts +++ b/src/client.cts @@ -419,14 +419,15 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re } // Consecutive requests of the same turn collapse into one labeled range. - // `span` is the number of STEP columns the group covers: step records - // count one each, turn aggregates count their stepCount, so the strip - // blocks always line up with the bars above (in both granularities). - const groups: { turn: number; count: number; span: number }[] = [] + // `span` is the number of STEP columns the group covers (step records + // count one each). In turn granularity the bars are uniform width, so + // the strip blocks are uniform too; in step granularity they span their + // steps' columns. Either way bars and blocks stay aligned. + const groups: { turn: number; count: number; span: number; agg: boolean }[] = [] for (const req of requests) { let grp = groups.length > 0 ? groups[groups.length - 1] : null if (grp === null || grp.turn !== (req.turn ?? 0)) { - grp = { turn: req.turn ?? 0, count: 0, span: 0 } + grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== undefined } groups.push(grp) } grp.count++ @@ -480,11 +481,6 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re const selected = props.selectedSeq === req.seq const hovered = props.hoveredSeq === req.seq const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn - // Turn aggregates span their steps' columns, so the bar IS the - // turn's width and lines up with its strip block below. - const barW = req.stepCount !== undefined - ? req.stepCount * (BAR_W + BAR_GAP) - BAR_GAP - : BAR_W const tip = (req.stepCount !== undefined && req.stepCount > 1 ? tr('tip.turn', { t: req.turn ?? 0, n: req.stepCount }) : tr('tip.step', { t: req.turn ?? 0, s: req.step ?? 0 })) @@ -494,11 +490,13 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re + CATS.map(c => catLabel(c.key) + ' ' + fmt(req[c.key] || 0)).join(' / ') return h('div', { key: req.seq, + // Uniform column width in BOTH granularities: turn aggregates + // keep the same fixed width as step bars. className: 'lc-bar' + (selected ? ' lc-bar-selected' : '') + (hovered ? ' lc-bar-hovered' : '') + (inTurn ? ' lc-bar-in-turn' : ''), - style: { width: barW + 'px' }, + style: { width: BAR_W + 'px' }, title: tip, onClick: () => { props.onSelect(selected ? null : req.seq) }, onMouseEnter: () => { props.onHover(req.seq) }, @@ -520,15 +518,16 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re // is shared hover-only state). h('div', { className: 'lc-turns', onMouseLeave: () => { props.onHoverTurn(null) } }, groups.map((grp, gi) => { - // One column per bar plus the shared gap; the flex gap between - // blocks restores the inter-group gap, so block spans line up - // with their bars exactly. + // Turn mode: uniform blocks under uniform bars (1:1). Step mode: + // blocks span their steps' columns. The flex gap between blocks + // mirrors the bar gap, so blocks always line up with the bars. const on = props.activeTurn === grp.turn + const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP return h('span', { key: 'turn-' + gi, className: 'lc-turn' + (on ? ' lc-turn-on' : ''), style: { - width: (grp.span * (BAR_W + BAR_GAP) - BAR_GAP) + 'px', + width: blockW + 'px', background: TURN_COLORS[gi % TURN_COLORS.length], }, title: 'T' + grp.turn,