mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
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:
@@ -601,15 +601,24 @@ body {
|
|||||||
another, and never toggled by the selection, so switching tabs cannot resize
|
another, and never toggled by the selection, so switching tabs cannot resize
|
||||||
the header (and with it the terminal). The bottom padding is the last row's
|
the header (and with it the terminal). The bottom padding is the last row's
|
||||||
gap; a wrapped strip also widens the row gap the routes run in, and opens the
|
gap; a wrapped strip also widens the row gap the routes run in, and opens the
|
||||||
spine channel on the left that joins one row's gap to another's. */
|
spine channel on the left that joins one row's gap to another's.
|
||||||
|
|
||||||
|
⚠ The row gap is ONE variable, --lineage-row-gap, set only here. Every tab
|
||||||
|
arrangement that spaces its rows itself (the ledger grid, the case clusters,
|
||||||
|
the state rows' item margins, near the end of this file) reads it with its
|
||||||
|
own fallback, so the routes get the same 12px gap in every layout instead of
|
||||||
|
losing a specificity contest to whichever rule comes later. */
|
||||||
.session-tabs.lineage-tree {
|
.session-tabs.lineage-tree {
|
||||||
padding-bottom: 14px;
|
--lineage-bottom-room: 14px;
|
||||||
|
padding-bottom: var(--lineage-bottom-room);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tabs.lineage-tree.tabs-two-rows,
|
.session-tabs.lineage-tree.tabs-two-rows,
|
||||||
.session-tabs.lineage-tree.tabs-auto-wrap {
|
.session-tabs.lineage-tree.tabs-auto-wrap {
|
||||||
row-gap: 12px;
|
--lineage-row-gap: 12px;
|
||||||
padding-left: 20px;
|
--lineage-spine-channel: 20px;
|
||||||
|
row-gap: var(--lineage-row-gap);
|
||||||
|
padding-left: var(--lineage-spine-channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tabs::-webkit-scrollbar {
|
.session-tabs::-webkit-scrollbar {
|
||||||
@@ -20718,8 +20727,28 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ⚠ The rows are spaced by a bottom margin on every item, not by row-gap.
|
||||||
|
Each .tab-triage-break is a zero-height flex line of its own, so with a
|
||||||
|
row-gap every group boundary cost TWO gaps (the gap before the break's line
|
||||||
|
and the one after it), and a negative margin cannot win them back: a flex
|
||||||
|
line's cross size is clamped at zero. A margin on the items spaces a group
|
||||||
|
boundary and a group's own continuation line alike, and the break, which
|
||||||
|
carries none, adds nothing. The last row's margin replaces the bottom
|
||||||
|
padding (with lineage, part of its room). The gap is --lineage-row-gap
|
||||||
|
while lineage lines are routed through it (`.session-tabs.lineage-tree`,
|
||||||
|
near the top of this file). */
|
||||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) {
|
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) {
|
||||||
padding-left: var(--tab-triage-gutter, 92px);
|
padding-left: var(--tab-triage-gutter, 92px);
|
||||||
|
padding-bottom: 0;
|
||||||
|
row-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > :not(.tab-triage-break) {
|
||||||
|
margin-bottom: var(--lineage-row-gap, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-tabs-host > .session-tabs.tabs-triage.lineage-tree:is(.tabs-auto-wrap, .tabs-two-rows) {
|
||||||
|
padding-bottom: calc(var(--lineage-bottom-room) - var(--lineage-row-gap));
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-head {
|
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-head {
|
||||||
@@ -20800,16 +20829,17 @@ html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .ta
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Header strip: each cluster is an inline box that wraps inside itself when a
|
/* Header strip: each cluster is an inline box that wraps inside itself when a
|
||||||
case has more tabs than fit on a line. */
|
case has more tabs than fit on a line. Both row gaps open up to
|
||||||
|
--lineage-row-gap while lineage lines are routed through them. */
|
||||||
.session-tabs-host > .session-tabs.tabs-clusters {
|
.session-tabs-host > .session-tabs.tabs-clusters {
|
||||||
gap: 6px;
|
gap: var(--lineage-row-gap, 6px) 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster {
|
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 2px;
|
gap: var(--lineage-row-gap, 2px) 2px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -20886,7 +20916,8 @@ html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .ta
|
|||||||
cell must stretch, or the active cell (taller with its actions) breaks
|
cell must stretch, or the active cell (taller with its actions) breaks
|
||||||
its row. */
|
its row. */
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 4px 6px;
|
/* The row gap opens up while lineage lines run through it. */
|
||||||
|
gap: var(--lineage-row-gap, 4px) 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab {
|
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab {
|
||||||
|
|||||||
@@ -384,6 +384,28 @@ describe('lineage wiring', () => {
|
|||||||
expect(wrapped).toContain('padding-left');
|
expect(wrapped).toContain('padding-left');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the routing row gap in every tab arrangement through one variable', () => {
|
||||||
|
// The ledger grid and the case clusters set their own `gap` at a specificity
|
||||||
|
// that beat the lineage rule, packing the lanes onto the cell borders. Every
|
||||||
|
// arrangement now reads the one variable, which only the lineage rule sets.
|
||||||
|
expect(stylesCss.match(/--lineage-row-gap:/g)).toHaveLength(1);
|
||||||
|
const wrapped = stylesCss.slice(stylesCss.indexOf('.session-tabs.lineage-tree.tabs-auto-wrap {'));
|
||||||
|
expect(wrapped.slice(0, wrapped.indexOf('}'))).toMatch(
|
||||||
|
/--lineage-row-gap: 12px;\s[^}]*row-gap: var\(--lineage-row-gap\);/
|
||||||
|
);
|
||||||
|
expect(stylesCss).toMatch(
|
||||||
|
/\.session-tabs-host > \.session-tabs\.tabs-ledger \{[^}]*gap: var\(--lineage-row-gap, 4px\) 6px;/
|
||||||
|
);
|
||||||
|
expect(stylesCss).toMatch(
|
||||||
|
/\.session-tabs-host > \.session-tabs\.tabs-clusters \{\s*gap: var\(--lineage-row-gap, 6px\) 6px;/
|
||||||
|
);
|
||||||
|
expect(stylesCss).toMatch(
|
||||||
|
/\.session-tabs-host > \.session-tabs\.tabs-clusters > \.tab-cluster \{[^}]*gap: var\(--lineage-row-gap, 2px\) 2px;/
|
||||||
|
);
|
||||||
|
// No arrangement may hard-code a gap the lineage rows depend on.
|
||||||
|
expect(stylesCss).not.toMatch(/\.session-tabs\.tabs-(ledger|clusters) \{[^}]*\bgap: \d+px( \d+px)?;/);
|
||||||
|
});
|
||||||
|
|
||||||
it('syncs the routing room before the wrap is measured, and redraws on selection', () => {
|
it('syncs the routing room before the wrap is measured, and redraws on selection', () => {
|
||||||
const overflow = appJs.slice(appJs.indexOf(' updateTabOverflowMode() {'));
|
const overflow = appJs.slice(appJs.indexOf(' updateTabOverflowMode() {'));
|
||||||
expect(overflow.indexOf('_syncLineageGutter')).toBeGreaterThan(-1);
|
expect(overflow.indexOf('_syncLineageGutter')).toBeGreaterThan(-1);
|
||||||
|
|||||||
+22
-4
@@ -295,7 +295,9 @@ describe('tab grouping in the render paths (app.js)', () => {
|
|||||||
const app = makeApp();
|
const app = makeApp();
|
||||||
app._fullRenderSessionTabs();
|
app._fullRenderSessionTabs();
|
||||||
const lead = () =>
|
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']);
|
expect(lead()).toEqual(['needs']);
|
||||||
document.documentElement.dataset.tabStateOrder = 'urgent-last';
|
document.documentElement.dataset.tabStateOrder = 'urgent-last';
|
||||||
app._renderSessionTabsImmediate();
|
app._renderSessionTabsImmediate();
|
||||||
@@ -424,9 +426,7 @@ describe('tab grouping wiring (static)', () => {
|
|||||||
const mobileCss = read('mobile.css');
|
const mobileCss = read('mobile.css');
|
||||||
|
|
||||||
it('stamps data-tab-arrangement and data-tab-state-order before first paint', () => {
|
it('stamps data-tab-arrangement and data-tab-state-order before first paint', () => {
|
||||||
expect(html).toContain(
|
expect(html).toContain("dataset.tabArrangement=(T==='case'||T==='ledger'||T==='classic')?T:'state'");
|
||||||
"dataset.tabArrangement=(T==='case'||T==='ledger'||T==='classic')?T:'state'"
|
|
||||||
);
|
|
||||||
expect(html).toContain("dataset.tabStateOrder=(A.tabStateOrder==='urgent-last')?'urgent-last':'urgent-first'");
|
expect(html).toContain("dataset.tabStateOrder=(A.tabStateOrder==='urgent-last')?'urgent-last':'urgent-first'");
|
||||||
// The catch branch (localStorage threw) must set both too.
|
// The catch branch (localStorage threw) must set both too.
|
||||||
expect(html).toContain(
|
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', () => {
|
it('lets the rows after the first start under the brand, labels left-aligned', () => {
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.header:has\(> \.session-tabs-host > \.session-tabs\.tabs-triage:is\(\.tabs-auto-wrap, \.tabs-two-rows\)\) > \.header-brand \{\s*position: absolute;/
|
/\.header:has\(> \.session-tabs-host > \.session-tabs\.tabs-triage:is\(\.tabs-auto-wrap, \.tabs-two-rows\)\) > \.header-brand \{\s*position: absolute;/
|
||||||
|
|||||||
Reference in New Issue
Block a user