Session 42: Player Intelligence System — archetypes, stat strips, player profile, enhanced cards (2011 tests)

Built from the Claude Design "VYNDR Player Intelligence" bundle (10 sections).

- Archetypes: src/services/archetypeService.js — 41 archetypes (15 NBA / 5
  WNBA-unique / 15 MLB / 6 soccer), classify -> primary+secondary+blend.
  Frontend visual map web/src/lib/archetypes.js (colors verified == backend).
  ArchetypeBadge (full/ghost/tint + glyphs) + ArchetypeBlend (DNA bar).
- StatStrip (compact/expanded): player name once, horizontal mono stats,
  inline GradeBadge props, onPlayerClick -> profile.
- Stats API: extended src/routes/stats.js with /player/:name, /leaders,
  /game/:id (rate-limited). Aggregation in playerIntelService.js (sanitizes
  name param; grades cache; graceful on cold cache). Next proxies added.
- Player Profile /player/[name]: all 9 design sections, graceful empty states.
- Enhanced GameCard (MLB pitchers + player-grouped StatStrips) + GradeResultCard
  (archetype strip + stat context + VYNDR intelligence, optional/self-hiding via
  gradeAdapter.buildIntelFields). Player-name links wired everywhere.
- Settings page replaces the S41 redirect (account/subscription/notifications/
  display/responsible-play/danger-zone with DELETE-gated delete). LINKS to the
  real /settings/security MFA page — does not replace it. + BookChip.
- Bonus: Stats Explorer /explore (real /api/stats/leaders leaderboard); added
  Explore + Settings to Nav MORE.

Deferred (need data pipelines, Session 43): Team Hub, Offseason Intel, Slate
redesign, Stats Explorer sub-panels.

