diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 0000000..f9e43c7 --- /dev/null +++ b/.design-sync/NOTES.md @@ -0,0 +1,34 @@ +# design-sync notes — Tensies + +- **This repo is NOT a React design system.** The app frontend is vanilla web components + layered CSS (no build step). The synced package is `.design-sync/bindings/` — thin React components that emit the exact markup the app's real CSS styles. The CSS/tokens/fonts ship verbatim from `static/css` + `static/fonts` (only `/static/` URLs rewritten); the JSX glue is new code, by the user's explicit choice ("CSS + thin React bindings", 2026-07-11). +- **Build chain**: `node .design-sync/bindings/build.mjs` (regenerates `src/assets.generated.ts` data URIs from `static/images`, bundles `dist/index.js`, emits `.d.ts`, assembles `dist/tensies.css` from the app stylesheets + copies woff2s into `dist/fonts/`) → then the converter with `--entry .design-sync/bindings/dist/index.js --node-modules .design-sync/bindings/node_modules`. Run the bindings build BEFORE the converter whenever `static/css/*`, `static/images/*`, or bindings src changed. +- `a2hs.css` is deliberately excluded from the shipped CSS (install-banner chrome tied to `landing-mich.webp` marketing imagery). `.game-bg`'s poster (`poster-game.webp`, 124KB) is inlined as a data URI. +- `src/pips.ts` in the bindings mirrors `static/js/pips.js` + `FACE_ROTATIONS` from `static/js/dice.js` — keep in sync if the app tables ever change. +- Playwright for the render check: macOS cache is `~/Library/Caches/ms-playwright` (has chromium-1228/-1232); `playwright@1.61.0` pins chromium-1228 — installed in `.ds-sync/` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`. +- Card chrome is white; Tensies is dark-on-dark — every authored preview wraps cells in `background: var(--color-bg)` surfaces. +- `body{position:fixed; overflow:hidden}` ships in the CSS (mobile app shell). Fine inside preview iframes; documented for designs in conventions.md ("fixed shell, inner scroll"). +- npm here blocks postinstall scripts (`allow-scripts` warnings) — esbuild still works via its JS fallback; ignore the warnings. + +## Preview-authoring gotchas (folded from the 2026-07-11 waves) + +- `var(--color-panel-screen)` is translucent — in preview cells it must layer over an opaque `var(--color-bg)` or it washes out grey on the white card sheet. +- `.btn-back` is absolutely positioned (`top: 1.45rem; left: calc(2rem - 5px)`, landing.css) — isolated cells pass `style={{position:'static'}}`, or give the wrapper `position: relative` + app padding beside an `h1.screen-title.has-back`. +- `.roll-area` breaks out edge-to-edge (`margin-inline: -1rem`) and the Roll button rises ~3rem above its bar — wrapper needs `padding: 3.5rem 1rem 1.5rem` + `overflow: hidden`. +- **No `.die-3d.match` rule exists in the shipped CSS** — matched and unmatched dice share one ivory material by design (dice.css comment); matched-ness reads positionally via the matched zone, the pink accent lives on RoundTarget. The bindings' `matched` prop still emits the app's `match` class; docs corrected 2026-07-11. +- ConfirmDialog/`` renders inline non-modal (no backdrop, no UA vertical centering) — previews supply a flex-centered dark positioning context. +- GameMenu closed renders nothing; PlayerListItem must sit inside PlayerList; EqIcon standalone needs a `.btn-audio` scaffold chip; TopBar wrappers want `maxWidth: 390`, no padding, `overflow: hidden`. +- Editing `cfg.overrides..viewport` AFTER a build trips `[CONFIG_STALE]` for those components (viewport is a graded key; only cardMode/primaryStory are presentation-only) and `preview-rebuild` rejects a whole `--components` list if ANY target is stale — set viewports before waves, or re-stamp with a full build. +- Skipped-as-uncapturable states: input focus rings, hover/press feedback, EQ/sonar/shimmer/tumble animation motion (single frames captured), PlayerList scroll edge-fades, TopBar open-menu. + +## Known render warns + +- (none recorded yet) + +## Re-sync risks + +- **The bindings mirror the app by hand.** If `static/css` class names/markup contracts change (e.g. player-mini structure, stamp markup, dialog classes) the bindings keep compiling but drift visually — re-run the pixel-level eyeball on the contact sheets after any app frontend change, and diff `src/pips.ts` against `static/js/pips.js`/`dice.js`. +- **`assets.generated.ts` is gitignored** — regenerated by `build.mjs` from `static/images`; a renamed/removed logo/avatar svg breaks the bindings build loudly (good), a redesigned one silently changes cards (fine — it's the source of truth). +- **Rye/Yellowtail woff2 files are copied at bindings-build time** from `static/fonts` — if fonts are renamed, update `build.mjs` FONTS list. +- **Only partially verified:** animation motion (tumbles, EQ dance, sonar, shimmer) is graded from single frames; focus/hover states not captured at all. +- **Toolchain assumptions:** host node v26, `.ds-sync` deps installed fresh per clone (`esbuild ts-morph @types/react` + `playwright@1.61.0` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`; macOS playwright cache at `~/Library/Caches/ms-playwright`, chromium-1228). +- **Grades live only in the uploaded `_ds_sync.json`** — the local `.cache/` is gitignored; a fresh clone re-syncs against the project anchor (config.projectId ff56dcca-19c6-4c6f-9dc4-c931abdb5c36, project "Tensies UI"). diff --git a/.design-sync/bindings/build.mjs b/.design-sync/bindings/build.mjs new file mode 100644 index 0000000..b9704d9 --- /dev/null +++ b/.design-sync/bindings/build.mjs @@ -0,0 +1,107 @@ +/* Build the tensies-ui bindings package: + 1. Generate src/assets.generated.ts (data URIs for the repo's brand SVGs + + a QR placeholder) so previews and designs are self-contained. + 2. esbuild src/index.ts → dist/index.js (ESM, react external). + 3. tsc --emitDeclarationOnly → dist/*.d.ts. + 4. Assemble dist/tensies.css from the app's REAL stylesheets + (static/css, index.html order, a2hs excluded) with font URLs rewritten + to ./fonts/ (copied alongside) and the board poster inlined as a data + URI. The CSS is the app's, byte-for-byte apart from those URL rewrites. */ + +import { execSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = join(HERE, '..', '..'); +const STATIC = join(REPO, 'static'); +const DIST = join(HERE, 'dist'); + +// ── 1. Generated assets ── +const svgUri = (p) => + `data:image/svg+xml;base64,${readFileSync(join(STATIC, 'images', p)).toString('base64')}`; + +// Deterministic QR-ish placeholder (finder squares + pseudo-random modules). +function qrPlaceholder() { + let seed = 7; + const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648); + const N = 21; + let cells = ''; + const finder = (x, y) => + ``; + for (let y = 0; y < N; y++) { + for (let x = 0; x < N; x++) { + const inFinder = (x < 8 && y < 8) || (x >= N - 8 && y < 8) || (x < 8 && y >= N - 8); + if (!inFinder && rand() > 0.55) cells += ``; + } + } + const svg = `${cells}${finder(0, 0)}${finder(14, 0)}${finder(0, 14)}`; + return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`; +} + +writeFileSync( + join(HERE, 'src', 'assets.generated.ts'), + `/* GENERATED by build.mjs from static/images — do not edit. */ +export const LOGO_SVG_URI = ${JSON.stringify(svgUri('logo.svg'))}; +export const LOGO_WINNER_SVG_URI = ${JSON.stringify(svgUri('logo-winner.svg'))}; +export const AVATAR_DEFAULT_URI = ${JSON.stringify(svgUri('avatar-default.svg'))}; +export const QR_PLACEHOLDER_URI = ${JSON.stringify(qrPlaceholder())}; +`, +); + +// ── 2 + 3. Bundle + declarations ── +mkdirSync(DIST, { recursive: true }); +const run = (cmd) => execSync(cmd, { cwd: HERE, stdio: 'inherit' }); +run( + 'npx esbuild src/index.ts --bundle --format=esm --jsx=automatic ' + + '--external:react --external:react-dom --external:react/jsx-runtime ' + + '--outfile=dist/index.js', +); +run('npx tsc -p tsconfig.json'); + +// ── 4. CSS + fonts ── +// index.html order, minus a2hs.css (install-banner chrome tied to marketing +// imagery — not part of the component design language). +const CSS_FILES = [ + 'critical.css', + 'controls.css', + 'shell.css', + 'sheet.css', + 'landing.css', + 'lobby.css', + 'stamp.css', + 'nearby.css', + 'game.css', + 'players-bar.css', + 'dice.css', + 'menu.css', + 'overlays.css', + 'auth.css', + 'profile.css', + 'game-detail.css', +]; + +mkdirSync(join(DIST, 'fonts'), { recursive: true }); +const FONTS = [ + 'inter-latin-variable.woff2', + 'ebgaramond-latin-variable.woff2', + 'rye-latin.woff2', + 'yellowtail-latin.woff2', +]; +for (const f of FONTS) copyFileSync(join(STATIC, 'fonts', f), join(DIST, 'fonts', f)); + +const posterUri = `data:image/webp;base64,${readFileSync(join(STATIC, 'video', 'poster-game.webp')).toString('base64')}`; + +let css = CSS_FILES.map((f) => `/* ── static/css/${f} ── */\n${readFileSync(join(STATIC, 'css', f), 'utf8')}`).join('\n\n'); +css = css + .replaceAll("url('/static/fonts/", "url('./fonts/") + .replaceAll("url('/static/video/poster-game.webp')", `url('${posterUri}')`); + +const leftover = css.match(/url\(['"]?\/static\/[^)]+\)/g); +if (leftover) { + console.error(`! unrewritten /static/ url(s) in CSS:\n ${[...new Set(leftover)].join('\n ')}`); +} +writeFileSync(join(DIST, 'tensies.css'), css); + +console.error(`built dist/: index.js + d.ts + tensies.css (${CSS_FILES.length} sheets) + ${FONTS.length} fonts`); diff --git a/.design-sync/bindings/docs/ActionButton.md b/.design-sync/bindings/docs/ActionButton.md new file mode 100644 index 0000000..147d8a4 --- /dev/null +++ b/.design-sync/bindings/docs/ActionButton.md @@ -0,0 +1,15 @@ +--- +category: Controls +--- + +# ActionButton + +A circular quick-action with caption below — the Copy Link / Share / Play / Check In row. Compose several inside `
`. + +```tsx +
+ + +
+``` +Icons are inline SVGs sized by `.btn-icon` (24×24 viewBox, `stroke="currentColor"`). diff --git a/.design-sync/bindings/docs/AudioButton.md b/.design-sync/bindings/docs/AudioButton.md new file mode 100644 index 0000000..372da3d --- /dev/null +++ b/.design-sync/bindings/docs/AudioButton.md @@ -0,0 +1,12 @@ +--- +category: Controls +--- + +# AudioButton + +Audio-share button with the live 5-bar equalizer: breathes when idle, shimmers with dancing magenta bars when playing, adds inward sonar rings when listening. + +```tsx +Play +Listen +``` diff --git a/.design-sync/bindings/docs/BackButton.md b/.design-sync/bindings/docs/BackButton.md new file mode 100644 index 0000000..0dd540c --- /dev/null +++ b/.design-sync/bindings/docs/BackButton.md @@ -0,0 +1,12 @@ +--- +category: Controls +--- + +# BackButton + +Round icon-only back chip with a chevron; sits beside a `.screen-title.has-back` heading. + +```tsx + +Join a Game +``` diff --git a/.design-sync/bindings/docs/Button.md b/.design-sync/bindings/docs/Button.md new file mode 100644 index 0000000..aa33904 --- /dev/null +++ b/.design-sync/bindings/docs/Button.md @@ -0,0 +1,14 @@ +--- +category: Controls +--- + +# Button + +The standard button. `primary` = shimmering gold CTA (one per screen: Create Game / Start Game / Join Game); `secondary` = raised dark panel for everything else. + +```tsx + + + +``` +`block` makes the full-width (max 400px) form CTA. diff --git a/.design-sync/bindings/docs/ColorTokens.md b/.design-sync/bindings/docs/ColorTokens.md new file mode 100644 index 0000000..36f28de --- /dev/null +++ b/.design-sync/bindings/docs/ColorTokens.md @@ -0,0 +1,9 @@ +--- +category: Foundations +--- + +# ColorTokens + +The Tensies color tokens — warm candlelit dark palette defined as `--color-*` custom properties in the shipped CSS. + +Use tokens, never hex: `style={{ background: 'var(--color-panel)', color: 'var(--color-text-warm)' }}`. Key tokens: `--color-accent` (pink #ff4d6d), `--color-bg` (deep brown), `--color-panel` / `--color-panel-screen` (translucent dark panels), `--color-field` (input wells), `--color-raised` (+`-hover`) (buttons), `--color-border` / `--color-border-strong` (amber hairlines), `--color-text` / `-warm` / `-muted` / `-label`, `--color-amber`, `--color-success`. Also `--radius-md` (12px), `--shadow-text`, `--shadow-text-lg`, `--font-family-base`, `--font-family-heading`. diff --git a/.design-sync/bindings/docs/ConfirmDialog.md b/.design-sync/bindings/docs/ConfirmDialog.md new file mode 100644 index 0000000..3211900 --- /dev/null +++ b/.design-sync/bindings/docs/ConfirmDialog.md @@ -0,0 +1,14 @@ +--- +category: Overlays +--- + +# ConfirmDialog + +A centered confirm dialog with a stacked action column. + +```tsx + + + + +``` diff --git a/.design-sync/bindings/docs/DiceLoader.md b/.design-sync/bindings/docs/DiceLoader.md new file mode 100644 index 0000000..b92be04 --- /dev/null +++ b/.design-sync/bindings/docs/DiceLoader.md @@ -0,0 +1,13 @@ +--- +category: Foundations +--- + +# DiceLoader + +The brand loader: a pink 6 and an ivory 4 hopping with squash-and-stretch over a pulsing ground shadow — pure CSS animation. + +```tsx + +

