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'"); }); });