Agents processing todos/ideas from admin-data.json should mirror them as cards on the author's Fizzy board. Slug → tag, progress → Review column, staging branch link in the card description. PAT is read from FIZZY_PAT env var; never hardcoded.
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
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:
src/lib/interactives.ts— the canonical registry. ExportsallInteractives(withslug,title,description,tags,themes, optionalsubcategory,dateAdded,hasGreenScreen,status: "published" | "draft" | "idea") andallThemes.publishedInteractivesfilters out drafts/ideas; this is what public pages use.getStaticPaths()here drives the/i/[slug]and/embed/[slug]routes and excludesideaentries.src/components/InteractiveRenderer.tsx— acomponentMapofslug → () => import(...)that lazy-loads the component.<InteractiveRenderer client:load slug={...} />is the single entry point used by both the full page and embed routes.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/themesand/themes/[theme]→ theme landing pages. "Discrete Math" is the one theme with subcategories (rendered viaThemeContentand/themes/discrete-math/[sub]); other themes list interactives directly and show anAddIdeaFormwhen the admin is logged in./admin→ unlinked dashboard (<meta name="robots" content="noindex">)- Blog (
/blog,/blog/[...slug]) reads from theblogcontent collection (src/content.config.ts).scripts/sync-blog.shcopies 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. underastro dev).src/lib/interactives.ts— forcommitStatusanddeleteInteractive, the function text-edits this source file and commits the result. Commits from the admin UI use descriptive messages likeadmin: 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_PATenv 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
mainand not to a per-sessionclaude/*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 colorschart-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
Alertor intro card explaining the premise - Put the play area center stage, controls below or to the side
- Use
Tabsto separate modes (e.g. Play / Explore / Solution inThreeBankAccounts) - Use
Badgefor status, tags, and hint chips - Reset with a
RotateCcwicon button; celebrate wins with confetti + a toast (sonneris 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.