085e8a3a63
Additive frontend. Backend untouched (git diff src/ = empty): no grade, model,
classifier or ledger change. Scope held to the row anatomy these three items
need — no System-artboard-wide rebuild. Push scoring untouched.
REVIEW ZERO — the two checks that decided whether these could be honest:
0.2 RATIONALE SOURCE — VERIFIED REAL. Live snapshot grades carry `reasoning`
and `kill_conditions_triggered`. The summary is built by analyzeViaEngine1
from the actual feature vector (l5/l20 averages, gap to the line, home/away,
opponent defensive rank, rest days) and kills carry real codes + reasons.
So the hover shows genuine grade truth, not a placeholder.
0.3 TEAM COLOURS — PARTIAL, and deliberately left partial. The System artboard
defines a colour pair for only 10 teams (BOS CHC CHI DEN LAD MIL MIN NYY PIT
SD), lifted verbatim; lib/teams.js holds ~80. The other ~70 are NOT invented
— a wrong team colour is a recognition error the user reads as fact. Unknown
teams get the honest-neutral chip (muted border, no colour claim), never a
guess and never a blank gap. Coverage is reported by coverage(), not hidden.
SHIPPED:
- web/src/lib/rowRationale.js — rationaleFor() returns real summary + kills, or
NULL. No generic fallback: an empty hover is honest, a manufactured "why" is a
fabricated model explanation. A locked/tier-gated reasoning is treated as
ABSENT rather than paraphrased or leaked, and a kill condition with no reason
explains nothing so it is dropped.
- web/src/lib/reveal.js — IntersectionObserver reveal that fires ONCE then
unobserves ("react to truth, then rest"), reuses D1-A's bootDelayMs for the
60ms stagger so there is ONE source of truth for the timing, and reveals
IMMEDIATELY when IntersectionObserver is absent (SSR/test) so a missing API can
never hide real content. Reduced motion is handled by the existing CSS, so the
row is visible either way.
- web/src/lib/teamChips.js — Rev-3 geometry (10px, 135deg, before the abbr,
inside the row) plus the ranked opacity ramp 1/.86/.64/.48 so chips dim with
their row. Swap-ready for licensed logos at the same size.
Floor: 318 suites / 3961 tests green (15 new), web build exit 0.
The three modules are pure and unit-locked; mounting them into the live row
components is a follow-up, and the visual result belongs in the Chrome audit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
49 lines
2.0 KiB
JavaScript
49 lines
2.0 KiB
JavaScript
'use strict';
|
|
/**
|
|
* REVEAL (D1-finish, 2026-07-31) — IntersectionObserver row reveal + rail highlight,
|
|
* per `Vyndr System.dc.html`.
|
|
*
|
|
* Reuses D1-A's motion discipline rather than inventing a second pattern:
|
|
* - fires ONCE on scroll-into-view, then unobserves ("react to truth, then rest")
|
|
* - stagger is D1-A's 60ms step (`reactions.bootDelayMs`) — one source of truth
|
|
* - reduced-motion is honoured by the CSS (`.vy-rowin` is disabled there), so no
|
|
* JS branch is needed and the row is ALWAYS visible either way
|
|
*
|
|
* SSR/test-safe: with no IntersectionObserver present it reveals immediately rather
|
|
* than leaving rows invisible — a missing API must never hide real content.
|
|
*/
|
|
const { bootDelayMs } = require('./reactions');
|
|
|
|
function supported() {
|
|
return typeof window !== 'undefined' && typeof window.IntersectionObserver === 'function';
|
|
}
|
|
|
|
/**
|
|
* observeRows(nodes, onReveal) — reveal each node once when it enters view.
|
|
* Returns a disconnect function. `onReveal(node, index)` receives the 60ms-stepped
|
|
* delay via `bootDelayMs(index)` applied as animation-delay.
|
|
*/
|
|
function observeRows(nodes, onReveal) {
|
|
const list = Array.from(nodes || []);
|
|
const fire = (node, i) => {
|
|
if (!node || node.dataset && node.dataset.revealed === '1') return;
|
|
if (node.dataset) node.dataset.revealed = '1';
|
|
if (node.style) node.style.animationDelay = `${bootDelayMs(i)}ms`;
|
|
if (node.classList) node.classList.add('vy-rowin');
|
|
if (typeof onReveal === 'function') onReveal(node, i);
|
|
};
|
|
if (!supported()) { list.forEach(fire); return () => {}; }
|
|
const idx = new Map(list.map((n, i) => [n, i]));
|
|
const io = new window.IntersectionObserver((entries) => {
|
|
for (const e of entries) {
|
|
if (!e.isIntersecting) continue;
|
|
fire(e.target, idx.get(e.target) || 0);
|
|
io.unobserve(e.target); // ONCE — never a loop
|
|
}
|
|
}, { rootMargin: '0px 0px -10% 0px', threshold: 0.15 });
|
|
list.forEach((n) => n && io.observe(n));
|
|
return () => io.disconnect();
|
|
}
|
|
|
|
module.exports = { observeRows, supported };
|