# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
npm run dev          # astro dev (local dev server)
npm run build        # astro build → dist/ (static output)
npm run preview      # serve built site
npm run lint         # ESLint over .js/.jsx/.ts/.tsx/.astro
npm run lint:fix     # auto-fix lint issues
npm run format       # prettier --write .
npm run format:check # prettier --check .
```

No test suite is configured. Requires Node >= 22.12.0.

`src/components/ui/**` is ignored by ESLint (shadcn-generated code).

## Architecture

Astro 6 + React 19 + Tailwind 4 + shadcn/ui. Output is `static` (`astro.config.mjs`). The site is deployed to Netlify; a single serverless function (`netlify/functions/admin.mts`, routed at `/api/admin`) backs the admin UI. The root README and `components.json` still reference the Hatch template this was forked from — marketing/sections components (`src/components/sections/hatch-*`) and the work/services MDX collections are leftover from that template.

### The interactives system (core content)

Each interactive is a self-contained React component in `src/components/interactives/<Name>.tsx` — they freely import shadcn UI, framer-motion, katex, recharts, canvas-confetti, three/@react-three/fiber, etc. Many are 500–2000+ LOC single-file components with their own state machines.

Three registrations must be kept in sync when adding or renaming an interactive:

1. **`src/lib/interactives.ts`** — the canonical registry. Exports `allInteractives` (with `slug`, `title`, `description`, `tags`, `themes`, optional `subcategory`, `dateAdded`, `hasGreenScreen`, `status: "published" | "draft" | "idea"`) and `allThemes`. `publishedInteractives` filters out drafts/ideas; this is what public pages use. `getStaticPaths()` here drives the `/i/[slug]` and `/embed/[slug]` routes and excludes `idea` entries.
2. **`src/components/InteractiveRenderer.tsx`** — a `componentMap` of `slug → () => import(...)` that lazy-loads the component. `<InteractiveRenderer client:load slug={...} />` is the single entry point used by both the full page and embed routes.
3. **`src/lib/interactive-components.ts`** — a second near-duplicate componentMap. Both must be updated together (the duplication is historical; grep for the slug before editing).

The admin API (`applyStatusChange` / `applyDelete` in `netlify/functions/admin.mts`) parses `src/lib/interactives.ts` with string matching against `slug: "..."` and `status: "..." as const,`. Preserve that exact formatting when editing entries — otherwise admin-driven status commits and deletes will silently fail.

### Routing

- `/` → `HomepageHero` (`src/pages/index.astro`)
- `/i/[slug]` → full interactive page with navbar, breadcrumb, admin bar, share bar (`InteractiveLayout`)
- `/embed/[slug]` → same interactive in a bare shell (`EmbedLayout`) for iframe embedding; generated for every non-idea interactive
- `/themes` and `/themes/[theme]` → theme landing pages. "Discrete Math" is the one theme with subcategories (rendered via `ThemeContent` and `/themes/discrete-math/[sub]`); other themes list interactives directly and show an `AddIdeaForm` when the admin is logged in.
- `/admin` → unlinked dashboard (`<meta name="robots" content="noindex">`)
- Blog (`/blog`, `/blog/[...slug]`) reads from the `blog` content collection (`src/content.config.ts`). `scripts/sync-blog.sh` copies posts from a hardcoded Obsidian vault path — it only works on the author's machine.

### Admin system

Client code in `src/lib/admin.ts` keeps state in `localStorage` (auth flag, notes, status overrides, ideas, todos) and mirrors every write to `POST /api/admin`. The admin password is hard-coded in `src/lib/admin.ts` and echoed in the Netlify function (override via the `ADMIN_PASSWORD` env var in production) — this is a personal tool, not a real auth boundary. Don't add secrets here.

The Netlify function persists state by committing to this same repo via the GitHub Contents API:
- `src/data/admin-data.json` — notes, status overrides, ideas, todos. Bundled at build time and imported as a fallback when the API is unreachable (e.g. under `astro dev`).
- `src/lib/interactives.ts` — for `commitStatus` and `deleteInteractive`, the function text-edits this source file and commits the result. Commits from the admin UI use descriptive messages like `admin: set <slug> to <status>`.

Requires `GITHUB_TOKEN` env var on Netlify; without it, writes silently no-op.

### Todo & idea workflow (Fizzy kanban mirror)

The admin UI lets the author create **todos** (per-interactive, stored in `src/data/admin-data.json` under `todos[slug]`) and **ideas** (proposals for new interactives, under `ideas[]`). When an agent is asked to review or work on these, mirror the work to the author's Fizzy kanban board.

**Board**
- URL: `https://fizzy.neeldhara.cloud/7/boards/03fziwyb6y5amnepbensjq0sv/`
- Account: `7`
- Auth: Fizzy personal access token. **Do not commit it to this repo.** Read it from the `FIZZY_PAT` env var (or whatever secret store the author provides). If the token is unavailable, skip the mirror step and flag it to the user — never hardcode a fallback.

**Card conventions**
- One card per todo (title = the todo text) or per idea (title = idea title).
- The interactive **slug** (e.g. `parity-bits-game`, `three-bank-accounts`) becomes a **tag** on the card, grouping cards by interactive. For ideas not yet tied to a slug, use the idea's working title as a tag.
- **Column placement:**
  - Todo the agent has made progress on → **Review** column.
  - Idea the agent has implemented → **Review** column, with an implementation overview in the card body (what was built, files touched, trade-offs).
  - Todos/ideas without progress → whatever the board's inbox/backlog column is. Confirm with the user if unclear.
- **Card description must include a review link** — the GitHub URL for the staging branch (compare view vs. main) or, if a PR exists, the PR URL.

**Commit flow**
- Progress on todos/ideas goes to a **staging branch**, not directly to `main` and not to a per-session `claude/*` branch (unless the user explicitly says so).
- Prefer a stable branch name (e.g. `staging`) so card links stay valid as more work lands, rather than a new branch per todo.

### Green screen mode

Interactives marked `hasGreenScreen: true` can be wrapped in `GreenScreenWrapper` to render on a pure `#00ff00` background with forced black/white foreground colors (for chroma-keying into videos). The wrapper injects `!important` CSS overrides; be aware when debugging styling inside one.

### Aliases and conventions

TS path aliases from `tsconfig.json`: `@/*` → `src/*`, plus `@components/*`, `@layouts/*`, `@lib/*`. shadcn aliases in `components.json` point components to `@/components`, utils to `@/lib/utils` (the `cn()` helper). Tailwind 4 is wired via `@tailwindcss/vite` (no tailwind.config.js); design tokens live in `src/styles/global.css`. Icon library is `lucide-react`.

ESLint enforces `simple-import-sort` — imports and exports get auto-sorted on `lint:fix`.

## Design & aesthetics

The site has a distinctive editorial-meets-playful feel: serif display type over restrained neutrals, one warm red-orange accent, and generous whitespace. Interactives are the payoff — they should feel polished and a little delightful, not enterprise-CRUD.

**Reach for shadcn before hand-rolling.** `Card` / `CardHeader` / `CardTitle` / `CardContent` is the canonical container for an interactive's sections. `Button`, `Badge`, `Alert`, `Tabs`, `Dialog`, `Slider`, `Switch`, `Tooltip`, `Input`, `Label` are already themed and wired up — use them. Edit `src/components/ui/*` only with deliberate reason (ESLint ignores this dir because it's shadcn-generated).

**Always use design tokens, never raw hex.** Colors live as CSS variables in `src/styles/global.css` and are exposed as Tailwind utilities:
- Surfaces: `bg-background`, `bg-card`, `bg-muted`, `bg-accent`
- Text: `text-foreground`, `text-muted-foreground`
- Brand accent: `bg-primary` / `text-primary` / `text-primary-foreground` — use sparingly, for true calls-to-action
- Borders: `border-border`
- Status: `text-destructive`, `text-success`, chart colors `chart-1`..`chart-5`

Colors are authored in OKLCH. If you need a shade that isn't in the palette, add a token rather than inlining.

**Dark mode is mandatory.** The `.dark` class on `<html>` flips the tokens. Any literal tailwind color (`bg-green-100`, `text-amber-700`, etc.) *must* come with a `dark:` counterpart — see `ThreeBankAccounts.tsx`'s `RANK_COLORS` for the pattern. Better yet, lean on design tokens so dark mode is automatic.

**Typography.** Castoro (serif) for `h1`/`h2` via `.font-display`; Imprima/Geist (sans) for body and `h3+`. This pairing is load-bearing for the editorial feel — don't swap `h1` to sans on a whim. The `container` utility caps at 1200px; `container-sm` caps at 960px for denser reading layouts.

**Radii.** Base `--radius` is `0.625rem`, exposed as `rounded-sm` → `rounded-4xl` on the Tailwind scale. Cards and panels are typically `rounded-xl`; pill buttons/badges are `rounded-full`; small chips are `rounded-md`.

**Motion and delight.** `framer-motion` for layout/presence animations, `canvas-confetti` for win states and discovery moments, `katex` for math (`katex/dist/katex.min.css` must be imported where used). `prefers-reduced-motion` is honored globally in `global.css` — you don't need to guard individual transitions, but don't gate critical state changes behind animation either.

**Interactive layout patterns.** Looking across the folder, published interactives tend to:
- Open with a short framing `Alert` or intro card explaining the premise
- Put the play area center stage, controls below or to the side
- Use `Tabs` to separate modes (e.g. *Play / Explore / Solution* in `ThreeBankAccounts`)
- Use `Badge` for status, tags, and hint chips
- Reset with a `RotateCcw` icon button; celebrate wins with confetti + a toast (`sonner` is wired up)
- Stay responsive — test at mobile widths, since these embed into blog posts and videos

**Taste defaults.** Prefer fewer, cleaner controls over exhaustive settings. If a new feature needs an options panel, that's a signal to reconsider the UX. When in doubt, match the visual weight and density of an existing, well-regarded interactive in the same theme rather than inventing a new idiom.

### Build gotchas

`astro.config.mjs` sanitizes Rollup chunk filenames (`[^\w./-]` → `_`) and pins esbuild to `es2022` / `utf8` — these work around Netlify's esbuild choking on `!` and `~` in chunk names. Don't revert without retesting on Netlify. `netlify.toml` also disables Netlify's own JS/CSS bundling so Vite's output is shipped as-is.
