fix(tabs): one lineage row gap for every tab arrangement, no double gap at state breaks

#544 reserves a 12px row gap for the lineage routes on
`.session-tabs.lineage-tree.tabs-auto-wrap`, and #538's arrangements set
their own row spacing at an equal or higher specificity later in the file:

- The ledger grid's `gap: 4px 6px` and the case strip's `gap: 6px` won, so
  the lanes packed onto the cell borders (y 39/40/41 in a 4px gap).
- In the state rows every `.tab-triage-break` is a zero-height flex line of
  its own, so each group boundary cost two row gaps: rows 54px apart instead
  of 42, and the header 12px taller per group (including a trailing one after
  the last group). A negative margin on the break cannot cancel it, because a
  flex line's cross size is clamped at zero (measured in Chromium).

The lineage row gap is now one custom property, `--lineage-row-gap`, set only
by the lineage rule. The ledger and both cluster gaps read it with their own
fallback, and the wrapped state strip spaces its rows with a bottom margin on
every item except the breaks (row-gap 0), its last row's margin replacing the
bottom padding. Rows are now one gap apart in every case, with lineage and
without (4px then, where a group boundary used to be 8px).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-08 21:36:04 +02:00
parent 3904428a4f
commit f5acf19a11
3 changed files with 83 additions and 12 deletions
+22 -4
View File
@@ -295,7 +295,9 @@ describe('tab grouping in the render paths (app.js)', () => {
const app = makeApp();
app._fullRenderSessionTabs();
const lead = () =>
[...container().querySelectorAll<HTMLElement>(':scope > .tab-triage-head--lead')].map((h) => h.dataset.triageGroup);
[...container().querySelectorAll<HTMLElement>(':scope > .tab-triage-head--lead')].map(
(h) => h.dataset.triageGroup
);
expect(lead()).toEqual(['needs']);
document.documentElement.dataset.tabStateOrder = 'urgent-last';
app._renderSessionTabsImmediate();
@@ -424,9 +426,7 @@ describe('tab grouping wiring (static)', () => {
const mobileCss = read('mobile.css');
it('stamps data-tab-arrangement and data-tab-state-order before first paint', () => {
expect(html).toContain(
"dataset.tabArrangement=(T==='case'||T==='ledger'||T==='classic')?T:'state'"
);
expect(html).toContain("dataset.tabArrangement=(T==='case'||T==='ledger'||T==='classic')?T:'state'");
expect(html).toContain("dataset.tabStateOrder=(A.tabStateOrder==='urgent-last')?'urgent-last':'urgent-first'");
// The catch branch (localStorage threw) must set both too.
expect(html).toContain(
@@ -450,6 +450,24 @@ describe('tab grouping wiring (static)', () => {
);
});
it('spaces the rows with item margins, so a row break costs no second gap', () => {
// Each break is a zero-height flex line of its own. With a row-gap, a group
// boundary cost two gaps (12px each with lineage lines, 54px row pitch
// instead of 42), and a negative margin on the break cannot win one back,
// because a flex line's cross size is clamped at zero.
const strip = css.indexOf('.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) {');
const block = css.slice(strip, css.indexOf('}', strip));
expect(block).toContain('row-gap: 0;');
expect(block).toContain('padding-bottom: 0;');
expect(css).toMatch(
/\.session-tabs\.tabs-triage:is\(\.tabs-auto-wrap, \.tabs-two-rows\) > :not\(\.tab-triage-break\) \{\s*margin-bottom: var\(--lineage-row-gap, 4px\);/
);
// With lineage the last row's margin is part of the bottom routing room.
expect(css).toMatch(
/\.session-tabs\.tabs-triage\.lineage-tree:is\(\.tabs-auto-wrap, \.tabs-two-rows\) \{[^}]*padding-bottom: calc\(var\(--lineage-bottom-room\) - var\(--lineage-row-gap\)\);/
);
});
it('lets the rows after the first start under the brand, labels left-aligned', () => {
expect(css).toMatch(
/\.header:has\(> \.session-tabs-host > \.session-tabs\.tabs-triage:is\(\.tabs-auto-wrap, \.tabs-two-rows\)\) > \.header-brand \{\s*position: absolute;/