From a10227ee5a22cc4df419f90f63e479c0f3aef439 Mon Sep 17 00:00:00 2001 From: Kev Date: Thu, 16 Jul 2026 05:09:14 -0400 Subject: [PATCH] =?UTF-8?q?Design=20completion=20train=20=E2=80=94=20impor?= =?UTF-8?q?t=20authoritative=20mockup=20+=20M1a=20mobile=20foundation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SETUP (required first step): overwrote specs/design-reference/ with the CURRENT authoritative mockup ("Vyndr System.dc.html" from claude.ai/design, the version with the combat card / pitcher identity / live grade-shift / correlation builder / FREE|PRO pricing / TRANSMISSION QUIET). 12 surfaces. Confirmed it has NO media queries — desktop-only, so the 390px expression is a deliberate design decision, not a shrink. M1a — mobile foundation (built to spec, VISUALLY UNVERIFIED at 390px; WSL2↔Chrome unreachable, the Chrome audit is the eyes): - Header-zone collapse (the concrete audit finding: ticker + SIGNAL LIVE + STALE stacking in ~110px). The decorative EKG is dropped <768px so the heartbeat reads as ONE clean status line; signal pulse + ticker are the single animated element (DESIGN-SPEC §4). LiveLayer gains .heartbeat-bar / .hb-ekg hooks. - Primary CTA (.vbtn) meets the 44px touch target on mobile; dense `small` buttons opt out (density is a feature). - .m-hero mono-hero clamp for the one-figure-per-card grammar at 390px. - M4 test-lock: vyndrParityQA asserts the header collapse, tap target, overflow containment, and the honest "UNVERIFIED at 390px" label are all in source. Per-surface stacking (M1b), billboards (M2), desktop parity (M3) continue. Co-Authored-By: Claude Opus 4.8 (1M context) --- specs/design-reference/vyndr-system.html | 157 +++++++++++++++++++++-- tests/unit/vyndrParityQA.test.js | 31 +++++ web/src/app/globals.css | 45 +++++++ web/src/components/vyndr/LiveLayer.tsx | 8 +- web/src/components/vyndr/VBtn.tsx | 3 + 5 files changed, 228 insertions(+), 16 deletions(-) diff --git a/specs/design-reference/vyndr-system.html b/specs/design-reference/vyndr-system.html index 182511d..d53e2ca 100644 --- a/specs/design-reference/vyndr-system.html +++ b/specs/design-reference/vyndr-system.html @@ -34,6 +34,18 @@ @keyframes vy-eventin{0%{opacity:0;transform:translateY(-3px)}12%,80%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-3px)}} @keyframes vy-cmdin{0%{opacity:0;transform:translateY(-10px) scale(.98)}100%{opacity:1;transform:none}} @keyframes vy-detailin{0%{opacity:0;transform:translateY(-2px)}100%{opacity:1;transform:none}} + @keyframes vy-wirein{0%{opacity:0;transform:translateY(4px)}8%,86%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-3px)}} + @keyframes vy-cursor{0%,49%{opacity:1}50%,100%{opacity:0}} + @keyframes vy-markflick{0%,88%,100%{opacity:1;filter:none}89%{opacity:.55;filter:drop-shadow(1.5px 0 rgba(255,71,87,.8)) drop-shadow(-1.5px 0 rgba(0,212,160,.8))}91%{opacity:1;filter:none}94%{opacity:.7;filter:drop-shadow(-1px 0 rgba(255,71,87,.7))}95%{opacity:1;filter:none}} + [data-reveal]{opacity:0;transform:translateY(16px);transition:opacity .6s cubic-bezier(.2,.8,.2,1),transform .6s cubic-bezier(.2,.8,.2,1)} + [data-reveal].vy-in{opacity:1;transform:none} + html{scroll-behavior:smooth} + .vy-navdot{display:flex;align-items:center;gap:9px;text-decoration:none;padding:3px 0} + .vy-navdot span.d{width:6px;height:6px;border-radius:2px;background:#2A2A38;transition:background .2s,box-shadow .2s} + .vy-navdot span.l{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:.16em;color:#4a4a58;opacity:0;transform:translateX(-4px);transition:opacity .2s,transform .2s,color .2s} + .vy-navdot:hover span.l{opacity:1;transform:none} + .vy-navdot.on span.d{background:#00D4A0;box-shadow:0 0 8px rgba(0,212,160,.6)} + .vy-navdot.on span.l{opacity:1;transform:none;color:#00D4A0} .rx-up{animation:vy-rxup .75s ease-out} .rx-down{animation:vy-rxdown .75s ease-out} .vy-scanwrap{position:absolute;inset:0;overflow:hidden;pointer-events:none;border-radius:inherit} @@ -44,6 +56,24 @@ + + + + +
+ THE WIRE + + --:--:-- + Session open — 312 props graded across 47 games. + LOG ▸ +
+
@@ -110,9 +140,22 @@
-
VYNDR
-
-
SPORTS INTELLIGENCE TERMINAL
+ +
+ + + + + + +
+
+
+ VYNDR + +
+ SPORTS INTELLIGENCE TERMINAL +
@@ -143,13 +186,13 @@
-
VISUAL SYSTEM · v2
+
01 · THE TERMINAL · VISUAL SYSTEM v2

The instrument, not the sportsbook.

Dense but ranked. Mostly still, then it reacts — a number pulses the instant a line moves, a card breathes when its game goes live. One hero per screen.

-
+
TONIGHT'S SLATE @@ -270,6 +313,19 @@ opened 25.5
+ +
+ + + + + +
+ OPEN 25.5 · 9:02 AM + STEAM 4:40 PM ▲ + NOW 27.5 +
+
@@ -322,8 +378,13 @@
- -
+ +
+ 02 +
THE SYSTEM
Five components + 74 marks. Every other page is assembled from these.
+
+ +
COMPONENT SYSTEM THE PARTS EVERY SURFACE INHERITS @@ -467,7 +528,7 @@
-
+
ARCHETYPE GLYPHS FULL SYSTEM · 74 UNIQUE MARKS · 8 SPORTS @@ -525,7 +586,13 @@
-
+ +
+ 03 +
THE SURFACES
What gets posted. The timeline is customer #1 — these stop a scroll.
+
+ +
STREAKS A BLOOMBERG ALERT, NOT A LOG LINE @@ -719,7 +786,10 @@
- VYNDR + + + VYNDR + JUL 11
@@ -761,7 +831,10 @@
- VYNDR + + + VYNDR + CLV-VERIFIED
@@ -787,7 +860,13 @@
-
+ +
+ 04 +
THE INTELLIGENCE
One analysis, many grades — style matchups, arsenal reads, correlation math.
+
+ +
STYLE MATCHUP COMBAT · STYLES MAKE FIGHTS · FIGHT-WEEK WEAPON @@ -1062,7 +1141,7 @@
-
+
COMPONENTS · 74 GLYPHS · EDGE BOARD · /u · SHARE · COMBAT · LIVE GRADE · PRO · STATES VYNDR VISUAL SYSTEM · v2
@@ -1239,6 +1318,30 @@ class Component extends DCLogic { clock(){ const d=new Date(); const p=n=>String(n).padStart(2,'0'); return p(d.getMinutes())+':'+p(d.getSeconds()); } + setupScroll(){ + if(!this.rootNode) return; + // arrival: sections + act dividers boot in once when reached + const sections=[...this.rootNode.querySelectorAll('[data-screen-label],[data-reveal]')]; + sections.forEach(s=>{ if(!s.hasAttribute('data-reveal')) s.setAttribute('data-reveal',''); }); + this._io=new IntersectionObserver((ents)=>{ + ents.forEach(en=>{ if(en.isIntersecting){ en.target.classList.add('vy-in'); this._io.unobserve(en.target); } }); + },{threshold:.08}); + sections.forEach(s=>this._io.observe(s)); + // safety: anything above the fold reveals immediately + setTimeout(()=>{ sections.forEach(s=>{ if(s.getBoundingClientRect().top{ + ents.forEach(en=>{ + if(!en.isIntersecting) return; + const id=en.target.id; + this.rootNode.querySelectorAll('.vy-navdot').forEach(a=>a.classList.toggle('on', a.getAttribute('data-nav')===id)); + }); + },{rootMargin:'-30% 0px -55% 0px'}); + map.forEach(id=>{ const el=this.rootNode.querySelector('#'+id); if(el) this._so.observe(el); }); + } + setupParlay(){ if(!this.parlayLegs) return; const legs=[...this.parlayLegs.querySelectorAll('[data-leg]')]; @@ -1445,6 +1548,7 @@ class Component extends DCLogic { this.buildGradeHistory(); this.setupBoardHover(); this.setupParlay(); + this.setupScroll(); this._key=(e)=>{ const k=(e.key||'').toLowerCase(); if((e.metaKey||e.ctrlKey) && k==='k'){ e.preventDefault(); if(this.state.cmdOpen){ this.setState({cmdOpen:false}); } else { this.openCmdBar(); } } @@ -1452,6 +1556,30 @@ class Component extends DCLogic { }; document.addEventListener('keydown',this._key); + // THE WIRE — session log: one entry, holds, then the next + this._wireEntries=[ + ['STEAM','Jokić o27.5 PTS — line 25.5 → 27.5 since open. Grade holds A+.','#00D4A0'], + ['GRADE','Skenes o6.5 K firmed to A — CHC whiff rate confirms the read.','#00D4A0'], + ['LIVE','Edwards o5.5 AST pacing ▲ — usage +6% with Gobert off.','#00D4A0'], + ['FLAG','Vlad Jr. streak correlated with team total — stake trimmed.','#FFB347'], + ['CLV','Portfolio closing-line value tonight: +2.8% across 14 edges.','#B8BCC8'], + ['FADE','LaVine o22.5 drifting — MIL perimeter D. Graded D, avoid.','#FF4757'] + ]; + this._wi=0; + const wireTick=()=>{ + if(!this.rootNode) return; + const w=this.rootNode.querySelector('[data-wire]'); + const t=this.rootNode.querySelector('[data-wire-time]'); + if(!w) return; + const [tag,msg,col]=this._wireEntries[this._wi % this._wireEntries.length]; this._wi++; + w.innerHTML=''+tag+' · '+msg; + if(t){const d=new Date();const p=n=>String(n).padStart(2,'0');t.textContent=p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());} + w.style.animation='none'; void w.offsetWidth; + w.style.animation='vy-wirein 6.4s ease-out'; + }; + this._wire=setInterval(wireTick,6500); + setTimeout(wireTick,1800); + // live grade-shift micro-view — grade reacts to game state this._lg=setInterval(()=>{ if(this.props.reactionsLive===false || !this._booted || !this.rootNode) return; @@ -1563,6 +1691,9 @@ class Component extends DCLogic { clearInterval(this._rx); clearTimeout(this._bootTimer); clearInterval(this._lg); + clearInterval(this._wire); + if(this._io) this._io.disconnect(); + if(this._so) this._so.disconnect(); if(this._key) document.removeEventListener('keydown',this._key); } } diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js index dad1db0..f2a0c3d 100644 --- a/tests/unit/vyndrParityQA.test.js +++ b/tests/unit/vyndrParityQA.test.js @@ -140,3 +140,34 @@ describe('QA.22 — honest waiting copy everywhere (no passive mystery)', () => expect(read(rel)).toContain('nextRunLabelET'); }); }); + +// ── DESIGN COMPLETION TRAIN — M1 mobile foundation (2026-07-16) ────────── +// Structural mobile rules become failing tests (M4 "test-lock what's +// lockable"). These assert the RULES exist in source — they do NOT prove +// visual correctness at 390px (that needs the Chrome audit). +describe('M1 — mobile foundation is wired (structural, not visual)', () => { + const css = read('app/globals.css'); + + it('header collapses on phones: the decorative EKG is hidden <768px', () => { + // The audit flagged ticker + SIGNAL LIVE + STALE stacking in ~110px. + expect(css).toMatch(/\.hb-ekg\s*\{[^}]*display:\s*none/); + // and the hook actually exists on the component + expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/hb-ekg/); + expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/heartbeat-bar/); + }); + + it('primary CTA meets a 44px touch target on mobile', () => { + expect(css).toMatch(/\.vbtn\s*\{[^}]*min-height:\s*44px/); + // full-size VBtn carries the class; dense `small` buttons opt out + expect(read('components/vyndr/VBtn.tsx')).toMatch(/small\s*\?\s*undefined\s*:\s*'vbtn'/); + }); + + it('the M1 mobile-foundation block is present and honestly labelled unverified', () => { + expect(css).toMatch(/DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION/); + expect(css).toMatch(/VISUALLY UNVERIFIED at 390px/); + }); + + it('document never scrolls sideways on phones (overflow contained)', () => { + expect(css).toMatch(/@media\s*\(max-width:\s*767px\)\s*\{[^]*?html,\s*body\s*\{[^}]*overflow-x:\s*hidden/); + }); +}); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 9c73df8..92c5f4b 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1361,6 +1361,51 @@ html[data-font="readable"] .wm::after { opacity: 0.45 !important; } .gl-full.gl-expanded { margin-top: 10px; } } +/* ============================================================ + DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION (2026-07-16) + The mockup (specs/design-reference/vyndr-system.html) is DESKTOP-ONLY. + These rules are the deliberate 390px expression of its grammar — not a + shrink: ONE animated element in the header (DESIGN-SPEC §4), one bold + mono hero per card with muted context (§3/§5), ≥44px primary tap targets, + zero horizontal overflow. BUILT TO SPEC, VISUALLY UNVERIFIED at 390px + (WSL2↔Chrome unreachable here — a true-390px Chrome audit is the eyes). + ============================================================ */ + +/* Header-zone collapse (audit: ticker + SIGNAL LIVE + STALE all stacking in + ~110px on narrow). The EKG is decorative proof-of-life — dropped on phones + so the heartbeat reads as ONE clean status line, and the ticker + live-dot + become the single animated element in the header. */ +@media (max-width: 767px) { + .hb-ekg { display: none !important; } + .heartbeat-bar { + height: 28px !important; + gap: 10px !important; + padding: 0 12px !important; + } + /* The status line is one line by contract — never let it wrap into the + "stacked" state the audit flagged. */ + .heartbeat-bar > span { white-space: nowrap; } +} + +/* Tap targets — the PRIMARY button component meets the 44px touch minimum. + Scoped to .vbtn only (not dense in-row data chips — density is a feature; + per-surface tap-target review rides with each M1b surface). */ +@media (max-width: 767px) { + .vbtn { + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + } +} + +/* Mono hero grammar (§3 "one bold hero figure among muted context"). Card + hero numbers opt in via .m-hero; they scale up on phones so the ONE figure + that matters dominates the stacked card. Context stays demoted. */ +@media (max-width: 640px) { + .m-hero { font-size: clamp(28px, 12vw, 56px) !important; line-height: 1; } +} + /* ============================================================ Session 60 (night2/E) — §7 reveal choreography. Stamp slam + 90ms staggered context panels. Entrance keyframes floor at diff --git a/web/src/components/vyndr/LiveLayer.tsx b/web/src/components/vyndr/LiveLayer.tsx index 1a115f3..3e0e091 100644 --- a/web/src/components/vyndr/LiveLayer.tsx +++ b/web/src/components/vyndr/LiveLayer.tsx @@ -118,7 +118,7 @@ export function HeartbeatBar() { const syncColor = tier === 'stale' ? 'var(--miss)' : tier === 'amber' ? 'var(--amber)' : 'var(--text-2)'; return (
SIGNAL LIVE - {/* EKG strip */} -
+ {/* EKG strip — decorative proof-of-life. Hidden <768px (mobile header + collapses to ONE status line; the signal pulse + ticker are the single + animated element there — DESIGN-SPEC §4 "one animated element"). */} +
{[0, 1].map((k) => ( diff --git a/web/src/components/vyndr/VBtn.tsx b/web/src/components/vyndr/VBtn.tsx index d4905f4..5782275 100644 --- a/web/src/components/vyndr/VBtn.tsx +++ b/web/src/components/vyndr/VBtn.tsx @@ -54,6 +54,9 @@ export default function VBtn({ type={type} disabled={disabled} onClick={onClick} + /* full-size CTAs meet the 44px mobile touch target (globals.css M1); + dense `small` buttons keep their compact size (density is a feature). */ + className={small ? undefined : 'vbtn'} style={{ ...base, ...VARIANTS[variant], ...style }} onMouseEnter={(e) => { if (variant === 'outline') e.currentTarget.style.borderColor = 'var(--g-a)';