Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .design-sync/NOTES.md
Original file line number Diff line number Diff line change
@@ -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/`<dialog open>` 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.<Name>.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").
107 changes: 107 additions & 0 deletions .design-sync/bindings/build.mjs
Original file line number Diff line number Diff line change
@@ -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) =>
`<rect x="${x}" y="${y}" width="7" height="7" fill="#1a1a1a"/><rect x="${x + 1}" y="${y + 1}" width="5" height="5" fill="#fff"/><rect x="${x + 2}" y="${y + 2}" width="3" height="3" fill="#1a1a1a"/>`;
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 += `<rect x="${x}" y="${y}" width="1" height="1" fill="#1a1a1a"/>`;
}
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><rect width="21" height="21" fill="#fff"/>${cells}${finder(0, 0)}${finder(14, 0)}${finder(0, 14)}</svg>`;
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`);
15 changes: 15 additions & 0 deletions .design-sync/bindings/docs/ActionButton.md
Original file line number Diff line number Diff line change
@@ -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 `<div className="lobby-actions">`.

```tsx
<div className="lobby-actions">
<ActionButton label="Copy Link"><svg className="btn-icon" …/></ActionButton>
<ActionButton label="Play" audio><EqIcon /></ActionButton>
</div>
```
Icons are inline SVGs sized by `.btn-icon` (24×24 viewBox, `stroke="currentColor"`).
12 changes: 12 additions & 0 deletions .design-sync/bindings/docs/AudioButton.md
Original file line number Diff line number Diff line change
@@ -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
<AudioButton>Play</AudioButton>
<AudioButton state="listening">Listen</AudioButton>
```
12 changes: 12 additions & 0 deletions .design-sync/bindings/docs/BackButton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
category: Controls
---

# BackButton

Round icon-only back chip with a chevron; sits beside a `.screen-title.has-back` heading.

```tsx
<BackButton onClick={goBack} />
<ScreenTitle hasBack>Join a Game</ScreenTitle>
```
14 changes: 14 additions & 0 deletions .design-sync/bindings/docs/Button.md
Original file line number Diff line number Diff line change
@@ -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
<Button block>Create Game</Button>
<Button variant="secondary">Cancel</Button>
<Button disabled>Start Game</Button>
```
`block` makes the full-width (max 400px) form CTA.
9 changes: 9 additions & 0 deletions .design-sync/bindings/docs/ColorTokens.md
Original file line number Diff line number Diff line change
@@ -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`.
14 changes: 14 additions & 0 deletions .design-sync/bindings/docs/ConfirmDialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
category: Overlays
---

# ConfirmDialog

A centered confirm dialog with a stacked action column.

```tsx
<ConfirmDialog title="Checked in at The Celt" body="Check out to remove your game from Nearby.">
<Button variant="secondary">Pick a different place</Button>
<Button>Check out</Button>
</ConfirmDialog>
```
13 changes: 13 additions & 0 deletions .design-sync/bindings/docs/DiceLoader.md
Original file line number Diff line number Diff line change
@@ -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
<DiceLoader />
<p className="loading-msg">Loading…</p>
```
Used on the loading screen and inside PauseOverlay. Center it in a column flex container on `var(--color-panel-screen)`.
13 changes: 13 additions & 0 deletions .design-sync/bindings/docs/DiceZones.md
Original file line number Diff line number Diff line change
@@ -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
<div style={{height: 420, display: 'flex', flexDirection: 'column'}}>
<DiceZones unmatched={[2,5,1,3,6,4]} matched={[6,6,6,6]} target={6} />
</div>
```
13 changes: 13 additions & 0 deletions .design-sync/bindings/docs/Die.md
Original file line number Diff line number Diff line change
@@ -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
<Die value={4} />
<Die value={6} matched />
<Die value={2} tumbling="a" />
```
9 changes: 9 additions & 0 deletions .design-sync/bindings/docs/EqIcon.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions .design-sync/bindings/docs/ErrorMsg.md
Original file line number Diff line number Diff line change
@@ -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
<ErrorMsg>Game not found</ErrorMsg>
```
11 changes: 11 additions & 0 deletions .design-sync/bindings/docs/FieldHint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
category: Controls
---

# FieldHint

Muted helper line above a field, with optional `.field-hint-link` inline links.

```tsx
<FieldHint>Play with any name, or <a className="field-hint-link" href="/signin">sign up</a> to keep your stats.</FieldHint>
```
17 changes: 17 additions & 0 deletions .design-sync/bindings/docs/FormStack.md
Original file line number Diff line number Diff line change
@@ -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
<FormStack>
<FieldHint>…</FieldHint>
<TextInput placeholder="Your name" />
<Button block>Create Game</Button>
<OrDivider />
<ErrorMsg />
</FormStack>
```
13 changes: 13 additions & 0 deletions .design-sync/bindings/docs/GameMenu.md
Original file line number Diff line number Diff line change
@@ -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
<div style={{position: 'relative', height: 500}}>
<GameMenu paused remaining="54:07" waitingOn="Waiting on Salty Walrus…" />
</div>
```
12 changes: 12 additions & 0 deletions .design-sync/bindings/docs/LobbyStamp.md
Original file line number Diff line number Diff line change
@@ -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
<LobbyStamp code="KQZXV" />
<LobbyStamp code="KQZXV" placeName="The Celt" />
```
11 changes: 11 additions & 0 deletions .design-sync/bindings/docs/OrDivider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
category: Controls
---

# OrDivider

The thin "── or ──" divider between form sections.

```tsx
<OrDivider />
```
11 changes: 11 additions & 0 deletions .design-sync/bindings/docs/PauseOverlay.md
Original file line number Diff line number Diff line change
@@ -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
<PauseOverlay hostName="Dapper Badger" />
```
12 changes: 12 additions & 0 deletions .design-sync/bindings/docs/PlayerCard.md
Original file line number Diff line number Diff line change
@@ -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
<PlayerCard name="Dapper Badger" wins={2} matched={7} hot />
<PlayerCard name="You" isMe wins={3} matched={4} leading />
```
14 changes: 14 additions & 0 deletions .design-sync/bindings/docs/PlayerList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
category: Lobby
---

# PlayerList

The lobby roster list — scrollable with edge fades.

```tsx
<PlayerList>
<PlayerListItem name="Dapper Badger" isHost />
<PlayerListItem name="Salty Walrus" />
</PlayerList>
```
Loading