+
{p.stat} {p.side}{p.line}
+ {/* ROW-GRAMMAR slot 4 — market context: best-price dot,
+ then movement chip, BEFORE any model output. */}
- {/* Phase 2.5 — a revised grade is PUBLIC: original struck through. */}
+ {!p.dead &&
}
+ {/* ROW-GRAMMAR slot 5 — model output. Phase 2.5: a revised
+ grade is PUBLIC — original struck through, then current. */}
{p.revisedFrom && (
{p.revisedFrom}
)}
@@ -346,7 +398,6 @@ export default function StatStrip({
)
)}
- {!p.dead &&
}
{!p.outcome && !p.dead &&
}
{!p.outcome && !p.dead &&
}
@@ -356,11 +407,20 @@ export default function StatStrip({
)}
- {p.delta && (
-
- Current {p.delta.currentLine} · {p.delta.delta > 0 ? '▲' : '▼'} {toward ? 'TOWARD' : 'AWAY'} {p.delta.delta > 0 ? '+' : ''}{p.delta.delta}
+ {/* ROW-GRAMMAR sub-line — stat + market context, fixed order:
+ last-10 dots · line sparkline · current-line delta. Each
+ renders only on real data; all absent → no sub-line. */}
+ {(p.last10Dots?.length || (p.history && p.history.length >= 3) || p.delta) ? (
+
+
+
+ {p.delta && (
+
+ Current {p.delta.currentLine} · {p.delta.delta > 0 ? '▲' : '▼'} {toward ? 'TOWARD' : 'AWAY'} {p.delta.delta > 0 ? '+' : ''}{p.delta.delta}
+
+ )}
- )}
+ ) : null}
);
})}
diff --git a/web/src/components/vyndr/index.ts b/web/src/components/vyndr/index.ts
index db85104..7cab8dd 100644
--- a/web/src/components/vyndr/index.ts
+++ b/web/src/components/vyndr/index.ts
@@ -25,6 +25,10 @@ export { default as StatStrip } from './StatStrip';
export type { StatCell, StripProp, StripArchetype } from './StatStrip';
export { default as BookChip } from './BookChip';
+/* S6 (A1 board) — global search + row-grammar micro-marks */
+export { default as SearchModal } from './SearchModal';
+export { DotStrip, LineSparkline } from './StatStrip';
+
export {
GRADE_COLORS,
GRADE_HEX,
diff --git a/web/src/lib/slateAdapter.js b/web/src/lib/slateAdapter.js
index d43e18d..03be4ce 100644
--- a/web/src/lib/slateAdapter.js
+++ b/web/src/lib/slateAdapter.js
@@ -384,6 +384,10 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
// + the ORIGINAL grade when a public revision happened.
movement: rec.movement || null,
revisedFrom: rec.revised_from_grade || null,
+ // S6 (A1 board, ROW-GRAMMAR sub-line) — real captured line history
+ // (sparkline, ≥3 points) + last-10 ●/○ vs tonight's locked line.
+ history: Array.isArray(rec.history) && rec.history.length > 0 ? rec.history : null,
+ last10Dots: Array.isArray(rec.last10_dots) && rec.last10_dots.length > 0 ? rec.last10_dots : null,
});
} else {
byPlayer[pk].props.push({
diff --git a/web/src/lib/teams.js b/web/src/lib/teams.js
new file mode 100644
index 0000000..19addae
--- /dev/null
+++ b/web/src/lib/teams.js
@@ -0,0 +1,119 @@
+/* ============================================================
+ S6 (A1 board) — static team registry for global search.
+ League membership is a stable public fact (not market data), so a
+ static list is honest. CommonJS so the Jest suite requires it and
+ the .tsx SearchModal imports it (allowJs).
+
+ Soccer is deliberately ABSENT: the product has no canonical soccer
+ team registry (no soccer team-hub branch, no fixed competition set)
+ — absent beats wrong. Add it when the soccer hub lands.
+
+ Team links go to /team/:abbr?sport= (the S51 Team Hub). MLB abbrs
+ match statsapi (AZ, CWS, ATH); NBA/WNBA hubs render the partial
+ graded-roster view until a free roster feed exists.
+ ============================================================ */
+
+const TEAMS = [
+ // ── MLB (30) ──────────────────────────────────────────────
+ { sport: 'mlb', abbr: 'AZ', name: 'Arizona Diamondbacks' },
+ { sport: 'mlb', abbr: 'ATL', name: 'Atlanta Braves' },
+ { sport: 'mlb', abbr: 'BAL', name: 'Baltimore Orioles' },
+ { sport: 'mlb', abbr: 'BOS', name: 'Boston Red Sox' },
+ { sport: 'mlb', abbr: 'CHC', name: 'Chicago Cubs' },
+ { sport: 'mlb', abbr: 'CWS', name: 'Chicago White Sox' },
+ { sport: 'mlb', abbr: 'CIN', name: 'Cincinnati Reds' },
+ { sport: 'mlb', abbr: 'CLE', name: 'Cleveland Guardians' },
+ { sport: 'mlb', abbr: 'COL', name: 'Colorado Rockies' },
+ { sport: 'mlb', abbr: 'DET', name: 'Detroit Tigers' },
+ { sport: 'mlb', abbr: 'HOU', name: 'Houston Astros' },
+ { sport: 'mlb', abbr: 'KC', name: 'Kansas City Royals' },
+ { sport: 'mlb', abbr: 'LAA', name: 'Los Angeles Angels' },
+ { sport: 'mlb', abbr: 'LAD', name: 'Los Angeles Dodgers' },
+ { sport: 'mlb', abbr: 'MIA', name: 'Miami Marlins' },
+ { sport: 'mlb', abbr: 'MIL', name: 'Milwaukee Brewers' },
+ { sport: 'mlb', abbr: 'MIN', name: 'Minnesota Twins' },
+ { sport: 'mlb', abbr: 'NYM', name: 'New York Mets' },
+ { sport: 'mlb', abbr: 'NYY', name: 'New York Yankees' },
+ { sport: 'mlb', abbr: 'ATH', name: 'Athletics' },
+ { sport: 'mlb', abbr: 'PHI', name: 'Philadelphia Phillies' },
+ { sport: 'mlb', abbr: 'PIT', name: 'Pittsburgh Pirates' },
+ { sport: 'mlb', abbr: 'SD', name: 'San Diego Padres' },
+ { sport: 'mlb', abbr: 'SF', name: 'San Francisco Giants' },
+ { sport: 'mlb', abbr: 'SEA', name: 'Seattle Mariners' },
+ { sport: 'mlb', abbr: 'STL', name: 'St. Louis Cardinals' },
+ { sport: 'mlb', abbr: 'TB', name: 'Tampa Bay Rays' },
+ { sport: 'mlb', abbr: 'TEX', name: 'Texas Rangers' },
+ { sport: 'mlb', abbr: 'TOR', name: 'Toronto Blue Jays' },
+ { sport: 'mlb', abbr: 'WSH', name: 'Washington Nationals' },
+ // ── NBA (30) ──────────────────────────────────────────────
+ { sport: 'nba', abbr: 'ATL', name: 'Atlanta Hawks' },
+ { sport: 'nba', abbr: 'BOS', name: 'Boston Celtics' },
+ { sport: 'nba', abbr: 'BKN', name: 'Brooklyn Nets' },
+ { sport: 'nba', abbr: 'CHA', name: 'Charlotte Hornets' },
+ { sport: 'nba', abbr: 'CHI', name: 'Chicago Bulls' },
+ { sport: 'nba', abbr: 'CLE', name: 'Cleveland Cavaliers' },
+ { sport: 'nba', abbr: 'DAL', name: 'Dallas Mavericks' },
+ { sport: 'nba', abbr: 'DEN', name: 'Denver Nuggets' },
+ { sport: 'nba', abbr: 'DET', name: 'Detroit Pistons' },
+ { sport: 'nba', abbr: 'GSW', name: 'Golden State Warriors' },
+ { sport: 'nba', abbr: 'HOU', name: 'Houston Rockets' },
+ { sport: 'nba', abbr: 'IND', name: 'Indiana Pacers' },
+ { sport: 'nba', abbr: 'LAC', name: 'LA Clippers' },
+ { sport: 'nba', abbr: 'LAL', name: 'Los Angeles Lakers' },
+ { sport: 'nba', abbr: 'MEM', name: 'Memphis Grizzlies' },
+ { sport: 'nba', abbr: 'MIA', name: 'Miami Heat' },
+ { sport: 'nba', abbr: 'MIL', name: 'Milwaukee Bucks' },
+ { sport: 'nba', abbr: 'MIN', name: 'Minnesota Timberwolves' },
+ { sport: 'nba', abbr: 'NOP', name: 'New Orleans Pelicans' },
+ { sport: 'nba', abbr: 'NYK', name: 'New York Knicks' },
+ { sport: 'nba', abbr: 'OKC', name: 'Oklahoma City Thunder' },
+ { sport: 'nba', abbr: 'ORL', name: 'Orlando Magic' },
+ { sport: 'nba', abbr: 'PHI', name: 'Philadelphia 76ers' },
+ { sport: 'nba', abbr: 'PHX', name: 'Phoenix Suns' },
+ { sport: 'nba', abbr: 'POR', name: 'Portland Trail Blazers' },
+ { sport: 'nba', abbr: 'SAC', name: 'Sacramento Kings' },
+ { sport: 'nba', abbr: 'SAS', name: 'San Antonio Spurs' },
+ { sport: 'nba', abbr: 'TOR', name: 'Toronto Raptors' },
+ { sport: 'nba', abbr: 'UTA', name: 'Utah Jazz' },
+ { sport: 'nba', abbr: 'WAS', name: 'Washington Wizards' },
+ // ── WNBA (13) ─────────────────────────────────────────────
+ { sport: 'wnba', abbr: 'ATL', name: 'Atlanta Dream' },
+ { sport: 'wnba', abbr: 'CHI', name: 'Chicago Sky' },
+ { sport: 'wnba', abbr: 'CON', name: 'Connecticut Sun' },
+ { sport: 'wnba', abbr: 'DAL', name: 'Dallas Wings' },
+ { sport: 'wnba', abbr: 'GSV', name: 'Golden State Valkyries' },
+ { sport: 'wnba', abbr: 'IND', name: 'Indiana Fever' },
+ { sport: 'wnba', abbr: 'LVA', name: 'Las Vegas Aces' },
+ { sport: 'wnba', abbr: 'LAS', name: 'Los Angeles Sparks' },
+ { sport: 'wnba', abbr: 'MIN', name: 'Minnesota Lynx' },
+ { sport: 'wnba', abbr: 'NYL', name: 'New York Liberty' },
+ { sport: 'wnba', abbr: 'PHX', name: 'Phoenix Mercury' },
+ { sport: 'wnba', abbr: 'SEA', name: 'Seattle Storm' },
+ { sport: 'wnba', abbr: 'WAS', name: 'Washington Mystics' },
+];
+
+/** /team/:abbr?sport= — the canonical Team Hub link (S51). */
+function teamHref(team) {
+ return `/team/${encodeURIComponent(team.abbr)}?sport=${encodeURIComponent(team.sport)}`;
+}
+
+/**
+ * Case-insensitive team match: full name substring, mascot (last word)
+ * prefix, or abbreviation prefix. Returns [] under 2 chars.
+ */
+function searchTeams(q, limit = 6) {
+ const s = String(q || '').trim().toLowerCase();
+ if (s.length < 2) return [];
+ const out = [];
+ for (const t of TEAMS) {
+ const name = t.name.toLowerCase();
+ const mascot = name.split(' ').pop() || '';
+ if (name.includes(s) || mascot.startsWith(s) || t.abbr.toLowerCase().startsWith(s)) {
+ out.push(t);
+ if (out.length >= limit) break;
+ }
+ }
+ return out;
+}
+
+module.exports = { TEAMS, searchTeams, teamHref };