Restore /zeitstrahl Datum-mode visual fidelity to the Concept-A spec (#833) #836

Merged
marcel merged 23 commits from feat/issue-833-zeitstrahl-fidelity into main 2026-06-14 14:12:42 +02:00
3 changed files with 36 additions and 11 deletions
Showing only changes of commit 239565ea20 - Show all commits

View File

@@ -92,8 +92,12 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
<style>
/* Establish a stacking context so the spine (z-index: -1) sits behind the
in-flow cards/pills/strips but still in front of the canvas — the line is
always background; the badges, dots and markers ride on top of it. */
always background; the badges, dots and markers ride on top of it.
--spine-x is the single source of truth for the spine's X position; the
year-node and connector dots in YearBand consume it via inheritance so the
markers can never desync from the line. */
.timeline-axis {
--spine-x: 0.5rem;
position: relative;
z-index: 0;
}
@@ -106,7 +110,7 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
position: absolute;
top: 0;
bottom: 0;
left: 0.5rem;
left: var(--spine-x);
width: 2px;
z-index: -1;
/* Three-stop life-thread: mint → navy → slate. Slate lives only as
@@ -115,8 +119,10 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
}
@media (min-width: 1024px) {
.timeline-axis {
--spine-x: 50%;
}
.timeline-axis::before {
left: 50%;
transform: translateX(-50%);
}
}

View File

@@ -85,9 +85,10 @@ const rows = $derived.by<Row[]>(() => {
z-index: 1;
}
/* Spine geometry below mirrors TimelineView's .timeline-axis::before
(left: 0.5rem phone / left: 50% desktop) — keep the two in sync so the
markers never desync from the spine. #833 REQ-003/004/005. */
/* Markers ride on the spine's single source of truth: --spine-x, declared
once on TimelineView's .timeline-axis and inherited here (0.5rem phone /
50% desktop). The 0.5rem fallback only applies when a YearBand is rendered
outside the axis (e.g. component tests). #833 REQ-003/004/005. */
/* Phone (< 1024px): badge sits at the left spine, clearing the node marker.
The badge sits above the node (z-index) so on desktop, where the centered
@@ -105,7 +106,7 @@ const rows = $derived.by<Row[]>(() => {
.year-node {
position: absolute;
top: 50%;
left: 0.5rem;
left: var(--spine-x, 0.5rem);
width: 11px;
height: 11px;
border-radius: 9999px;
@@ -121,7 +122,7 @@ const rows = $derived.by<Row[]>(() => {
.letter-dot {
position: absolute;
top: 0.9rem;
left: 0.5rem;
left: var(--spine-x, 0.5rem);
width: 13px;
height: 13px;
border-radius: 9999px;
@@ -140,9 +141,8 @@ const rows = $derived.by<Row[]>(() => {
margin-left: auto;
margin-right: auto;
}
.year-node {
left: 50%;
}
/* .year-node needs no desktop override — it inherits --spine-x: 50% from
the axis. */
.letter-row {
width: 50%;
padding-left: 0;

View File

@@ -145,4 +145,23 @@ describe('YearBand', () => {
card.getBoundingClientRect().left + 0.5
);
});
it('positions the year-node from the inherited --spine-x token (REQ-003/004)', async () => {
await page.viewport(375, 800);
// The spine X lives once on .timeline-axis as --spine-x; the markers must
// track that token, not a hard-coded offset, so they never desync.
document.documentElement.style.setProperty('--spine-x', '3rem');
try {
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const node = document.querySelector('[data-testid="year-node"]') as HTMLElement;
const section = document.querySelector('section') as HTMLElement;
const n = node.getBoundingClientRect();
const s = section.getBoundingClientRect();
const nodeCenter = n.left + n.width / 2;
// --spine-x:3rem = 48px from the band's left edge
expect(Math.abs(nodeCenter - s.left - 48)).toBeLessThan(2);
} finally {
document.documentElement.style.removeProperty('--spine-x');
}
});
});