M1b screen 01: the flat EDGE BOARD — Design's mobile board IA

The one genuinely-new mobile screen. Design's mobile BOARD is a FLAT edge-ranked
list (all graded props across every game on one list, sorted by edge) — not the
desktop's game-grouped cards. Implemented to the drawing with REAL snapshot data:
- slateAdapter.flattenToEdgeBoard(cards) — pure transform of the assembled
  GameCardData[] (grade→game join already done) into ranked rows, edge desc.
  STRICT null edge sorts LAST (never 0-coerced to the top — Data Semantics Rule).
  Threaded edge_pct through buildPlayerStripsFromProps (was dropped). 6 unit tests.
- MobileEdgeBoard component — Design's exact screen-01 rows: rank (green #1),
  player + prop, matchup sub-line with TeamChips + live-dot, tier grade chip,
  and the edge% as the one bold mono hero (green +, red −). Ranked opacity ramp
  (1 → .55) + green inset border on the top reads. Breadth strip EDGES/AVG CLV/
  GAMES — CLV honest '—' (per-slate CLV isn't computed; never fabricated).
- Slate: <768px renders the flat board, ≥768px keeps game cards (same data,
  toggled by width). Ungraded slate still shows game cards on phones (no blank).

Built to Design's screen-01 drawing, VISUALLY UNVERIFIED at 390px — the core
mobile screen, top of the master-audit list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-16 21:46:47 -04:00
parent f02dc4a6d5
commit 8a24ac9129
5 changed files with 298 additions and 11 deletions
+33 -11
View File
@@ -7,7 +7,8 @@ import { useRouter } from 'next/navigation';
import VyndrGameCard, { type GameCardData } from '@/components/vyndr/GameCard';
import type { SlateSport, GameLines, GameStreak } from '@/components/GameCard';
import { PropRowProp, Tier } from '@/components/PropRow';
import { isRelevantGame, detectBestLines, indexGrades, indexDeltas, buildPlayerStripsFromProps, formatGameTime, buildPitcherMap, pitchersForGameTeams, gradeKey } from '@/lib/slateAdapter';
import { isRelevantGame, detectBestLines, indexGrades, indexDeltas, buildPlayerStripsFromProps, formatGameTime, buildPitcherMap, pitchersForGameTeams, gradeKey, flattenToEdgeBoard, edgeBoardBreadth } from '@/lib/slateAdapter';
import MobileEdgeBoard from '@/components/vyndr/MobileEdgeBoard';
// Wave 4A (Step 3) — OUTLOOK MODE: the never-empty grid. When there are no
// live games (and it's not a fetch failure) the grid shows REAL data —
// yesterday's proven receipts + tomorrow's date-pinned schedule.
@@ -1181,16 +1182,37 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook
graded prop has a real ≥2-book median to compare the model against. */}
{dateOffset === 0 && <MarketBreadth items={breadthItems} />}
<div style={{ display: 'grid', gap: 16 }}>
{orderedGames.map((g, i) => (
<VyndrGameCard
key={`${g.sport}-${g.homeTeam}-${g.awayTeam}-${i}`}
game={slateGameToCardData(g, gradeIndex, deltaIndex, pitcherMap, activeStat, viability, liveIndex)}
preferredBooks={preferredBooks}
onOpen={() => router.push('/scan')}
/>
))}
</div>
{(() => {
// Build each game's card once, then present two ways (Rev 3, screen 01):
// MOBILE = Design's FLAT edge-ranked board (all props, one list, sorted
// by edge); DESKTOP = the game-grouped cards. Same data, different IA —
// toggled by width in globals.css (.edge-board-mobile / -desktop).
const cards = orderedGames.map((g) => slateGameToCardData(g, gradeIndex, deltaIndex, pitcherMap, activeStat, viability, liveIndex));
const edgeRows = flattenToEdgeBoard(cards);
const breadth = edgeBoardBreadth(edgeRows, cards);
const hasBoard = edgeRows.length > 0;
return (
<>
{hasBoard && (
<div className="edge-board-mobile" style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, overflow: 'hidden' }}>
<MobileEdgeBoard rows={edgeRows} breadth={breadth} />
</div>
)}
{/* Cards: on mobile they hide ONLY when the flat board is present
(an ungraded slate still shows the game cards on phones). */}
<div className={hasBoard ? 'edge-board-desktop' : ''} style={{ display: 'grid', gap: 16 }}>
{cards.map((card, i) => (
<VyndrGameCard
key={`${card.sport}-${card.away.abbr}-${card.home.abbr}-${i}`}
game={card}
preferredBooks={preferredBooks}
onOpen={() => router.push('/scan')}
/>
))}
</div>
</>
);
})()}
{/* Session 23 — intelligence layer. These coexist WITH the odds
above; they never replace games. Both self-hide when empty, so