mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(tabs): never size the state label column from hidden headings
#538's by-state header strip lines its labels up in a column measured into --tab-triage-gutter by _sizeTabTriageGutter(). Under 600px mobile.css hides the headings (the phone row keeps its chips, no labels), but the sizer measured them anyway: every part was 0 wide, yet `width + 5 * (parts - 1)` made each labelled heading 5, so the "nothing to size" guard never tripped, the column became 15px and the group key was cached as if measured. A phone turned to landscape (>= 768px) or a foldable opened (Find N5: folded under 600, unfolded 1124 wide) then crossed into the wrapping strip with no tab render behind it (the resize handler only calls updateTabOverflowMode()), the key had not changed, and "WAITING 2" sat in a 15px column on top of the first tab of its row. Only the wrapping strip reads the column, so the sizer now measures nothing and forgets its key while the strip does not wrap, counts only parts that are laid out (a hidden heading sizes and keys nothing), and updateTabOverflowMode() calls it right after deciding the wrap. Phones and tablets therefore never measure (no layout read per render pass), and every flip into the wrapping strip, the 768px breakpoint included, measures afresh. The 600px breakpoint only matters below 768px, where nothing is measured. Tests in test/tab-triage.test.ts pin that hidden headings size and key nothing, that the real updateTabOverflowMode() measures on the unfold with no render, and that wrapping again re-measures with the counts unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -903,7 +903,7 @@ Tests: `test/terminal-touch-tap.test.ts`.
|
||||
|
||||
**Tab layouts** (`tabArrangement`, per-device, default `state`; Discussion #426): App Settings → Appearance → Tabs → **Tab Layout**. Four values: `state` (option C), `case` (option A), `ledger` (option B) and `classic`, the strip as before. `<html data-tab-arrangement>` is stamped pre-paint and by `applyTabOrientation()`; the render paths read it through `isTabTriage()`, `isTabClusters()` and `isTabLedger()`. Named groups in the vertical rail (owner tab layouts, `_projectTabGroups()` non-null) win over both groupings, and the grouped tree renders as before.
|
||||
|
||||
**By state.** The list is split into four groups: **needs you** (a permission or question dialog; a failed session joins it), **waiting** (idle prompt pending), **working**, **idle** (also ended sessions, an agent that exited inside a live pane, and web tabs). Idle is `quiet` (`TAB_TRIAGE_GROUPS`): its heading element still exists, because it anchors the row's `order` band and, as the lead, holds the row's place beside the brand, but it draws no label or count (`.tab-triage-head--quiet`). `tabStateOrder` (`urgent-first` default, `urgent-last`) decides which end they start from; only the group order flips, never the rows inside a group. The desktop header strip draws a row per group with its label left-aligned in a column that `_sizeTabTriageGutter()` measures to the widest label on screen (re-measured only when the label text changes, and once on `document.fonts.ready`), so no row carries a fixed gutter's worth of empty space. The brand leaves the flow and sits over the strip's top-left corner (`.header:has(...)`), so the first row's heading (`.tab-triage-head--lead`, natural width) is pushed past it by `--tab-triage-brand` (a ResizeObserver keeps that width, so no render pass reads layout for it) and every later row starts at the edge, under "Codeman"; the flat rail and the sidebar draw a section per group; the tablet strip (600-767px) keeps its scrolling row with the headings as inline dividers; phones keep their chip row in group order with the headings hidden (mobile.css). Classification is `_mobileOverviewState()` + `_mobileOverviewExit()`, the home screens' own; the fold into four groups and the order bands are pure in `CodemanTabTriage` (constants.js, `computeTabTriageLayout()`).
|
||||
**By state.** The list is split into four groups: **needs you** (a permission or question dialog; a failed session joins it), **waiting** (idle prompt pending), **working**, **idle** (also ended sessions, an agent that exited inside a live pane, and web tabs). Idle is `quiet` (`TAB_TRIAGE_GROUPS`): its heading element still exists, because it anchors the row's `order` band and, as the lead, holds the row's place beside the brand, but it draws no label or count (`.tab-triage-head--quiet`). `tabStateOrder` (`urgent-first` default, `urgent-last`) decides which end they start from; only the group order flips, never the rows inside a group. The desktop header strip draws a row per group with its label left-aligned in a column that `_sizeTabTriageGutter()` measures to the widest label on screen (re-measured only when the label text changes, when the strip starts wrapping, and once on `document.fonts.ready`), so no row carries a fixed gutter's worth of empty space. The brand leaves the flow and sits over the strip's top-left corner (`.header:has(...)`), so the first row's heading (`.tab-triage-head--lead`, natural width) is pushed past it by `--tab-triage-brand` (a ResizeObserver keeps that width, so no render pass reads layout for it) and every later row starts at the edge, under "Codeman"; the flat rail and the sidebar draw a section per group; the tablet strip (600-767px) keeps its scrolling row with the headings as inline dividers; phones keep their chip row in group order with the headings hidden (mobile.css). ⚠️ Only the wrapping strip reads the label column, so `_sizeTabTriageGutter()` measures nothing (and forgets its key) while the strip does not wrap, skips parts that are not laid out (a hidden heading measures 0 per part, and the 5px gap between label and count used to turn that into a 15px column cached as if measured), and `updateTabOverflowMode()` calls it right after deciding to wrap, because a phone turned to landscape or a foldable opened crosses 768px with no tab render behind it (the resize handler only calls `updateTabOverflowMode()`). Classification is `_mobileOverviewState()` + `_mobileOverviewExit()`, the home screens' own; the fold into four groups and the order bands are pure in `CodemanTabTriage` (constants.js, `computeTabTriageLayout()`).
|
||||
|
||||
⚠️ **By state is the flex `order` property, never a DOM reorder**, exactly like the sorted rail: `#sessionTabs` stays in `sessionOrder`, so the Alt+N badges, drag, the keyboard walk (which sorts by COMPUTED order) and the sidebar filter keep reading the list they always read. Each group owns a band of `TAB_TRIAGE_STRIDE` order values: its heading, its rows, its web tabs, then the row break that ends the header line. The headings and breaks are `aria-hidden` direct children of `#sessionTabs`, reconciled in place by `_syncTabTriageChrome()` after BOTH render paths, because a state change is an incremental pass and can still empty a group or fill a new one. Inside a header row tabs keep TAB order; a sorted rail ranks each section the way it ranks the flat rail. The header strip only wraps into rows on desktop, where `updateTabOverflowMode()` forces `tabs-auto-wrap`; the breaks only display in a wrapping strip, since a `flex-basis: 100%` break in a nowrap scroller would steal width.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user