fix(stammbaum): address re-review nits — opaque rail, stale docs, rail clarity (#692)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m38s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m38s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
- Rail chip background opaque (was /85) so G{n} labels stay AA-legible over
tree content (Leonie).
- Rail effect: replace the reactKey hack with an inputsFinite guard that both
tracks deps and guards NaN; name the fallback-stack magics; correct the stale
'xMidYMid' comment (the CTM mapping is preserveAspectRatio-agnostic) (Felix/Markus).
- GLOSSARY zoom range 0.25–3.0 → 0.25–10; ADR-027 preserveAspectRatio note
xMidYMid → xMinYMin (Elicit traceability).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,21 +22,33 @@ type Chip = { rank: number; label: number; top: number; visible: boolean };
|
||||
let chips = $state<Chip[]>([]);
|
||||
let height = $state(0);
|
||||
|
||||
// Map each generation-row centre from SVG user space to a screen-y via the
|
||||
// live CTM (exact under preserveAspectRatio="xMidYMid meet" — no manual
|
||||
// letterbox math). Runs in an effect so it reads the CTM after the viewBox DOM
|
||||
// update is flushed. `panZoom` and `height` are the recompute triggers.
|
||||
// Fallback stacked positions when no screen transform is available (e.g. an
|
||||
// unsized test container): list the chips top-down rather than hiding them.
|
||||
const FALLBACK_TOP = 24;
|
||||
const FALLBACK_GAP = 28;
|
||||
// Off-screen cull margin (px) so a chip just past an edge isn't popped abruptly.
|
||||
const CULL_MARGIN = 16;
|
||||
|
||||
// Map each generation-row centre from SVG user space to a screen-y via the live
|
||||
// CTM. This reads whatever transform the browser actually computed, so it is
|
||||
// independent of `preserveAspectRatio` (no manual letterbox math). Runs in an
|
||||
// effect — not a $derived — so the CTM is read AFTER the viewBox DOM update is
|
||||
// flushed. NB: this reads layout (getScreenCTM + getBoundingClientRect) once per
|
||||
// pan/zoom/inertia frame; fine for a handful of chips, revisit if rows grow large.
|
||||
$effect(() => {
|
||||
// reactKey makes the effect re-run on pan, zoom and resize; the CTM reflects
|
||||
// the parent's viewBox, which Svelte cannot track on its own.
|
||||
const reactKey = panZoom.x + panZoom.y + panZoom.z + height;
|
||||
const ctm = svg && Number.isFinite(reactKey) ? svg.getScreenCTM() : null;
|
||||
// Reading pan/zoom + height registers them as effect dependencies (so it
|
||||
// re-runs on pan, zoom and resize — the CTM reflects the parent's viewBox,
|
||||
// which Svelte cannot track on its own) and doubles as a NaN guard.
|
||||
const inputsFinite = Number.isFinite(panZoom.x + panZoom.y + panZoom.z + height);
|
||||
const ctm = svg && inputsFinite ? svg.getScreenCTM() : null;
|
||||
const top0 = svg ? svg.getBoundingClientRect().top : 0;
|
||||
// Always emit one chip per labelled row so the labels exist regardless of
|
||||
// transform availability; the CTM only positions them (fallback: stacked).
|
||||
chips = rows.map((row, i) => {
|
||||
const top = ctm ? new DOMPoint(0, row.centerY).matrixTransform(ctm).y - top0 : 24 + i * 28;
|
||||
const visible = !ctm || height <= 0 || (top >= -16 && top <= height + 16);
|
||||
const top = ctm
|
||||
? new DOMPoint(0, row.centerY).matrixTransform(ctm).y - top0
|
||||
: FALLBACK_TOP + i * FALLBACK_GAP;
|
||||
const visible = !ctm || height <= 0 || (top >= -CULL_MARGIN && top <= height + CULL_MARGIN);
|
||||
return { rank: row.rank, label: row.label, top, visible };
|
||||
});
|
||||
});
|
||||
@@ -50,7 +62,7 @@ $effect(() => {
|
||||
<div
|
||||
role="text"
|
||||
aria-label={`Generation ${chip.label}`}
|
||||
class="absolute left-1 -translate-y-1/2 rounded-sm border border-line bg-surface/85 px-1.5 py-0.5 font-serif text-xs text-ink-3 shadow-sm"
|
||||
class="absolute left-1 -translate-y-1/2 rounded-sm border border-line bg-surface px-1.5 py-0.5 font-serif text-xs text-ink-3 shadow-sm"
|
||||
style="top: {chip.top}px"
|
||||
>
|
||||
G{chip.label}
|
||||
|
||||
Reference in New Issue
Block a user