Backend 1940 -> 2011 tests (+71), 157 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-18 11:12:24 -04:00
parent 32069863dc
commit 8bc79f3c38
33 changed files with 2655 additions and 22 deletions
+2 -1
View File
@@ -20,12 +20,13 @@ const PRIMARY = [
{ id: 'ledger', label: 'Ledger', href: '/ledger' },
];
const MORE = [
{ label: 'Explore', href: '/explore' },
{ label: 'Compare', href: '/compare' },
{ label: 'Tracker', href: '/tracker' },
{ label: 'The Report', href: '/blog' },
{ label: 'Invite', href: '/invite' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Settings', href: '/settings/security' },
{ label: 'Settings', href: '/settings' },
];
const TICKER_ITEMS = [
@@ -0,0 +1,62 @@
import { badgeStyle, glyphSvg } from '@/lib/archetypes';
interface ArchetypeBadgeProps {
archetype: string; // archetype name, e.g. "POWER PULL" (case-insensitive)
sport?: string; // nba/mlb/wnba/soccer — informational; styling is per-archetype
variant?: 'full' | 'ghost' | 'tint';
size?: 'sm' | 'md';
showDesc?: boolean;
}
/**
* ArchetypeBadge (Session 42) — ported pixel-for-pixel from the design's
* ArchetypeBadge.dc.html. JetBrains Mono label + a per-archetype glyph.
* full = solid fill (PRIMARY)
* ghost = transparent + colored border (SECONDARY)
* tint = subtle tinted bg (default)
* Data never glitches — this is a chrome label, no animation.
*/
export default function ArchetypeBadge({
archetype,
variant = 'tint',
size = 'sm',
showDesc = false,
}: ArchetypeBadgeProps) {
const s = badgeStyle(archetype, variant, size);
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, verticalAlign: 'middle' }}>
<span
className="mono"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 5,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.07em',
lineHeight: 1,
boxSizing: 'border-box',
whiteSpace: 'nowrap',
fontSize: s.fontSize,
padding: s.padding,
borderRadius: s.radius,
color: s.textColor,
background: s.bg,
border: `1px solid ${s.borderColor}`,
textShadow: s.textShadow,
}}
>
<span
style={{ display: 'inline-flex', flex: 'none', width: s.glyphSize, height: s.glyphSize, color: s.glyphColor }}
dangerouslySetInnerHTML={{ __html: glyphSvg(s.glyph) }}
/>
{s.name}
</span>
{showDesc && s.desc && (
<span style={{ fontFamily: 'var(--sans)', fontSize: s.descSize, color: '#7A7E8C', whiteSpace: 'nowrap' }}>
{s.desc}
</span>
)}
</span>
);
}
@@ -0,0 +1,76 @@
import { archetypeColor } from '@/lib/archetypes';
import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
export interface BlendSegment {
archetype: string;
weight: number;
}
interface ArchetypeBlendProps {
blend: BlendSegment[];
size?: 'sm' | 'md';
showLegend?: boolean;
caption?: string;
}
/**
* ArchetypeBlend (Session 42) — the production-DNA bar. Ported from the
* design's ArchetypeBlend.dc.html. Each segment's width is that archetype's
* share of the player's statistical value; the widest is PRIMARY.
*/
export default function ArchetypeBlend({
blend,
size = 'md',
showLegend = true,
caption = '',
}: ArchetypeBlendProps) {
const sm = size === 'sm';
const raw = (Array.isArray(blend) ? blend : []).slice(0, 4);
const total = raw.reduce((s, b) => s + (Number(b.weight) || 0), 0) || 1;
const pct = (w: number) => Math.round(((Number(w) || 0) / total) * 100);
const barH = sm ? 6 : 10;
const gap = sm ? 7 : 13;
const pctSize = sm ? 10 : 13;
const capSize = sm ? 11 : 12.5;
const roleW = sm ? 54 : 66;
const ROLECOL = ['#C6CBD6', '#6B6F7E', '#6B6F7E', '#5A5E6B'];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap, width: '100%' }}>
<div style={{ position: 'relative', display: 'flex', height: barH, borderRadius: 99, overflow: 'hidden', gap: 1.5, background: '#07070D', border: '1px solid #15151F' }}>
{raw.map((b, i) => (
<span
key={i}
title={`${b.archetype}${pct(b.weight)}% of production profile`}
style={{
width: `${(Math.max(0, Number(b.weight) || 0) / total) * 100}%`,
background: archetypeColor(b.archetype),
boxShadow: `inset 0 0 0 100px ${i === 0 ? 'transparent' : 'rgba(0,0,0,0.18)'}`,
}}
/>
))}
</div>
{showLegend && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '9px 16px', alignItems: 'center' }}>
{raw.map((b, i) => (
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span className="mono" style={{ fontSize: 8, fontWeight: 700, letterSpacing: '0.08em', color: ROLECOL[i] || '#5A5E6B', width: roleW, textAlign: 'right' }}>
{i === 0 ? 'PRIMARY' : i === raw.length - 1 && raw.length > 2 ? 'TERTIARY' : 'SUPPORTING'}
</span>
<ArchetypeBadge archetype={b.archetype} size={sm ? 'sm' : 'md'} variant={i === 0 ? 'full' : 'ghost'} />
<span className="mono" style={{ fontSize: pctSize, fontWeight: 700, color: i === 0 ? '#FFFFFF' : '#9499A8' }}>
{pct(b.weight)}%
</span>
</span>
))}
</div>
)}
{caption && (
<div style={{ fontFamily: 'var(--sans)', fontSize: capSize, lineHeight: 1.5, color: '#7E8390' }}>{caption}</div>
)}
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { bookInfo } from '@/lib/books';
interface BookChipProps {
book: string; // DK / FD / MGM / CZR / ESPN / BR / PB ... (case-insensitive)
size?: 'sm' | 'md';
showName?: boolean;
nameColor?: string;
}
/**
* BookChip (Session 42) — a sportsbook tile with the book's brand color, ported
* from the design's BookChip.dc.html. JetBrains Mono mono-code in a tinted tile;
* optional full name. Data chrome — never glitches.
*/
export default function BookChip({ book, size = 'sm', showName = false, nameColor = '#E6E8EE' }: BookChipProps) {
const b = bookInfo(book);
const sm = size === 'sm';
const tile = sm ? 26 : 34;
const monoSize = sm ? (b.mono.length > 2 ? 8 : 10) : (b.mono.length > 2 ? 10 : 13);
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: sm ? 8 : 10, verticalAlign: 'middle' }}>
<span
className="mono"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flex: 'none',
fontWeight: 800,
lineHeight: 1,
width: tile,
height: tile,
borderRadius: sm ? 6 : 8,
fontSize: monoSize,
color: b.fg,
background: b.bg,
border: `1px solid ${b.bd}`,
letterSpacing: '-0.02em',
}}
>
{b.mono}
</span>
{showName && (
<span style={{ fontFamily: 'var(--sans)', fontSize: sm ? 12 : 14, fontWeight: 600, color: nameColor, whiteSpace: 'nowrap' }}>
{b.name}
</span>
)}
</span>
);
}
+55 -2
View File
@@ -3,6 +3,9 @@
import SportBadge from '@/components/vyndr/SportBadge';
import SectionHead from '@/components/vyndr/SectionHead';
import GradeBadge from '@/components/vyndr/GradeBadge';
import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
import StatStrip, { type StatCell, type StripProp, type StripArchetype } from '@/components/vyndr/StatStrip';
import { playerHref } from '@/lib/playerHref';
export interface GameLine {
book: string;
@@ -23,6 +26,19 @@ export interface GameProp {
side: string;
delta?: string;
}
/** Session 42 — design's enhanced card: one strip per player (name once). */
export interface PlayerStrip {
player: string;
team: string;
archetype?: StripArchetype;
stats: StatCell[];
props: StripProp[];
}
export interface StartingPitcher {
name: string;
era: string;
archetype?: string;
}
export interface GameCardData {
id: string;
sport: string;
@@ -37,6 +53,9 @@ export interface GameCardData {
lines: GameLine[];
props?: GameProp[];
streaks?: Array<{ player: string; text: string }>;
// Session 42 — Player Intelligence enhancements (optional, self-hiding).
pitchers?: { away: StartingPitcher; home: StartingPitcher };
playerStrips?: PlayerStrip[];
}
interface GameCardProps {
@@ -130,6 +149,22 @@ export default function GameCard({ game: g, onAddParlay, onOpen }: GameCardProps
{g.venue && (<><span style={{ color: 'var(--text-2)' }}> · </span>{g.venue}</>)}
</div>
{/* MLB STARTING PITCHERS (Session 42) */}
{g.pitchers && (
<div className="mono" style={{ padding: '0 16px 11px', fontSize: 11, color: 'var(--text-1)', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span style={{ color: 'var(--text-2)', letterSpacing: '0.04em' }}>STARTING</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: '#C8CCD6' }}>
{g.pitchers.away.name} <span style={{ color: 'var(--text-1)' }}>{g.pitchers.away.era} ERA</span>
{g.pitchers.away.archetype && <ArchetypeBadge archetype={g.pitchers.away.archetype} size="sm" />}
</span>
<span style={{ color: 'var(--text-2)' }}>vs</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: '#C8CCD6' }}>
{g.pitchers.home.name} <span style={{ color: 'var(--text-1)' }}>{g.pitchers.home.era} ERA</span>
{g.pitchers.home.archetype && <ArchetypeBadge archetype={g.pitchers.home.archetype} size="sm" />}
</span>
</div>
)}
<div style={{ height: 1, background: 'var(--border)' }} />
{/* GAME LINES */}
@@ -155,10 +190,28 @@ export default function GameCard({ game: g, onAddParlay, onOpen }: GameCardProps
<div style={{ height: 1, background: 'var(--border)' }} />
{/* PROPS */}
{/* PROPS — Session 42: prefer the player-grouped StatStrip (name once,
archetype + horizontal stats + all graded props on one line); fall
back to the legacy per-prop rows for callers that don't supply strips. */}
<div style={{ padding: '13px 16px' }}>
<SectionHead style={{ marginBottom: 11 }}>GRADED PROPS</SectionHead>
{g.props && g.props.length > 0 ? (
{g.playerStrips && g.playerStrips.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{g.playerStrips.map((ps, i) => (
<StatStrip
key={i}
player={ps.player}
team={ps.team}
sport={g.sport}
archetype={ps.archetype}
stats={ps.stats}
props={ps.props}
variant="compact"
onPlayerClick={() => { window.location.href = playerHref(ps.player, g.sport); }}
/>
))}
</div>
) : g.props && g.props.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
{g.props.map((p, i) => (
<PropRow key={i} prop={p} onAddParlay={onAddParlay} />
+60 -2
View File
@@ -4,7 +4,10 @@ import { useEffect, useState } from 'react';
import SportBadge from '@/components/vyndr/SportBadge';
import SectionHead from '@/components/vyndr/SectionHead';
import VBtn from '@/components/vyndr/VBtn';
import ArchetypeBlend from '@/components/vyndr/ArchetypeBlend';
import GradeBadge from '@/components/vyndr/GradeBadge';
import { gradeColor, gradeHex } from '@/lib/vyndrTokens';
import { playerHref } from '@/lib/playerHref';
export interface GradeResultData {
player: string;
@@ -22,6 +25,11 @@ export interface GradeResultData {
killConditions?: string[];
books: Array<{ name: string; line: number; odds: string; best?: boolean }>;
altLadder?: Array<{ line: number; grade: string }>;
// Session 42 — Player Intelligence additions (all optional; self-hide).
archetypeBlend?: Array<{ archetype: string; weight: number }>;
propDNA?: { reliable: string[]; volatile: string[] };
statContext?: { season?: string; last10?: string; vsOpp?: string };
vyndrIntel?: { form?: number | string; usage?: string; matchup?: string; rest?: string };
}
interface GradeResultCardProps {
@@ -80,10 +88,10 @@ export default function GradeResultCard({
>
{sweep && <div className="crt-sweep-local" />}
{/* 1. HEADER */}
{/* 1. HEADER — player name links to the full profile (Session 42) */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px', background: 'var(--bg-2)', borderBottom: '1px solid var(--border)' }}>
<div>
<div style={{ fontSize: 22, fontWeight: 800, letterSpacing: '-0.01em', lineHeight: 1.1 }}>{d.player}</div>
<a href={playerHref(d.player, d.sport)} style={{ fontSize: 22, fontWeight: 800, letterSpacing: '-0.01em', lineHeight: 1.1, color: 'inherit', textDecoration: 'none' }}>{d.player}</a>
<div className="mono" style={{ fontSize: 12, color: 'var(--text-1)', marginTop: 3 }}>
<span style={{ color: sideColor, fontWeight: 700 }}>{d.side.toUpperCase()} {d.line}</span>
<span style={{ color: 'var(--text-2)', margin: '0 7px' }}>·</span>{d.stat}
@@ -95,6 +103,24 @@ export default function GradeResultCard({
</div>
</div>
{/* 1b. ARCHETYPE STRIP (Session 42) — between header and grade hero */}
{Array.isArray(d.archetypeBlend) && d.archetypeBlend.length > 0 && (
<div style={{ padding: '13px 20px', background: '#0C0E0D', borderBottom: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 8 }}>
<ArchetypeBlend blend={d.archetypeBlend} size="sm" showLegend caption="Why this grade: the prop sits in this player's PRIMARY lane, so the model weights it heavily." />
{d.propDNA && (
<div className="mono" style={{ fontSize: 11, color: '#7E8A86' }}>
<span style={{ color: 'var(--text-2)' }}>PROP DNA</span>
{(d.propDNA.reliable || []).map((s) => (
<span key={s}> · {s.replace(/_/g, ' ')} <span style={{ color: 'var(--g-a)' }}> reliable</span></span>
))}
{(d.propDNA.volatile || []).map((s) => (
<span key={s}> · {s.replace(/_/g, ' ')} <span style={{ color: 'var(--amber)' }}> volatile</span></span>
))}
</div>
)}
</div>
)}
{/* 2. GRADE HERO — intel surface */}
<div className="intel-surface" style={{ padding: '26px 20px 22px', textAlign: 'center' }}>
<div className="label" style={{ position: 'relative', zIndex: 2, color: 'rgba(232,255,244,.5)', marginBottom: 2 }}>VYNDR GRADE</div>
@@ -162,6 +188,38 @@ export default function GradeResultCard({
</div>
)}
{/* 5b. STAT CONTEXT (Session 42) */}
{d.statContext && (d.statContext.season || d.statContext.last10 || d.statContext.vsOpp) && (
<div style={{ padding: '16px 20px', borderTop: '1px solid var(--border)' }}>
<SectionHead style={{ marginBottom: 12 }}>STAT CONTEXT</SectionHead>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 1, background: 'var(--border)', border: '1px solid var(--border)', borderRadius: 9, overflow: 'hidden' }}>
{[
{ l: 'SEASON', v: d.statContext.season, col: 'var(--text-0)' },
{ l: 'LAST 10', v: d.statContext.last10, col: 'var(--g-a)' },
{ l: 'vs OPP', v: d.statContext.vsOpp, col: 'var(--g-a)' },
].map((x, i) => (
<div key={i} style={{ background: 'var(--bg-1)', padding: '11px 12px' }}>
<div className="mono" style={{ fontSize: 9, color: 'var(--text-2)', letterSpacing: '0.06em', marginBottom: 5 }}>{x.l}</div>
<div className="mono" style={{ fontSize: 16, fontWeight: 600, color: x.v ? x.col : 'var(--text-2)' }}>{x.v || '—'}</div>
</div>
))}
</div>
</div>
)}
{/* 5c. VYNDR INTELLIGENCE (Session 42) */}
{d.vyndrIntel && (
<div className="intel-surface" style={{ margin: '0 20px 16px', padding: '15px 16px', borderRadius: 12, border: '1px solid rgba(0,212,160,0.24)' }}>
<div className="mono" style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.1em', color: 'var(--g-a)', marginBottom: 12 }}>VYNDR INTELLIGENCE</div>
<div className="mono" style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center', fontSize: 12 }}>
{d.vyndrIntel.form != null && (<><span style={{ color: 'var(--text-2)' }}>Form</span><span style={{ color: 'var(--g-ap)', fontWeight: 700 }}>{d.vyndrIntel.form}</span><span style={{ color: '#2A3531' }}>·</span></>)}
{d.vyndrIntel.usage && (<><span style={{ color: 'var(--text-2)' }}>Usage</span><span style={{ color: 'var(--text-0)', fontWeight: 600 }}>{d.vyndrIntel.usage}</span><span style={{ color: '#2A3531' }}>·</span></>)}
{d.vyndrIntel.matchup && (<><span style={{ color: 'var(--text-2)' }}>Matchup</span><GradeBadge grade={d.vyndrIntel.matchup} size="sm" /><span style={{ color: '#2A3531' }}>·</span></>)}
{d.vyndrIntel.rest && (<><span style={{ color: 'var(--text-2)' }}>Rest</span><span style={{ color: 'var(--g-a)', fontWeight: 600 }}>{d.vyndrIntel.rest}</span></>)}
</div>
</div>
)}
{/* 6. KILL CONDITIONS */}
{hasKill && (
<div style={{ margin: '0 20px 16px', border: '1px solid rgba(255,179,71,.4)', borderRadius: 8, background: 'rgba(255,179,71,.06)', padding: '13px 15px' }}>
+145
View File
@@ -0,0 +1,145 @@
import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
import GradeBadge from '@/components/vyndr/GradeBadge';
export interface StatCell {
label: string;
value: string;
}
export interface StripProp {
stat: string;
line: number | string;
side: string; // O / U / Over / Under
grade: string;
}
export interface StripArchetype {
primary: string;
secondary?: string | null;
}
interface StatStripProps {
player: string;
team: string;
sport?: string;
archetype?: StripArchetype;
stats: StatCell[];
last10?: StatCell[] | string;
props?: StripProp[];
meta?: string; // expanded: "ATL · 3B · #27"
variant?: 'compact' | 'expanded';
onPlayerClick?: () => void;
}
const Sep = ({ ch = '|' }: { ch?: string }) => (
<span style={{ color: '#3A3A48', margin: '0 8px' }}>{ch}</span>
);
/**
* StatStrip (Session 42) — the horizontal player line. Ported from the design's
* Stat Strip section. The player name appears ONCE; stats flow horizontally as
* a JetBrains Mono run (never stacked with the name repeated per stat).
* compact = game cards / inline
* expanded = profile hero / grade result
*/
export default function StatStrip({
player,
team,
archetype,
stats,
last10,
props,
meta,
variant = 'compact',
onPlayerClick,
}: StatStripProps) {
const last10Str = typeof last10 === 'string'
? last10
: Array.isArray(last10)
? last10.map((c) => `${c.value} ${c.label}`).join(' · ')
: '';
const nameStyle: React.CSSProperties = onPlayerClick
? { cursor: 'pointer', textDecoration: 'none', color: 'inherit' }
: {};
const PlayerName = ({ children, ...rest }: { children: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>) =>
onPlayerClick ? (
<span role="link" tabIndex={0} onClick={onPlayerClick} onKeyDown={(e) => { if (e.key === 'Enter') onPlayerClick(); }} style={{ ...nameStyle, ...(rest.style || {}) }}>
{children}
</span>
) : (
<span {...rest}>{children}</span>
);
if (variant === 'expanded') {
return (
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: 20 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<PlayerName style={{ fontWeight: 800, fontSize: 22, letterSpacing: '-0.01em', textTransform: 'uppercase', ...nameStyle }}>
{player}
</PlayerName>
<span className="mono" style={{ fontSize: 12, color: 'var(--text-1)' }}>{meta || team}</span>
</div>
{archetype && (
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<ArchetypeBadge archetype={archetype.primary} size="md" showDesc />
{archetype.secondary && <ArchetypeBadge archetype={archetype.secondary} size="md" variant="ghost" />}
</div>
)}
<div style={{ height: 1, background: 'var(--border)', margin: '16px 0' }} />
<div className="mono" style={{ display: 'flex', gap: 26, flexWrap: 'wrap', marginBottom: 10 }}>
{stats.map((s, i) => (
<div key={i}>
<span style={{ fontSize: 18, color: '#fff', fontWeight: 600 }}>{s.value}</span>{' '}
<span style={{ fontSize: 11, color: 'var(--text-1)' }}>{s.label}</span>
</div>
))}
</div>
{last10Str && (
<div className="mono" style={{ fontSize: 12, color: 'var(--text-1)' }}>
<span style={{ color: 'var(--g-a)', letterSpacing: '0.04em' }}>LAST 10</span>
<Sep ch="·" />
{last10Str}
</div>
)}
</div>
);
}
// compact
return (
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 9 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, flexWrap: 'wrap' }}>
<PlayerName style={{ fontWeight: 700, fontSize: 14, color: '#fff', ...nameStyle }}>{player}</PlayerName>
<span className="mono" style={{ fontSize: 11, color: 'var(--text-1)' }}>{team}</span>
{archetype && <ArchetypeBadge archetype={archetype.primary} size="sm" variant="full" />}
{archetype?.secondary && (
<>
<span className="mono" style={{ color: '#3A3A48', fontSize: 11 }}>/</span>
<ArchetypeBadge archetype={archetype.secondary} size="sm" variant="ghost" />
</>
)}
</div>
<div className="mono game-lines-grid" style={{ fontSize: 12, color: 'var(--text-0)', letterSpacing: '0.02em' }}>
{stats.map((s, i) => (
<span key={i}>
{i > 0 && <Sep />}
{s.value} {s.label}
</span>
))}
</div>
{props && props.length > 0 && (
<div className="game-lines-grid" style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', overflowX: 'auto' }}>
<span className="mono" style={{ fontSize: 11, color: 'var(--text-1)', letterSpacing: '0.06em' }}>PROPS</span>
{props.map((p, i) => (
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
{i > 0 && <span style={{ color: '#3A3A48' }}>·</span>}
<span className="mono" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#B8BCC8' }}>
{p.stat} {p.side}{p.line} <GradeBadge grade={p.grade} size="sm" />
</span>
</span>
))}
</div>
)}
</div>
);
}
+9 -1
View File
@@ -12,9 +12,17 @@ export { default as GradeResultCard } from './GradeResultCard';
export type { GradeResultData } from './GradeResultCard';
export { default as ProcessingGrade } from './ProcessingGrade';
export { default as GameCard } from './GameCard';
export type { GameCardData, GameLine, GameProp } from './GameCard';
export type { GameCardData, GameLine, GameProp, PlayerStrip, StartingPitcher } from './GameCard';
export { default as ClaimMeter } from './ClaimMeter';
/* Player Intelligence (Session 42) */
export { default as ArchetypeBadge } from './ArchetypeBadge';
export { default as ArchetypeBlend } from './ArchetypeBlend';
export type { BlendSegment } from './ArchetypeBlend';
export { default as StatStrip } from './StatStrip';
export type { StatCell, StripProp, StripArchetype } from './StatStrip';
export { default as BookChip } from './BookChip';
export {
GRADE_COLORS,
GRADE_HEX,