fix(tabs): keep the in-tiles tab marker in the ledger layout

#561 marks the tab of every tiled session with an inset underline
(`.session-tab.in-tiles:not(.active) { box-shadow: inset 0 -2px 0 ... }`,
0,3,0). #538's ledger draws each cell's status bar as an inset box-shadow
too (`.session-tabs-host > .session-tabs.tabs-ledger > .session-tab`,
0,4,0), so in the ledger the bar replaced the marker and nothing in the
strip said which sessions were on the grid.

The ledger now restates the marker beside its bar for a tiled, inactive
cell; the bar still follows --ledger-bar, so needs-you, waiting and exited
cells keep their colours. By state, by case, classic and the side rail
paint no cell shadow of their own and already showed it. The test scans
every arrangement-scoped rule that paints a tab cell's shadow and requires
an .in-tiles variant that keeps both, so a new arrangement cannot drop it
again.

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 bd3c368512
commit deb11bade2
2 changed files with 50 additions and 0 deletions
+7
View File
@@ -21005,6 +21005,13 @@ html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .ta
--ledger-bar: var(--text-muted);
}
/* A tiled session's cell keeps the tile grid's underline (.in-tiles) beside
its status bar: both are inset shadows, and the bar's rule above would
otherwise replace the marker. */
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab.in-tiles:not(.active) {
box-shadow: inset 3px 0 0 var(--ledger-bar), inset 0 -2px 0 color-mix(in srgb, var(--accent, #4a9eff) 60%, transparent);
}
/* The active cell has to win against a grid of look-alike cells. The skin's
active rule (1px accent border, no shadow, both !important) is too quiet
here, so an inset second pixel doubles the border without resizing the
+43
View File
@@ -22,6 +22,7 @@
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import postcss, { type AtRule } from 'postcss';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
FakeEl,
@@ -451,4 +452,46 @@ describe('the tab marker', () => {
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
expect(css).toMatch(/\.session-tab\.in-tiles/);
});
it('every tab arrangement that draws its own cell shadow keeps the marker in it', () => {
// The marker is an inset box-shadow, so an arrangement that paints its
// cells with a box-shadow of its own (the ledger's status bar, #538)
// replaces it unless it restates the marker alongside its own shadow.
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
type Found = { selector: string; shadow: string; media: string };
const shadows: Found[] = [];
postcss.parse(css).walkRules((rule) => {
let shadow = '';
rule.walkDecls('box-shadow', (d) => {
shadow = d.value;
});
if (!shadow) return;
const media: string[] = [];
for (let p = rule.parent; p && p.type !== 'root'; p = p.parent) {
if (p.type === 'atrule') media.push((p as AtRule).params);
}
for (const selector of rule.selectors) shadows.push({ selector, shadow, media: media.join(' ') });
});
const marker = shadows.find((r) => r.selector === '.session-tab.in-tiles:not(.active)');
expect(marker?.shadow).toMatch(/^inset 0 -2px 0 /);
// A whole tab cell at rest, scoped to an arrangement: not a pseudo-element,
// not a passing state (hover, press, drag) and not the active tab, which
// the marker skips anyway.
const cells = shadows.filter((r) => {
const last = r.selector.split(/\s*[\s>+~]\s*/).pop()!;
return (
/^\.session-tab(?![\w-])/.test(last) &&
r.selector !== last &&
!/::|:hover|:active|\.active(?![\w-])|drag-over|in-tiles/.test(last.replace(':not(.active)', ''))
);
});
expect(cells.map((r) => r.selector)).toContain('.session-tabs-host > .session-tabs.tabs-ledger > .session-tab');
const missing = cells.filter((cell) => {
const tiled = shadows.find(
(r) => r.selector === `${cell.selector}.in-tiles:not(.active)` && r.media === cell.media
);
return !tiled || !tiled.shadow.includes(cell.shadow) || !tiled.shadow.includes(marker!.shadow);
});
expect(missing.map((r) => r.selector)).toEqual([]);
});
});