Wave D1 primitives — and the audit says E1/E9 were not Wave 1
PHASE 0 — the order proposed E1 + E9 as Wave-1 and told me to follow the
audit if it disagreed. It disagrees: E9 calibration curve is WAVE D3,
gated on MODEL work ("resolve n>=20 vs N30, accrue buckets"), and E1
movement strip is WAVE D6, a large surface build. E9's gate is live right
now -- calibration is WITHDRAWN at 0 eligible dates, so the curve could
only render its empty state today. Building it would ship a component
whose entire purpose is unavailable.
PHASE 1 — three of the five real Wave D1 items were ALREADY DONE, and the
2026-07-31 audit has aged:
D1 glyph library audit: 38/83 wired (46%)
now: COMPLETE for everything wireable -- 39 of 83
designed glyphs map to a real archetype, all 39
are wired, colours match the registry exactly
(0 disagreements).
A1 card token audit: BUILT-BUT-DRIFTED, "in only 1 file"
now: BUILT-TO-SPEC -- it IS the --bg-1 token,
consumed by 32 files. The audit counted literal
hex, which is what a correctly tokenised value
looks like.
B1 boundary blue audit: PARTIAL, hex in 2 files
now: BUILT -- --priced-out/#8fb2de is a token with a
documented colour law, 4 consumers.
The 44 unwired glyphs are NOT a wiring gap: they have no backend
archetype, so wiring them means inventing 44 archetypes to consume
artwork -- the fabrication this programme refuses. That is the 41-vs-74
scope question and it is Kev's call. Separately, 2 registry archetypes
have NO designed glyph (DUAL THREAT, PAINT BOSS) -- a design gap.
PHASE 2 — what was genuinely absent is now built. web/src/lib/motion.js:
nudge() capped at 180ms so it reads as acknowledgement rather than
latency; bootStagger capped at 240ms because uncapped, row 40 waits 1.1s
and the stagger BECOMES the latency it exists to disguise; rowHover
returns handlers not CSS so touch cannot stick a hover state; and
revealOnIntersect returns an unobserve in every path and reveals
IMMEDIATELY when there is no IntersectionObserver or motion is reduced --
content is never hidden behind a capability check.
Reduced motion is honoured, not softened. The sharpest of the 10 tests:
bootStagger under reduced motion returns opacity 1, not merely delay 0 --
if the CSS animation supplies the opacity, skipping it leaves the row
invisible forever.
PHASE 3 — the reachability guard gains a PRIMITIVES section: a module
built to be embedded must declare its exports AND name its intended
consumers, because a primitive imported by nothing is the same
built-but-unread class as an unmounted component.
WAVE-2 UNGATED: F9-F11 offseason hub, F5 article media, E10/E12 Report,
E1 movement strip. GATED: E9 + E15 on model, E16/F8 on the resolution tail
and the card-system reconciliation, E2/E6 on licensing, E13 on another
order.
Read-only throughout; serving fingerprint unchanged; accrual clock
unchanged at 0 eligible dates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/* ============================================================
|
||||
WAVE D1 — the four motion primitives (E17, E18, E27, E28).
|
||||
All four were ABSENT; the gap audit lists them as self-contained with no
|
||||
dependency, which is why they come before any surface that would embed them.
|
||||
|
||||
Plain CommonJS so .tsx imports it AND the Jest suite requires it directly
|
||||
(the vyndrTokens.js / archetypes.js pattern).
|
||||
|
||||
DESIGN-SPEC PART 4 governs: motion is for PERCEIVED SPEED, never decoration.
|
||||
Every primitive here is skipped outright when the viewer asks for reduced
|
||||
motion -- an accessibility preference is not a style to soften.
|
||||
============================================================ */
|
||||
|
||||
/** Honour the OS-level preference. Server-side, assume reduced (no motion). */
|
||||
function prefersReducedMotion() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return true;
|
||||
try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
/**
|
||||
* E17 — nudge(): the acknowledgement pulse.
|
||||
*
|
||||
* Confirms an action REGISTERED without claiming it finished. Deliberately
|
||||
* short: a long animation reads as latency, which is the opposite of what a
|
||||
* perceived-speed primitive is for.
|
||||
*/
|
||||
const NUDGE_MS = 180;
|
||||
function nudge(el, opts = {}) {
|
||||
if (!el || prefersReducedMotion()) return () => {};
|
||||
const scale = opts.scale ?? 1.03;
|
||||
const prev = el.style.transition;
|
||||
el.style.transition = `transform ${NUDGE_MS}ms cubic-bezier(.2,.8,.2,1)`;
|
||||
el.style.transform = `scale(${scale})`;
|
||||
const t = setTimeout(() => {
|
||||
el.style.transform = '';
|
||||
setTimeout(() => { el.style.transition = prev; }, NUDGE_MS);
|
||||
}, NUDGE_MS);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* E18 — boot stagger: rows arrive in sequence, not all at once.
|
||||
*
|
||||
* The delay is CAPPED. Uncapped, a 40-row board would make the last row wait
|
||||
* 1.2s, and a stagger that makes content late has become the thing it exists to
|
||||
* disguise.
|
||||
*/
|
||||
const STAGGER_STEP_MS = 28;
|
||||
const STAGGER_CAP_MS = 240;
|
||||
function bootStagger(index, opts = {}) {
|
||||
if (prefersReducedMotion()) return { animationDelay: '0ms', opacity: 1 };
|
||||
const step = opts.step ?? STAGGER_STEP_MS;
|
||||
const cap = opts.cap ?? STAGGER_CAP_MS;
|
||||
return { animationDelay: `${Math.min(index * step, cap)}ms` };
|
||||
}
|
||||
|
||||
/**
|
||||
* E27 — row hover: the pointer-only affordance.
|
||||
*
|
||||
* Returned as a props object rather than CSS so a row cannot acquire a hover
|
||||
* state on touch, where there is no hover and the style would stick after a tap.
|
||||
*/
|
||||
function rowHover(opts = {}) {
|
||||
const tint = opts.tint || 'rgba(255,255,255,.028)';
|
||||
return {
|
||||
onMouseEnter: (e) => { if (e.currentTarget) e.currentTarget.style.background = tint; },
|
||||
onMouseLeave: (e) => { if (e.currentTarget) e.currentTarget.style.background = ''; },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* E28 — reveal on intersection.
|
||||
*
|
||||
* Returns an unobserve function; a caller that forgets it leaks an observer per
|
||||
* row. Without IntersectionObserver (or under reduced motion) the element is
|
||||
* shown IMMEDIATELY -- content is never hidden behind a capability check.
|
||||
*/
|
||||
function revealOnIntersect(el, onReveal, opts = {}) {
|
||||
if (!el) return () => {};
|
||||
if (prefersReducedMotion() || typeof IntersectionObserver === 'undefined') {
|
||||
if (typeof onReveal === 'function') onReveal(el);
|
||||
return () => {};
|
||||
}
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) {
|
||||
if (typeof onReveal === 'function') onReveal(e.target);
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
}
|
||||
}, { rootMargin: opts.rootMargin || '0px 0px -10% 0px', threshold: opts.threshold ?? 0.05 });
|
||||
io.observe(el);
|
||||
return () => { try { io.disconnect(); } catch { /* already gone */ } };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nudge, bootStagger, rowHover, revealOnIntersect, prefersReducedMotion,
|
||||
NUDGE_MS, STAGGER_STEP_MS, STAGGER_CAP_MS,
|
||||
};
|
||||
Reference in New Issue
Block a user