refactor(timeline): single-source the spine X position via --spine-x
All checks were successful
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m9s
SDD Gate / RTM Check (pull_request) Successful in 18s
SDD Gate / Contract Validate (pull_request) Successful in 25s
SDD Gate / Constitution Impact (pull_request) Successful in 19s
CI / Unit & Component Tests (push) Successful in 4m33s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m19s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 26s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
CI / Unit & Component Tests (pull_request) Successful in 4m24s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m21s
All checks were successful
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m9s
SDD Gate / RTM Check (pull_request) Successful in 18s
SDD Gate / Contract Validate (pull_request) Successful in 25s
SDD Gate / Constitution Impact (pull_request) Successful in 19s
CI / Unit & Component Tests (push) Successful in 4m33s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m19s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 26s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
CI / Unit & Component Tests (pull_request) Successful in 4m24s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m21s
The spine offset (0.5rem phone / 50% desktop) was hard-coded in both TimelineView's .timeline-axis::before and YearBand's .year-node/.letter-dot, kept in sync only by a comment. Declare --spine-x once on .timeline-axis and have the markers consume it by inheritance, so a change to the spine position moves the markers with it. Add a test that the year-node tracks the token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #836.
This commit is contained in:
@@ -92,8 +92,12 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
|
|||||||
<style>
|
<style>
|
||||||
/* Establish a stacking context so the spine (z-index: -1) sits behind the
|
/* 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
|
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 {
|
.timeline-axis {
|
||||||
|
--spine-x: 0.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@@ -106,7 +110,7 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0.5rem;
|
left: var(--spine-x);
|
||||||
width: 2px;
|
width: 2px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
/* Three-stop life-thread: mint → navy → slate. Slate lives only as
|
/* 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) {
|
@media (min-width: 1024px) {
|
||||||
|
.timeline-axis {
|
||||||
|
--spine-x: 50%;
|
||||||
|
}
|
||||||
.timeline-axis::before {
|
.timeline-axis::before {
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,10 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spine geometry below mirrors TimelineView's .timeline-axis::before
|
/* Markers ride on the spine's single source of truth: --spine-x, declared
|
||||||
(left: 0.5rem phone / left: 50% desktop) — keep the two in sync so the
|
once on TimelineView's .timeline-axis and inherited here (0.5rem phone /
|
||||||
markers never desync from the spine. #833 REQ-003/004/005. */
|
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.
|
/* 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
|
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 {
|
.year-node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0.5rem;
|
left: var(--spine-x, 0.5rem);
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
@@ -121,7 +122,7 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
.letter-dot {
|
.letter-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.9rem;
|
top: 0.9rem;
|
||||||
left: 0.5rem;
|
left: var(--spine-x, 0.5rem);
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
@@ -140,9 +141,8 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
.year-node {
|
/* .year-node needs no desktop override — it inherits --spine-x: 50% from
|
||||||
left: 50%;
|
the axis. */
|
||||||
}
|
|
||||||
.letter-row {
|
.letter-row {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|||||||
@@ -145,4 +145,23 @@ describe('YearBand', () => {
|
|||||||
card.getBoundingClientRect().left + 0.5
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user