fix(tabs): route the lineage spine between the state labels and the tabs

With tabArrangement 'state' (the default) and lineage lines on (the desktop
default), the spine ran straight through the WORKING / WAITING / NEEDS YOU
label of every row it passed. #538's
`.session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows)` pads the
strip by the label gutter and pulls each label to the strip's edge with a
negative margin; it outranks #544's `.lineage-tree.tabs-auto-wrap`
`padding-left: 20px`, so the spine channel disappeared, and the spine,
anchored at the strip's left edge (computeLineageTree), sat in the label
column.

The channel now opens BETWEEN the label column and the tabs: with lineage,
the state strip pads by gutter + --lineage-spine-channel, every label keeps
its column at the edge (still under the brand) and hands the channel back
after itself, and the lead label still starts right after the brand.
computeLineageTree takes the channel's left edge as `spineLeft`, which
session-lineage.js reads back from the padding the CSS laid out (content edge
minus the channel), so the two cannot drift; without it, or where the channel
is the padding itself (classic, ledger, case), the spine stays at the strip's
edge exactly as before. The spine's clamp now only considers tabs in rows the
spine can run beside (every row after the first), so a first row that starts
left of the channel (a quiet idle group right after the brand) no longer
drags the spine back over the labels.

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 f5acf19a11
commit 382d7dd406
5 changed files with 221 additions and 13 deletions
+30
View File
@@ -468,6 +468,36 @@ describe('tab grouping wiring (static)', () => {
);
});
it('opens the lineage spine channel between the label column and the tabs', () => {
// The gutter padding used to replace the lineage channel, so the spine ran
// through every label. With lineage lines, the strip pads by both, every label
// keeps its column at the edge and gives the channel back after itself, and
// the first row's label still starts right after the brand.
const lineage = '.session-tabs-host > .session-tabs.tabs-triage.lineage-tree:is(.tabs-auto-wrap, .tabs-two-rows)';
const rule = (selector: string) => {
const start = css.indexOf(`${selector} {`);
expect(start, selector).toBeGreaterThan(-1);
return css.slice(start, css.indexOf('}', start));
};
expect(rule(lineage)).toContain(
'padding-left: calc(var(--tab-triage-gutter, 92px) + var(--lineage-spine-channel));'
);
const head = rule(`${lineage} > .tab-triage-head`);
expect(head).toContain('margin-left: calc(-1 * (var(--tab-triage-gutter, 92px) + var(--lineage-spine-channel)));');
expect(head).toContain('margin-right: var(--lineage-spine-channel);');
const lead = rule(`${lineage} > .tab-triage-head--lead`);
expect(lead).toContain(
'margin-left: calc(var(--tab-triage-brand, 100px) - var(--tab-triage-gutter, 92px) - var(--lineage-spine-channel));'
);
expect(lead).toContain('margin-right: 0;');
// The lead rule must come after the head rule it overrides (equal specificity).
expect(css.indexOf(`${lineage} > .tab-triage-head--lead {`)).toBeGreaterThan(
css.indexOf(`${lineage} > .tab-triage-head {`)
);
// session-lineage.js reads the channel's position back from that padding.
expect(read('session-lineage.js')).toContain("getPropertyValue('--lineage-spine-channel')");
});
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;/