From deb11bade24e6694c642f3fe6fbfea30739f7073 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Thu, 8 Oct 2026 20:46:51 +0200 Subject: [PATCH] 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) --- src/web/public/styles.css | 7 ++++++ test/tile-grid-chrome.test.ts | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 7dd979dc..ca2b846e 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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 diff --git a/test/tile-grid-chrome.test.ts b/test/tile-grid-chrome.test.ts index 45ffc451..e3e64e8c 100644 --- a/test/tile-grid-chrome.test.ts +++ b/test/tile-grid-chrome.test.ts @@ -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([]); + }); });