From 34a2966c19237a3a5b199bae38ffdda3e143da8d Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sat, 24 Oct 2026 14:08:37 +0200 Subject: [PATCH] fix: apply the review items for #689 at merge Clamp the stored toolbar width to MIN_WIDTH in apply() and step the arrow keys from the width on screen, so a drag to the window edge or a held ArrowRight no longer stores a negative width or leaves an ArrowLeft dead zone (browser test added) Drop Escape as a reset key on the handle: the global capture-phase Escape handler already closes every panel on it (static pin added) Say in the changeset, the wiki and the densityFor comment that the percentages go at the narrowest step in the Compact style only, and drop Escape from the changeset Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/header-divider.md | 2 +- docs/wiki/The-Dashboard.md | 6 +++--- src/web/public/header-divider.js | 11 ++++++----- test/header-divider.browser.test.ts | 22 ++++++++++++++++++++++ test/header-divider.test.ts | 2 ++ 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.changeset/header-divider.md b/.changeset/header-divider.md index 2ff36cb9..6868a3a1 100644 --- a/.changeset/header-divider.md +++ b/.changeset/header-divider.md @@ -2,4 +2,4 @@ 'aicodeman': minor --- -Drag the line between the session tabs and the header toolbar to resize the toolbar. Dragging it right narrows the toolbar, which then wraps onto a second line instead of squeezing out the tabs, and the usage readings shed their labels, then their provider names, then their percentages as it narrows; double-click, Home or Escape resets it. Arrow keys resize it from the keyboard, and the width is remembered on the device. +Drag the line between the session tabs and the header toolbar to resize the toolbar. Dragging it right narrows the toolbar, which then wraps onto a second line instead of squeezing out the tabs, and the usage readings shed their labels, then their provider names, then (in the Compact style) their percentages as it narrows; double-click or Home resets it. Arrow keys resize it from the keyboard, and the width is remembered on the device. diff --git a/docs/wiki/The-Dashboard.md b/docs/wiki/The-Dashboard.md index 764afc94..db23eb42 100644 --- a/docs/wiki/The-Dashboard.md +++ b/docs/wiki/The-Dashboard.md @@ -180,9 +180,9 @@ Hiding System Stats or Plan Usage still hides them in every style. The line between the session tabs and the toolbar is a handle: drag it right to narrow the toolbar (it wraps onto a second line and the usage readings shed their labels, then their provider names, -then their percentages), left to widen it. Arrow keys resize it, and double-click or Home resets it. -It stops at the widest reading, the width is kept per device, and it is not shown on phones, in the -sidebar layout, in the vertical rail or in a solo window. +then, in the Compact style, their percentages), left to widen it. Arrow keys resize it, and +double-click or Home resets it. It stops at the widest reading, the width is kept per device, and it +is not shown on phones, in the sidebar layout, in the vertical rail or in a solo window. New header controls never appear on phones. Phone layout is deliberately minimal and is covered in [Mobile Guide](Mobile-Guide). diff --git a/src/web/public/header-divider.js b/src/web/public/header-divider.js index 33fc5603..0309bf62 100644 --- a/src/web/public/header-divider.js +++ b/src/web/public/header-divider.js @@ -23,8 +23,8 @@ /** * How much of the usage readings to show for a toolbar this wide. The narrower the toolbar, the more of - * the labels go: window labels first, then provider names (shortened), then the percentages (the rings - * stay, and the chip's tooltip keeps the numbers). + * the labels go: window labels first, then provider names (shortened), then, in the Compact style, the + * percentages (the rings stay, and the chip's tooltip keeps the numbers). */ function densityFor(width) { if (width >= 560) return ''; @@ -134,7 +134,7 @@ } function apply(width) { - wanted = width === null ? null : Math.round(width); + wanted = width === null ? null : Math.max(MIN_WIDTH, Math.round(width)); writeStored(wanted); settle(); relayout(); @@ -188,10 +188,11 @@ handle.addEventListener('dblclick', () => apply(null)); handle.addEventListener('keydown', (event) => { - if (event.key === 'Home' || event.key === 'Escape') { + if (event.key === 'Home') { apply(null); } else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { - apply((wanted ?? current()) + (event.key === 'ArrowLeft' ? KEY_STEP : -KEY_STEP)); + // Step from the width on screen, not the request, so a key held past the floor never leaves a dead zone. + apply(current() + (event.key === 'ArrowLeft' ? KEY_STEP : -KEY_STEP)); } else { return; } diff --git a/test/header-divider.browser.test.ts b/test/header-divider.browser.test.ts index 7f12f298..ebf756eb 100644 --- a/test/header-divider.browser.test.ts +++ b/test/header-divider.browser.test.ts @@ -126,6 +126,28 @@ describe('header divider (real Chromium)', () => { await p.close(); }); + it('never stores a width below the minimum, and widens on the first ArrowLeft after a held ArrowRight', async () => { + await open({ chip: true }); + const stored = () => p.evaluate(() => Number(localStorage.getItem('codeman-header-right-width'))); + // a drag into the window's last pixels used to store a negative width, which the next load dropped + await dragTo(-6); + expect(await stored()).toBeGreaterThanOrEqual(160); + await p.locator('.header-divider').dblclick(); + await p.locator('.header-divider').focus(); + for (let i = 0; i < 60; i++) await p.keyboard.press('ArrowRight'); + await settled(); + expect(await stored()).toBeGreaterThanOrEqual(160); + const floor = (await box('#headerRight')).width; + // Escape is the global close-panels key, not a reset + await p.keyboard.press('Escape'); + await settled(); + expect(Math.abs((await box('#headerRight')).width - floor)).toBeLessThan(2); + await p.keyboard.press('ArrowLeft'); + await settled(); + expect((await box('#headerRight')).width).toBeGreaterThan(floor); + await p.close(); + }); + it('stops the divider at the widest reading so nothing pokes out past it', async () => { await open({ chip: true }); await dragTo(20); diff --git a/test/header-divider.test.ts b/test/header-divider.test.ts index 72a988e0..46d154e4 100644 --- a/test/header-divider.test.ts +++ b/test/header-divider.test.ts @@ -69,6 +69,8 @@ describe('header divider', () => { expect(js).toContain('localStorage'); expect(js).not.toContain('/api/settings'); expect(js).toMatch(/key === 'Home'/); + // Escape belongs to the global close-panels handler (app.js), so the handle must not claim it too. + expect(js).not.toMatch(/'Escape'/); expect(js).toContain("addEventListener('dblclick'"); }); });