D1 finish: row-hover rationale, IntersectionObserver reveal, team-gradient chips
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
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
/**
|
||||
* TEAM-GRADIENT CHIPS (D1-finish, 2026-07-31) — Rev-3 pattern from HANDOFF.md:
|
||||
* 10-12px chip, 135deg two-colour gradient, placed BEFORE each team abbr and
|
||||
* INSIDE the row, so the ranked opacity ramp (1 / .86 / .64 / .48) dims it too.
|
||||
* Swap for a licensed logo <img> at the same size when assets land.
|
||||
*
|
||||
* 🔴 COVERAGE IS PARTIAL AND THAT IS DELIBERATE. The System artboard defines a
|
||||
* colour pair for only 10 teams; the registry (`lib/teams.js`) holds ~80. We do
|
||||
* NOT invent the other ~70 — a wrong team colour is a recognition error the user
|
||||
* reads as fact. Unknown teams render the HONEST-NEUTRAL chip (a muted border,
|
||||
* no colour claim), never a guessed hue and never a blank gap.
|
||||
*
|
||||
* Pairs below are lifted VERBATIM from `Vyndr System.dc.html` — no eyedropping,
|
||||
* no approximation.
|
||||
*/
|
||||
|
||||
/** abbr -> [c1, c2], exactly as the artboard defines them. */
|
||||
const TEAM_GRADIENTS = Object.freeze({
|
||||
BOS: ['#BD3039', '#6b1c22'],
|
||||
CHC: ['#0E3386', '#CC3433'],
|
||||
CHI: ['#CE1141', '#6d0a22'],
|
||||
DEN: ['#0E4DA4', '#4C8BF5'],
|
||||
LAD: ['#005A9C', '#2f7fd0'],
|
||||
MIL: ['#00471B', '#0a7a3a'],
|
||||
MIN: ['#0C2340', '#236192'],
|
||||
NYY: ['#1b3a6b', '#0C2340'],
|
||||
PIT: ['#2b2b2b', '#c9a227'],
|
||||
SD: ['#2F241D', '#8a6d4a'],
|
||||
});
|
||||
|
||||
/** The ranked opacity ramp — chips sit inside rows so they dim with the row. */
|
||||
const RANK_OPACITY = Object.freeze([1, 0.86, 0.64, 0.48]);
|
||||
|
||||
/** Opacity for rank index i (clamped to the ramp's tail). */
|
||||
function rankOpacity(i) {
|
||||
const n = Number(i);
|
||||
if (!Number.isFinite(n) || n < 0) return RANK_OPACITY[0];
|
||||
return RANK_OPACITY[Math.min(Math.floor(n), RANK_OPACITY.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* chipStyle(abbr) — inline style for the chip, or the honest-neutral style when
|
||||
* the team has no designed colour. Returns `{ style, known }` so a caller can
|
||||
* tell "we know this team" from "we are not claiming to".
|
||||
*/
|
||||
function chipStyle(abbr) {
|
||||
const key = String(abbr || '').trim().toUpperCase();
|
||||
const pair = TEAM_GRADIENTS[key];
|
||||
const base = {
|
||||
display: 'inline-block', width: 10, height: 10, borderRadius: 3,
|
||||
verticalAlign: -1.5, marginRight: 3,
|
||||
};
|
||||
if (!pair) {
|
||||
// HONEST-NEUTRAL: a shape, not a colour claim.
|
||||
return { known: false, style: { ...base, background: 'transparent', border: '1px solid var(--border-hi)' } };
|
||||
}
|
||||
return { known: true, style: { ...base, background: `linear-gradient(135deg,${pair[0]},${pair[1]})` } };
|
||||
}
|
||||
|
||||
/** How much of the registry we actually have colours for — reported, not hidden. */
|
||||
function coverage(totalTeams) {
|
||||
const known = Object.keys(TEAM_GRADIENTS).length;
|
||||
const total = Number(totalTeams);
|
||||
return { known, total: Number.isFinite(total) ? total : null };
|
||||
}
|
||||
|
||||
module.exports = { TEAM_GRADIENTS, RANK_OPACITY, rankOpacity, chipStyle, coverage };
|
||||
Reference in New Issue
Block a user