Loading…

+``` +Used on the loading screen and inside PauseOverlay. Center it in a column flex container on `var(--color-panel-screen)`. diff --git a/.design-sync/bindings/docs/DiceZones.md b/.design-sync/bindings/docs/DiceZones.md new file mode 100644 index 0000000..bff20f8 --- /dev/null +++ b/.design-sync/bindings/docs/DiceZones.md @@ -0,0 +1,13 @@ +--- +category: Game +--- + +# DiceZones + +The player's board: unmatched dice scattered loose on the left, matched dice collected right with casual tilts. Parent must have a height. + +```tsx +
+ +
+``` diff --git a/.design-sync/bindings/docs/Die.md b/.design-sync/bindings/docs/Die.md new file mode 100644 index 0000000..df61d4b --- /dev/null +++ b/.design-sync/bindings/docs/Die.md @@ -0,0 +1,13 @@ +--- +category: Game +--- + +# Die + +One 3-D bone-ivory die — a CSS cube with six pip faces, rotated so `value` faces front. `matched` marks it locked on the round target (same ivory material by design — matched-ness reads positionally; the pink accent lives on RoundTarget); `tumbling` runs a roll animation. + +```tsx + + + +``` diff --git a/.design-sync/bindings/docs/EqIcon.md b/.design-sync/bindings/docs/EqIcon.md new file mode 100644 index 0000000..2adfc71 --- /dev/null +++ b/.design-sync/bindings/docs/EqIcon.md @@ -0,0 +1,9 @@ +--- +category: Controls +--- + +# EqIcon + +The 5-bar equalizer icon used inside audio buttons; amber at rest, dancing magenta inside an active `.btn-audio`. + +Composed automatically by `AudioButton`; use standalone only inside a `.btn-audio` element. diff --git a/.design-sync/bindings/docs/ErrorMsg.md b/.design-sync/bindings/docs/ErrorMsg.md new file mode 100644 index 0000000..c415ef0 --- /dev/null +++ b/.design-sync/bindings/docs/ErrorMsg.md @@ -0,0 +1,11 @@ +--- +category: Controls +--- + +# ErrorMsg + +Inline error line under a form — accent pink, centered, height reserved so the layout doesn't jump. + +```tsx +Game not found +``` diff --git a/.design-sync/bindings/docs/FieldHint.md b/.design-sync/bindings/docs/FieldHint.md new file mode 100644 index 0000000..96ee412 --- /dev/null +++ b/.design-sync/bindings/docs/FieldHint.md @@ -0,0 +1,11 @@ +--- +category: Controls +--- + +# FieldHint + +Muted helper line above a field, with optional `.field-hint-link` inline links. + +```tsx +Play with any name, or sign up to keep your stats. +``` diff --git a/.design-sync/bindings/docs/FormStack.md b/.design-sync/bindings/docs/FormStack.md new file mode 100644 index 0000000..c8c0a94 --- /dev/null +++ b/.design-sync/bindings/docs/FormStack.md @@ -0,0 +1,17 @@ +--- +category: Controls +--- + +# FormStack + +The vertical form column used by the landing and join forms: hint, inputs, gold CTA, divider, actions, error line. + +```tsx + + + + + + + +``` diff --git a/.design-sync/bindings/docs/GameMenu.md b/.design-sync/bindings/docs/GameMenu.md new file mode 100644 index 0000000..ce0f6b6 --- /dev/null +++ b/.design-sync/bindings/docs/GameMenu.md @@ -0,0 +1,13 @@ +--- +category: Overlays +--- + +# GameMenu + +The in-game host menu: Pause/Resume toggle with sliding switch, live pause status (countdown + who's missing), and the danger End Game item. Fills its nearest positioned ancestor. + +```tsx +
+ +
+``` diff --git a/.design-sync/bindings/docs/LobbyStamp.md b/.design-sync/bindings/docs/LobbyStamp.md new file mode 100644 index 0000000..dd9fed3 --- /dev/null +++ b/.design-sync/bindings/docs/LobbyStamp.md @@ -0,0 +1,12 @@ +--- +category: Lobby +--- + +# LobbyStamp + +The vintage postage-stamp game invite — perforated vermilion frame, guilloche engraving, Rye serial (the game code), Yellowtail watermark, dice seal, QR. The lobby centerpiece; scales to its container width. + +```tsx + + +``` diff --git a/.design-sync/bindings/docs/OrDivider.md b/.design-sync/bindings/docs/OrDivider.md new file mode 100644 index 0000000..9186028 --- /dev/null +++ b/.design-sync/bindings/docs/OrDivider.md @@ -0,0 +1,11 @@ +--- +category: Controls +--- + +# OrDivider + +The thin "── or ──" divider between form sections. + +```tsx + +``` diff --git a/.design-sync/bindings/docs/PauseOverlay.md b/.design-sync/bindings/docs/PauseOverlay.md new file mode 100644 index 0000000..05341f2 --- /dev/null +++ b/.design-sync/bindings/docs/PauseOverlay.md @@ -0,0 +1,11 @@ +--- +category: Overlays +--- + +# PauseOverlay + +The non-host pause wait screen: TENSIES wordmark, hopping-dice loader, and the wait message over a near-opaque dim. + +```tsx + +``` diff --git a/.design-sync/bindings/docs/PlayerCard.md b/.design-sync/bindings/docs/PlayerCard.md new file mode 100644 index 0000000..fab1605 --- /dev/null +++ b/.design-sync/bindings/docs/PlayerCard.md @@ -0,0 +1,12 @@ +--- +category: Game +--- + +# PlayerCard + +A players-bar mini card: name, "you" badge, wins chip, dice-progress bar. `hot` = opponent at 7+ matched (red); `disconnected` dims. + +```tsx + + +``` diff --git a/.design-sync/bindings/docs/PlayerList.md b/.design-sync/bindings/docs/PlayerList.md new file mode 100644 index 0000000..0f3f612 --- /dev/null +++ b/.design-sync/bindings/docs/PlayerList.md @@ -0,0 +1,14 @@ +--- +category: Lobby +--- + +# PlayerList + +The lobby roster list — scrollable with edge fades. + +```tsx + + + + +``` diff --git a/.design-sync/bindings/docs/PlayerListItem.md b/.design-sync/bindings/docs/PlayerListItem.md new file mode 100644 index 0000000..45de6f6 --- /dev/null +++ b/.design-sync/bindings/docs/PlayerListItem.md @@ -0,0 +1,11 @@ +--- +category: Lobby +--- + +# PlayerListItem + +One roster row: gradient-ringed avatar, name, gold HOST badge on the host. + +```tsx + +``` diff --git a/.design-sync/bindings/docs/PlayersBar.md b/.design-sync/bindings/docs/PlayersBar.md new file mode 100644 index 0000000..9e08533 --- /dev/null +++ b/.design-sync/bindings/docs/PlayersBar.md @@ -0,0 +1,14 @@ +--- +category: Game +--- + +# PlayersBar + +The horizontally scrolling row of PlayerCards under the game title row — local player first. + +```tsx + + + + +``` diff --git a/.design-sync/bindings/docs/RollButton.md b/.design-sync/bindings/docs/RollButton.md new file mode 100644 index 0000000..c7e93c4 --- /dev/null +++ b/.design-sync/bindings/docs/RollButton.md @@ -0,0 +1,12 @@ +--- +category: Game +--- + +# RollButton + +The big Roll bar pinned under the dice zones. Disables with a "Paused" label while the host has the game paused. + +```tsx + + +``` diff --git a/.design-sync/bindings/docs/RoundStatus.md b/.design-sync/bindings/docs/RoundStatus.md new file mode 100644 index 0000000..97c0092 --- /dev/null +++ b/.design-sync/bindings/docs/RoundStatus.md @@ -0,0 +1,11 @@ +--- +category: Game +--- + +# RoundStatus + +The round header: gold-rimmed "Round N" pill above the target die. + +```tsx + +``` diff --git a/.design-sync/bindings/docs/RoundTarget.md b/.design-sync/bindings/docs/RoundTarget.md new file mode 100644 index 0000000..d048ef2 --- /dev/null +++ b/.design-sync/bindings/docs/RoundTarget.md @@ -0,0 +1,11 @@ +--- +category: Game +--- + +# RoundTarget + +The flat pink target die from the round header — the value everyone is rolling for. + +```tsx + +``` diff --git a/.design-sync/bindings/docs/ScreenBody.md b/.design-sync/bindings/docs/ScreenBody.md new file mode 100644 index 0000000..aca4690 --- /dev/null +++ b/.design-sync/bindings/docs/ScreenBody.md @@ -0,0 +1,14 @@ +--- +category: Chrome +--- + +# ScreenBody + +The content column of a screen — carries side padding, sits under the TopBar in a full-height flex column. + +```tsx +
+ + +
+``` diff --git a/.design-sync/bindings/docs/ScreenTitle.md b/.design-sync/bindings/docs/ScreenTitle.md new file mode 100644 index 0000000..3e2da48 --- /dev/null +++ b/.design-sync/bindings/docs/ScreenTitle.md @@ -0,0 +1,11 @@ +--- +category: Chrome +--- + +# ScreenTitle + +Screen heading in EB Garamond, e.g. "Waiting for Players". `hasBack` leaves room for the BackButton chip. + +```tsx +Waiting for Players +``` diff --git a/.design-sync/bindings/docs/SectionLabel.md b/.design-sync/bindings/docs/SectionLabel.md new file mode 100644 index 0000000..740ee8b --- /dev/null +++ b/.design-sync/bindings/docs/SectionLabel.md @@ -0,0 +1,11 @@ +--- +category: Chrome +--- + +# SectionLabel + +Small uppercase section heading, e.g. "Fellow Bar Rats". + +```tsx +Fellow Bar Rats +``` diff --git a/.design-sync/bindings/docs/Sheet.md b/.design-sync/bindings/docs/Sheet.md new file mode 100644 index 0000000..ee7bb34 --- /dev/null +++ b/.design-sync/bindings/docs/Sheet.md @@ -0,0 +1,13 @@ +--- +category: Overlays +--- + +# Sheet + +The bottom sheet: blurred dark panel sliding up from the bottom with a glowing top edge — used for the join form and place picker. + +```tsx + + + +``` diff --git a/.design-sync/bindings/docs/TextInput.md b/.design-sync/bindings/docs/TextInput.md new file mode 100644 index 0000000..da50dfa --- /dev/null +++ b/.design-sync/bindings/docs/TextInput.md @@ -0,0 +1,12 @@ +--- +category: Controls +--- + +# TextInput + +Text field: inset dark well, amber border, centered text, pink focus ring. `code` renders the large uppercase 5-letter game-code style. + +```tsx + + +``` diff --git a/.design-sync/bindings/docs/TopBar.md b/.design-sync/bindings/docs/TopBar.md new file mode 100644 index 0000000..8d55774 --- /dev/null +++ b/.design-sync/bindings/docs/TopBar.md @@ -0,0 +1,14 @@ +--- +category: Chrome +--- + +# TopBar + +The branded top bar: dice logo + "Tensies" wordmark + hamburger. Pre-game screens use it alone; the game screen passes a PlayersBar as children. + +```tsx + + + + +``` diff --git a/.design-sync/bindings/docs/TypeSpecimen.md b/.design-sync/bindings/docs/TypeSpecimen.md new file mode 100644 index 0000000..d9d7efb --- /dev/null +++ b/.design-sync/bindings/docs/TypeSpecimen.md @@ -0,0 +1,9 @@ +--- +category: Foundations +--- + +# TypeSpecimen + +The type system: EB Garamond (`var(--font-family-heading)`) for screen/section headings, Inter (`var(--font-family-base)`) for body and UI, Rye + Yellowtail only inside the vintage lobby stamp. + +Headings: `

` (EB Garamond, large). Body: Inter at ~1rem with `--color-text-warm`. Button labels: Inter 700 with 0.05em letter-spacing. Never use Rye/Yellowtail outside stamp-styled artwork. diff --git a/.design-sync/bindings/docs/WinnerOverlay.md b/.design-sync/bindings/docs/WinnerOverlay.md new file mode 100644 index 0000000..7956094 --- /dev/null +++ b/.design-sync/bindings/docs/WinnerOverlay.md @@ -0,0 +1,11 @@ +--- +category: Overlays +--- + +# WinnerOverlay + +The round-winner celebration dialog: banner with the round pill, winner logo, name in lights, and the next-round countdown bar. + +```tsx + +``` diff --git a/.design-sync/bindings/package-lock.json b/.design-sync/bindings/package-lock.json new file mode 100644 index 0000000..08b9275 --- /dev/null +++ b/.design-sync/bindings/package-lock.json @@ -0,0 +1,567 @@ +{ + "name": "tensies-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tensies-ui", + "version": "1.0.0", + "devDependencies": { + "@types/react": "^19.0.0", + "esbuild": "^0.25.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.8.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/.design-sync/bindings/package.json b/.design-sync/bindings/package.json new file mode 100644 index 0000000..b0d0de9 --- /dev/null +++ b/.design-sync/bindings/package.json @@ -0,0 +1,22 @@ +{ + "name": "tensies-ui", + "version": "1.0.0", + "private": true, + "description": "React bindings for the Tensies design language. Thin components that emit the exact markup styled by the app's real static/css stylesheets — the CSS, tokens, and fonts are shipped verbatim from the app; only this JSX glue is new.", + "type": "module", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "node build.mjs" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "esbuild": "^0.25.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.8.0" + } +} diff --git a/.design-sync/bindings/src/chrome.tsx b/.design-sync/bindings/src/chrome.tsx new file mode 100644 index 0000000..991eaee --- /dev/null +++ b/.design-sync/bindings/src/chrome.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from 'react'; +import { LOGO_SVG_URI } from './assets.generated'; + +/** + * The branded top bar (`.game-topbar`): dice logo mark + "Tensies" wordmark + + * the three-line hamburger. Pre-game screens use it alone (`.app-header` + * variant); the game screen passes a `` as children below the + * title row. `username` shows the signed-in pill (`.header-username`). + */ +export function TopBar({ username, children }: { username?: string; children?: ReactNode }) { + const cls = children ? 'game-topbar' : 'game-topbar app-header'; + return ( +
+
+
+ + Tensies +
+ {username ? ( + + @{username} + + ) : null} + +
+ {children} +
+ ); +} + +/** + * Screen heading (`.screen-title`) — EB Garamond serif, e.g. "Waiting for + * Players". `hasBack` reserves room for the round back chip beside it. + */ +export function ScreenTitle({ hasBack, children }: { hasBack?: boolean; children: ReactNode }) { + return

{children}

; +} + +/** Small uppercase section heading (`.section-label`), e.g. "Fellow Bar Rats". */ +export function SectionLabel({ children }: { children: ReactNode }) { + return

{children}

; +} + +/** + * The scrolling content column of a screen (`.screen-body`) — sits under the + * `TopBar` inside a full-height flex screen and carries the side padding. + */ +export function ScreenBody({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/.design-sync/bindings/src/controls.tsx b/.design-sync/bindings/src/controls.tsx new file mode 100644 index 0000000..915ad5b --- /dev/null +++ b/.design-sync/bindings/src/controls.tsx @@ -0,0 +1,158 @@ +import type { ButtonHTMLAttributes, FormHTMLAttributes, InputHTMLAttributes, ReactNode } from 'react'; + +export interface ButtonProps extends ButtonHTMLAttributes { + /** Visual weight: `primary` is the shimmering gold CTA, `secondary` the raised dark panel. */ + variant?: 'primary' | 'secondary'; + /** Full-width (`.btn-block`, capped at 400px) — the standard form CTA shape. */ + block?: boolean; +} + +/** + * The Tensies button (`.btn`). `primary` is the gold, shimmer-animated call to + * action ("Create Game", "Start Game"); `secondary` is the raised dark-panel + * button used for everything else. Disabled state dims to 45%. + */ +export function Button({ variant = 'primary', block, className, children, type = 'button', ...rest }: ButtonProps) { + const cls = ['btn', `btn-${variant}`, block ? 'btn-block' : '', className ?? ''].filter(Boolean).join(' '); + return ( + + ); +} + +/** The shared 5-bar equalizer icon (`.eq`) used inside audio buttons. */ +export function EqIcon() { + return ( + + ); +} + +export interface AudioButtonProps extends ButtonHTMLAttributes { + /** `idle` breathes softly; `playing` shimmers + dances the EQ; `listening` adds inward sonar rings. */ + state?: 'idle' | 'playing' | 'listening'; +} + +/** + * An audio-share button (`.btn-audio`): a secondary button with the live + * 5-bar equalizer icon. Used for "Play" (broadcast the game code as sound) + * and "Listen" (receive it). The active states animate — EQ bars dance + * magenta, Listen adds contracting sonar rings. + */ +export function AudioButton({ state = 'idle', className, children, type = 'button', ...rest }: AudioButtonProps) { + const cls = ['btn', 'btn-secondary', 'btn-audio', state !== 'idle' ? state : '', className ?? ''] + .filter(Boolean) + .join(' '); + return ( + + ); +} + +export interface TextInputProps extends InputHTMLAttributes { + /** Game-code style (`.code-input`): large, bold, letter-spaced, uppercased. */ + code?: boolean; +} + +/** + * The Tensies text input — inset dark field with amber border and centered + * text (styled by controls.css `input` + `.code-input`). Use `code` for the + * 5-letter game-code field. + */ +export function TextInput({ code, className, ...rest }: TextInputProps) { + const cls = [code ? 'code-input' : '', className ?? ''].filter(Boolean).join(' ') || undefined; + return ; +} + +/** Inline error line (`.error-msg`) — accent-pink, centered, reserves its height. */ +export function ErrorMsg({ children }: { children?: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +/** Muted helper line above a form field (`.field-hint`), with optional inline links (`.field-hint-link`). */ +export function FieldHint({ children }: { children?: ReactNode }) { + return

{children}

; +} + +/** + * Vertical form column (`.form-stack`) — the standard layout for the landing + * and join forms: hint, inputs, CTA, divider, actions, error line. + */ +export function FormStack({ children, ...rest }: FormHTMLAttributes) { + return ( +
+ {children} +
+ ); +} + +/** The "─── or ───" divider (`.or-divider`) between form sections. */ +export function OrDivider() { + return ( + + ); +} + +/** + * The round icon-only back chip (`.btn-back`, styled in landing.css) — a + * chevron with a visually-hidden "Back" label. + */ +export function BackButton(props: ButtonHTMLAttributes) { + return ( + + ); +} + +export interface ActionButtonProps { + /** Caption under the circle (`.lobby-action-label`), e.g. "Copy Link". */ + label: string; + /** The icon — an inline SVG sized by `.btn-icon`, or an `` for audio actions. */ + children: ReactNode; + /** Adds `.btn-audio` chrome (breathing glow) for sound actions like "Play". */ + audio?: boolean; + /** Checked-in/on state (`.is-on`) for togglable actions like Check In. */ + on?: boolean; + onClick?: () => void; + 'aria-label'?: string; +} + +/** + * A circular quick-action (`.lobby-action`) with its caption below — the row + * of Copy Link / Share / Play / Check In actions on the lobby and landing + * screens. Compose several inside a `div.lobby-actions`. + */ +export function ActionButton({ label, children, audio, on, onClick, ...rest }: ActionButtonProps) { + const cls = ['lobby-action', audio ? 'btn-audio' : '', on ? 'is-on' : ''].filter(Boolean).join(' '); + return ( +
+ + {label} +
+ ); +} diff --git a/.design-sync/bindings/src/foundations.tsx b/.design-sync/bindings/src/foundations.tsx new file mode 100644 index 0000000..0990551 --- /dev/null +++ b/.design-sync/bindings/src/foundations.tsx @@ -0,0 +1,124 @@ +import type { ReactNode } from 'react'; +import { PIP_POSITIONS } from './pips'; + +/** One 3×3 pip-grid die for the loader (`.die` in critical.css — flat, 2-D). */ +function LoaderDie({ variant, value }: { variant: 'pink' | 'ivory'; value: number }) { + const on = new Set(PIP_POSITIONS[value] ?? []); + return ( +
+ {Array.from({ length: 9 }, (_, i) => ( + + ))} +
+ ); +} + +/** + * The Tensies brand loader: a pink 6 and an ivory 4 hopping with + * squash-and-stretch over a pulsing ground shadow, resting in the logo pose. + * Pure CSS animation (critical.css `.stage`); used on the loading screen and + * the pause overlay. Follow it with a `.loading-msg` line when used as a + * loading screen. + */ +export function DiceLoader() { + return ( +