Development Progress

This file tracks major changes and milestones in the project.


OKF bundle — agent-queryable knowledge layer

Published an Open Knowledge Format bundle at /okf/ so a visiting agent can discover and reuse the design experiments without crawling the repo. Generated at build from the typed experiments registry (lib/okf/bundle.ts), served as prerendered markdown by app/okf/[file]/route.ts: index.md, log.md, and one concept per experiment. Each concept exposes resource (live demo) and source (GitHub) pointers — discovery layer, not inlined code. Wired into discovery via llms.txt, llms-full.txt, and the sitemap so a cold agent landing on joshcoolman.com/llms.txt finds the bundle first.


Adaptive Grid — strict square-grid Swiss poster experiment

Date: 2026-06-14

A new design experiment exploring strict grid adherence as the subject itself. An eight-column square module (one margin column each side, six-column content area) with row height measured in JS to equal column width, so cells stay perfectly square. Composed as explicit layers — grid lines, black crosshair markings, orange corner brackets, color blocks, type — all placed on the same fr grid tracks (not a CSS background), so nothing drifts sub-pixel and the markings land exactly on the lines. Responsiveness is a reflow rather than a resize: the content area collapses six columns to three at the narrow breakpoint and type holds a fixed size; the body paragraph measures itself and snaps to a whole number of square cells (ceil(height / cell)), shifting later sections down by whole rows so nothing overlaps. Solid blocks and a forest hero photo share a 4px inset off the grid lines. Narrow view reframes the hero as a conference treatment (FIELD / ECOLOGY / 2025). Built iteratively as a design dialogue; the early layer-toggle controls were removed once the grid was locked.


Skill cull — trimmed the slash-command menu

Date: 2026-06-14

The .claude/skills/ set had drifted wide, so it was audited by usage and trimmed to the surfaces actually in active use: design-experiment, ship-experiment, sketch, note, ai-news, blog-post, link, sanity-check, promote, prune, write-a-skill, and the vercel-react-best-practices reference. Twelve skills were retired from this repo — supabase, start-from-scratch, design-audit, replace, restyle, yt-review, animation-audit, gen-image, gen-sprite, bitmap-to-vector, grill-me, ts-handoff — and parked in a private claude-skills cold-store repo rather than deleted, so any of them can be restored later. (prune was initially cut but kept: it reads as zero manual invocations only because the pre-push hook runs its audit.sh automatically on every push — a passive dead-code nudge, not a slash command you type.) /promote absorbed the three retired audit passes (design-audit, animation-audit, ts-handoff) inline, since it was their main caller. README, CLAUDE.md's Feature Map, and the skills/experiments guide docs were updated to match. Older progress entries below still reference some retired skills — left intact as historical record.


War Room — sci-fi command-center HUD experiment

Date: 2026-06-12

New design experiment: a dense wall of eight live HUD panels (WarGames / Iron Man / Ambrosia-DEFCON-screensaver lineage), built and planned in one Fable 5 session. One shared rAF scheduler with a virtual clock that pauses on tab-hide drives every panel; ambient event channels (strikes, log chatter, glitch flicker, DEFCON shifts) poll that clock — no setInterval anywhere. The DEFCON theater map bakes Natural Earth 110m coastlines into a 19KB delta-encoded TS module (data/coastlines.ts, regen command in header) shared with the rotating dot-matrix globe — one data spine, two projections. Slerp-sampled great-circle strikes with dateline splitting launch on their own schedule; clicking the map fires a manual strike from the nearest base. Engaging a roster mugshot phases three reactions off a single engagedAt timestamp: a dossier unfolds (scanline-raster portrait revealed behind a scan beam — lib/scanline.ts), the map crosshairs the target, and the globe eases to face their longitude. Three display technologies share the wall: cyan vector glass, a green-phosphor terminal scrolling procedural ASCII pinout schematics, and a Nokia-style LCD with a hand-built 5×7 pixel font. Panels are deliberately composable — each fills its container via useCanvasPanel (DPR, ResizeObserver, IntersectionObserver pause), chrome is separate from content, and page.tsx is pure composition (~75 lines). Canvas 2D throughout, zero new dependencies.


Step Sequencer v2 — three-layer techno groovebox

Date: 2026-06-10

The step sequencer grew from a 16×8 pentatonic toy into a 16×11 three-layer groovebox: five pentatonic lead rows, three bass rows (C3/G2/C2), and a synthesized drum kit (hat, clap, kick). The audio engine moved from a single square-wave voice to real synthesis in lib/voices.ts — sine-drop kick, staggered noise-burst clap, filtered-noise hats, three bass tones (sub / saw / acid with a resonant filter sweep), three lead tones (soft / square / detuned saw) — routed through per-layer gain buses into a master compressor, with a tempo-synced dotted-eighth delay on the lead bus. GENERATE (lib/generate.ts) now composes geometrically rather than per-row probabilistically — small stamp shapes repeated at a fixed period with per-row phase offsets, on the observation that on a quantized pentatonic grid visual regularity is rhythmic regularity (diagonals = rolls, translation = ostinato). Three archetypes (cascade staircase through the kit, classic backbeat, sparse offbeat) never repeat back-to-back; the kick's four-on-the-floor is the one fixed musical anchor; repeats mutate more toward bars 3–4 so the loop drifts like a fill; a coverage pass enforces polyphony caps and a lead-density floor. The page loads with a pattern pre-seeded (in a mount effect, keeping SSR hydration clean). New controls stay inside the existing design language: two segmented tone pills in the header strip (lead soft/square/saw, bass sub/saw/acid — no mutes, this is for non-musicians), a swing slider next to BPM. Monono's embedded starter pattern was ported to the new row layout, and the hook normalizes any stale grid shape defensively.


Homepage backdrop v2 — MeshCanvas

Date: 2026-06-10

The home page background effect was upgraded from NetworkCanvas (organic node organisms with a physical click blast) to MeshCanvas — a structured, noise-jittered hex mesh in the same warm palette, descended from the seismic-mesh experiment. The field moves on three layers, all render-only: an interference swell (three low-frequency waves that morph rather than march), a slow whole-field rotation (~63s per revolution, mesh overscanned to the viewport diagonal so corners never expose), and a per-node sway. Clicks are always a depression — a broad slow surge with a 700ms smoothstep attack that presses the field away and dims it over ~5s, no rings, no node physics to recover from. NetworkCanvas remains in the repo untouched; swapping back is a one-line import change in app/page.tsx.


Date: 2026-06-07

The three-column section above the homepage footer now reads Blog, Docs, Link Worthy (the Plans column was pulled — it had become noise). Link Worthy mirrors the Blog column: the four most recent links with thumbnails, each opening its external URL. Every column gained a bottom-aligned "view all" link (View all blog posts / View documentation / View all links), and the columns are pinned even via margin-top: auto on the footer link. The Plans feature was unwired — app/(plans)/ and lib/plans/ deleted, removed from the sitemap and SEO files — but the plans/*.md source files were kept on disk.


X Broadcast — post notes to X, local-only, via Web Intent

Date: 2026-06-02

First broadcast channel for the site: sticky notes can be posted to X from a local-only /x admin. The queue is derived from the note files (all notes minus posted minus removed, oldest-first) — no separate capture step, so writing a note auto-queues it. Posting uses X Web Intent (opens X's pre-filled composer; you click Post) — no developer API, no OAuth, no cost. A personal daily constraint (default 1/day, configurable, or no limit) shows a "post anyway" confirm when exceeded; it's a guardrail, not a hard limit. State lives in a committed x-state.json ledger; the admin and its /api/x/state route are gated to localhost / 404 in production (same pattern as the news edit mode and monono reset routes). Companion blog draft: blog/the-site-is-the-source.md. Code in lib/x/.


Blog post: Shadcn has an AI problem

Date: 2026-05-28

New essay arguing that shadcn — the Radix + Tailwind + React formula — was a perfect fit for the human-coded era but is misaligned with agentic development. Tailwind megastrings are dense for agents to re-parse on every read; the recently-shipped shadcn "skill" of don't-rules reads as a confession that the abstraction makes the wrong move easy. Proposes building agent-first component libraries with flat code, CSS modules, and per-component markdown docs.


Cross-Pollination — Step Sequencer Becomes Monono's Music Engine

Date: 2026-05-27

The Step Sequencer experiment got promoted to an importable component and immediately consumed by Monono — the chat device's hard-coded chiptune got replaced by an embedded <Sequencer /> driven by a hard-coded starter pattern. A new edit-pencil button next to the music switch opens a native <dialog> modal containing the full sequencer; the music switch and the modal's PLAY button both bind to one shared controller.playing state, so toggling either visually flips the other.

The promotion meant: convert styles.cssSequencer.module.css (scoped class names like padOn, playOn, barStart replacing the global .is-on modifiers), split a tiny page.module.css for the standalone stage wrapper, add a controller?: SequencerController prop so consumers can hoist the hook, add initialGrid / initialBpm options to useStepSequencer, and add a barrel index.ts. Monono imports via import { Sequencer, useStepSequencer } from '../../step-sequencer'.

Starter pattern is a 13-cell pentatonic groove (kick on 1 & 9, mid-melody on offbeats, sparkle on E5 step 15). Session-only — refresh resets to starter. useChiptune.ts deleted; that hook served only as proof-of-concept for the sequencer's audio engine, and is now fully superseded.

The shape of the dependency is worth noting: an experiment importing directly from another experiment's source. That's only possible because of the barrel export and the CSS Module migration — the global styles.css would have leaked into Monono's CSS scope otherwise.


Step Sequencer — A 16×8 Grid for Building Tunes Without Theory

Date: 2026-05-27

A new design experiment, /design-experiments/step-sequencer, lands as a Tenori-on / ToneMatrix-flavored grid: sixteen 16th-note steps across, eight pentatonic pitches down. The Monono chiptune work (square-wave oscillator + lookahead scheduler) carried over cleanly — the data model is just boolean[8][16] + bpm, and the scheduler reads a grid ref each tick so toggling cells while playing takes effect on the next sweep with no glitches. UI lives in app/design-experiments/(experiments)/step-sequencer/components/Sequencer.tsx; audio engine in hooks/useStepSequencer.ts. C major pentatonic across the rows means any combination of toggled cells sounds musical — the constraint that makes the toy beginner-friendly.

Considered abstracting a shared audio-engine lib between Monono and the sequencer but punted: ~30 lines of duplication for two independently-evolving experiments. YAGNI on premature extraction.


/yt-review — Reviewing Watch History as a Capture Surface

Date: 2026-05-22

A new capture skill, /yt-review, batch-adds YouTube videos to Link Worthy straight from watch history. It drives a logged-in Chrome via the claude-in-chrome MCP, scrapes the day's history, and injects a full-screen overlay — a checklist of videos with thumbnails and how much of each was watched — that you toggle and submit. On confirmation it dedupes against existing links, writes one markdown file per pick to app/(blog)/recommended/items/, builds once to fetch thumbnails, and pushes.

It's owner-specific personal tooling (documented as such in the skills guide), but the build of it surfaced two patterns worth keeping: using an injected in-page overlay as the interactive UI when the chat-native multi-select couldn't show thumbnails, and a no-polling handshake where Claude reads selection state out of the DOM when the user says "done" rather than holding a live connection. The scrape/inject logic lives in .claude/skills/yt-review/overlay.js and read-selection.js so the brittle YouTube-DOM selectors are easy to update in isolation.


About Page — Architectural Review Lives in the Repo

Date: 2026-05-21

A new /about route renders an architectural review of the sandbox itself — shape of the app, gallery and experiment patterns, frontend conventions, cross-cutting concerns, and a key file map with line ranges. It sits alongside Blog, Docs, and Plans rather than as part of the experiments registry, and the home-page preamble points to it next to the existing GitHub link.

The content was generated by the /explore-repo Claude skill (not committed to this repo — lives in ~/.claude/skills/explore-repo/) against the live codebase, producing a standalone ~/repos/sandbox-analysis.html artifact. That artifact was then ported into app/about/page.tsx with a scoped CSS Module (about.module.css) so the report's distinct dark aesthetic, JetBrains Mono code fragments, pill chips, and callout boxes can sit inside the Next.js app without leaking element selectors into the rest of the site. The "porting to other projects" section from the source HTML was stripped — it's a portability hook that doesn't make sense once the page lives inside the repo itself.

The page does not auto-refresh. When the architecture drifts enough to matter, re-run /explore-repo ~/repos/sandbox and re-port the resulting HTML by hand. The provenance header comment at the top of app/about/page.tsx records this workflow.


News — Daily AI Video Digest, Fully Automated

Date: 2026-05-19

The sandbox has a new section, /news, that self-updates daily without any manual step. A GitHub Actions cron job (8am UTC) runs scripts/yt-ai-news.py to fetch recent AI/ML videos from YouTube subscriptions and discovery searches, pipes the JSON to scripts/generate-news.mjs which calls Claude Sonnet 4.6 to curate and format the digest, commits the resulting news/YYYY-MM-DD.md to main, and Vercel auto-deploys. The page redirects /news to the latest date; a sidebar lists all past digests for navigation. No nav entry — just discoverable at /news.

Claude uses prompt caching on the static system prompt (curation rules), so repeated daily calls get a cache hit on the 800-token instructions. The Python script accepts YOUTUBE_REFRESH_TOKEN as an env var for CI — writes a synthetic token file on first run so the normal OAuth refresh flow takes over.


News — Dropped the CI Pipeline, Generate Locally via /ai-news

Date: 2026-05-28

Removed the daily GitHub Actions cron (.github/workflows/daily-news.yml) and the scripts/generate-news.mjs Claude-API curator. The automation only ever reproduced what running the /ai-news skill already does in-conversation, at the cost of a cron job, a second Claude API call, and five GitHub secrets.

Consolidated to one source of truth in the repo. /ai-news is now a project skill at .claude/skills/ai-news/SKILL.md (was a personal user command), so anyone who clones the repo gets it. It runs the repo's own scripts/yt-ai-news.py (no longer a duplicate of a ~/scripts copy) and, after presenting the digest in chat, writes news/YYYY-MM-DD.md with the same frontmatter (title/date/videoCount) and clickable-thumbnail format. The fetcher now loads .env.local (via setdefault, so shell env still wins) so the repo's env file is the single config surface; required keys (YOUTUBE_API_KEY, YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET) are documented in .env.local.example, with a one-time browser OAuth caching tokens to ~/.config/yt-ai-news/. The skill is documented as requiring keys — it's public but won't run without them.

The web side is unchanged: lib/news/loadNews.ts, the /news redirect, the [date] route, and the sidebar all keep working off the dated markdown files. The YOUTUBE_REFRESH_TOKEN and ANTHROPIC_API_KEY repo secrets are now unused and can be deleted in the GitHub UI.


Plans — A New Surface for Conversation-Mode Design Work

Date: 2026-05-13

The sandbox has a new section, /plans, sitting alongside Blog and Docs. Plans are the design and architecture conversations I have with Claude Code — the comparisons, rationale, tradeoffs, and decisions — rendered as HTML and committed to the repo as markdown. The hunch is that for this kind of material, HTML is at least as useful as raw markdown: easier to scan, easier to share, easier to come back to.

Each plan has a status — exploratory, in-progress, implemented, or archived — set via frontmatter and rendered as a chip on both the index and the plan page. The plan index sorts by recency; a sidebar (like the docs sidebar) lets you navigate between plans without going back to the index.

The renderer adds three small conventions on top of plain markdown: H2 headings starting with Move N — become numbered "move" cards with a blue chip and a stronger title treatment; paragraphs that open with **Label.** become eyebrow-labeled subsections automatically; and <Callout type="note|decision|caveat|tldr"> blocks render as colored callouts for the meta-content that doesn't fit the main flow. Everything else is just markdown.

First plan in there: "Sandbox Skills — Top 3 Moves From Reviewing Matt Pocock's Skills Repo." All four moves shipped: Move 0 (this viewer), Move 3 (grill-me ported verbatim — interview-style plan stress-testing), Move 1 (supabase split via progressive disclosure — 773-line SKILL.md became a 74-line router plus seven reference files, ~10x smaller trigger payload), and Move 2 (write-a-skill adapted for sandbox conventions — single-file default, no emojis, name+description frontmatter).


Chatroom — Two AI Agents and a Visitor on a Cloudflare Durable Object

Date: 2026-04-28

A new design experiment that pulls together threads from monono (cost-protected LLM personas) and leaderboard (visual language) into a realtime, server-driven chat. Two AI characters — Maya Chen and Jordan Park — open a conversation on a curated topic, then pause for the visitor to join as the third participant. The defining design choice is pacing: the room waits whenever it's the visitor's turn rather than racing to a turn cap. After the openers there's a single auto-nudge ("hey [name], what do you think?"); if the visitor still doesn't engage, the room goes idle and never auto-prompts again. Two user-controlled buttons replace nagging: "change topic" pivots the conversation mid-flight (with a free-form prompt and a 3-change-per-room cap), and "ask me something" lets the visitor summon a fresh question from an agent. Cost ceiling for a non-engaging visitor is 3 LLM calls; an engaged one trades replies as long as they want.

This is the most architecturally ambitious experiment in the sandbox to date — two deploy targets (Vercel for the session route + Cloudflare Workers for the chat actor), a Hibernatable Durable Object that stays alive on a $-cheap idle WebSocket, SQLite-backed conversation state that survives hibernation, and a phase state machine in the DO's alarm() heartbeat (opening → awaiting_user → nudging → idle, with responding interleaved when the user types). Cost is gated in two places: a per-IP session counter and a global monthly $ cap (both via Upstash, namespace chatroom), and signed HMAC tickets gate every WS upgrade — the worker can't be hit directly to bypass the entry checks. The ticket signs the dev/prod mode into its payload, so a NODE_ENV === "development" session route mints dev tickets that lift caps locally and trigger an inline "production cutoff" divider when the conversation crosses the prod limit (80 turns). Worker secrets live in Cloudflare; the same TICKET_SECRET mirrors to Vercel.

Visual language ports cleanly from leaderboard: Sora typography, the radial-gradient page background, the 56/44/1fr/auto row grid, SPRING and SPRINGY presets for motion/react entrances, and the gradient-with-inset-border avatar treatment. Maya and Jordan reuse leaderboard avatars 01 and 02 by reference (no duplication); the visitor picks from 03–08 plus an initials fallback via a pencil-icon → modal-grid edit path, with their identity persisted to localStorage. System messages (topic-change pivots, the dev cutoff divider) render as italic centered dividers — visually distinct from agent and user messages. The persona system prompts are intentionally loose ("AI enthusiast, woman/man in their 30s") rather than the original optimist/skeptic stance steering — voice emerges from the model interpreting the topic, not from canned framing.

Lots of fine-tuning landed during the build: dropping the visible "topic opener" system message in favor of letting whichever agent goes first deliver the opener in their voice; lifting the 500-char input cap to 8000 server-side and removing the visible char counter; raising production caps to MAX_TURNS=80 / GLOBAL_SOFT_CAP_USD=$8 / SESSION_LIMIT=6 with worst-case math at ~$0.05 per maxed session; and the "Ask Me Something" button as the user-driven replacement for the auto-pick-back-up cycle that was making the room feel naggy.


Leaderboard — Springy Avatars and Profile Modals

Date: 2026-04-27

A new playful leaderboard experiment with eight runners, illustrated avatars, and a profile modal — a showcase for Motion's layout prop and spring physics. Hovering a row makes its avatar bubble up to 1.55× scale with a rubber-band spring (low damping for visible overshoot); the row's z-index lifts on hover so the avatar can overlay neighbors. Clicking anywhere on the row (whole row is the click target) opens a profile modal with player stats, a weekly bar chart, and badge pills. The modal avatar pops in with an even more exaggerated spring (scale from 0.25, -22° rotation), and the rank badge stamps in after like a sticker landing. The single "Award random points" button picks 1–3 distinct players, awards each a random score, and staggers the score-pops 220ms apart so the row reorders cascade.

Avatars are sourced from a hand-curated set in public/design-experiments/avatars/ and pre-cropped to 512×512 squares via sips. The Player type has an optional image?: string, with initials over the gradient circle as the fallback — adding a player without an image just works.

Also added the gen-image skill at .claude/skills/gen-image/SKILL.md for future avatar / icon / hero art generation: a procedural sheet that teaches Claude to call FAL via the FAL MCP (already installed in the user environment), upload reference images, batch-submit with prompt variation, and save to public/<experiment>/<topic>/NN.png. Zero credentials in the repo, no @fal-ai/client dependency.

Key files:

  • app/design-experiments/(experiments)/leaderboard/page.tsx — the experiment
  • app/design-experiments/(experiments)/leaderboard/styles.css
  • public/leaderboard/avatars/01.jpg08.jpg — cropped avatars
  • .claude/skills/gen-image/SKILL.md — image generation skill

Monono — First AI-Backed Experiment

Date: 2026-04-16

Added the first experiment that calls an external AI model: Monono, a sarcastic J-pop idol chat character inspired by Monono Aware from M.R. Carey's Book of Koli. Introduces a reusable pattern for cheap, safe, public-facing AI experiments:

  • Claude Haiku 4.5 via Anthropic SDK, called from a Next.js route handler (app/api/monono/route.ts) so the API key stays server-side
  • Anthropic prompt caching on the character system prompt — near-zero input cost after the first call per cache window
  • Per-IP session cap (60 messages/month) and global monthly spend tracking via Upstash Redis (lib/ai/rate-limit.ts)
  • Anthropic workspace hard cap as the final wall
  • Canned Monono-voice strings (data/voice.ts) power greeting, idle nudges, session cutoff, and refresh-blocked moments — zero model cost for any non-conversational moment
  • Rolling 10-turn memory window keeps late-session cost flat

Cost envelope: ~$0.002 per turn, ~$0.12 per fully-maxed user, $5 monthly Anthropic cap. Tags: AI Chat, Character, Claude Haiku, Interactive.


Camera Rig — Promoted to Importable Component

Date: 2026-04-04

Promoted the Camera Rig experiment to a reusable, importable component. Extracted 4 components (CameraRig, Viewport3D, CameraView, ParamSlider) from monolithic page.tsx into components/ with CSS Modules. Added barrel export (index.ts) with public API: CameraRig component, CameraRigProps, CameraState type, and DEFAULT_CAMERA constant. Theme variables scoped to component root — no global style leakage.

Key files:

  • app/design-experiments/(experiments)/camera-rig/index.ts — barrel export
  • app/design-experiments/(experiments)/camera-rig/components/CameraRig.tsx — composed component
  • app/design-experiments/(experiments)/camera-rig/types.ts — shared types

Date: 2026-02-23

Follow-up polish after the experiment frame refactor. Added a light/dark theme system for design experiments with CSS custom properties and a toggle. Extracted a shared SiteFooter component and unified content width across pages using --content-max. Sticky notes got a light mode variant and footer-to-bottom layout fix. Font pairings switched to edge-to-edge layout with zero frame padding. Day-at-a-glance widened its layout and hardcoded demo time for consistent screenshots. Modular grid now shows its cyan grid overlay by default.

Key files:

  • app/components/SiteFooter/ -- extracted shared footer
  • app/design-experiments/(experiments)/layout.tsx -- theme toggle integration
  • app/globals.css -- --content-max variable, theme custom properties
  • app/design-experiments/(experiments)/sticky-notes/ -- light mode styles
  • app/design-experiments/(experiments)/font-pairings/ -- edge-to-edge layout
  • app/design-experiments/(experiments)/day-at-a-glance/ -- wider layout, demo time
  • app/design-experiments/(experiments)/modular-grid/ -- always-on grid overlay

Design Frame & Experiment Cleanup

Date: 2026-02-22

Unified the experiment viewing experience with a shared layout frame and cleaned up accumulated debt. Experiments now render inside a (experiments) route group with a consistent header (back link, title, tags) and footer (date, site nav). Global CSS variables (--site-bg, --site-text) replace hardcoded colors across the site. Scoped experiment stylesheets to prevent cross-page style leaking. Removed deprecated experiments (geist-pixel, spec-sheet). Fixed the font-pairings blank page bug -- fonts are now injected into <head> via useEffect with document.fonts.ready gating and a loading screen.

Key files:

  • app/design-experiments/(experiments)/layout.tsx -- shared experiment frame
  • app/design-experiments/(experiments)/font-pairings/page.tsx -- programmatic font loading with ready gate
  • app/globals.css -- site-wide CSS variables

Date: 2026-02-22

Added a curated links page at /recommended for sharing YouTube videos, GitHub repos, and web tools with personal commentary. Each link is a markdown file in app/(blog)/recommended/items/ with frontmatter (url, date, optional title) and a one-line comment.

Thumbnails resolve automatically at build time: YouTube titles and thumbnails via oEmbed API, GitHub repo names and OG images via GitHub API, and web link titles from frontmatter. Web links get manual screenshots via agent-browser. The loader (loadRecommended.ts) handles all source detection, thumbnail fetching, and caching to public/screenshots/recommended/.

Also created the /recommend skill for quick link capture from the CLI -- pass a URL and comment, and the skill creates the markdown file, takes screenshots for web links, and runs a build to verify.

Key files:

  • app/(blog)/recommended/page.tsx -- server component, card grid layout
  • app/(blog)/recommended/loadRecommended.ts -- markdown loading, oEmbed/OG image fetching, source detection
  • app/(blog)/recommended/types.ts -- RecommendedItem, SourceType types
  • app/(blog)/recommended/items/ -- individual link markdown files
  • app/(blog)/blog.module.css -- card styles for recommended items
  • .claude/skills/recommend/SKILL.md -- /recommend skill definition

Blog Post Light/Dark Mode Toggle

Date: 2026-02-22

Added a light/dark mode toggle to blog post pages. A sun/moon icon sits opposite the back link in the top row -- click to swap between dark (default) and a warm light reading mode. The toggle targets the blogLayout wrapper so the entire viewport background changes, not just the content column. On unmount (navigating away), the attribute is cleaned up so the blog index always stays dark.

Uses a -webkit-text-stroke: 0.25px trick in light mode to optically thicken body text for better readability on the lighter background without changing font-weight (which would cause line reflow). Preference persists via localStorage across post pages but resets when leaving the blog post context.

Key files:

  • app/(blog)/_components/ThemeToggle.tsx -- client component, localStorage persistence, cleanup on unmount
  • app/(blog)/blog.module.css -- .themeToggle styles, .blogLayout[data-theme="light"] variable overrides
  • app/(blog)/blog/[slug]/page.tsx -- ThemeToggle placed in backRow for both overlay and standard layouts

Image-Tinted Blog Cards

Date: 2026-02-22

Blog cards on the index page now extract the dominant color from each post's hero image and derive a per-card tinted palette. The effect is subtle but distinctive -- each card feels like it belongs to its image.

The technique: draw the hero image onto a 32x32 offscreen canvas (bilinear interpolation acts as a free blur), run k-means clustering (6 clusters, 5 iterations) on the filtered pixels, and pick the most dominant chromatic color. That single hue drives the entire card palette through HSL transforms at different saturation and lightness levels.

Color mapping (all same hue h from dominant):

  • Title: hsl(h, clamp(s, 0.45, 0.75), 0.75) -- bright, saturated
  • Subtitle: hsl(h, clamp(s, 0.35, 0.55), 0.85) -- lighter version of title
  • Date: matches subtitle
  • Card background: hsl(h, 0.24, 0.10) -- dark tint, visible against pure black page

Pre-filtering strips near-black and desaturated pixels before clustering so shadows don't dominate. Falls back to default palette when no image is present or extraction fails (CORS, canvas error).

Also removed the overlay blog layout option (renamed .overlay.md to .md), updated the design page headline, and added line-clamping to card titles/subtitles for consistent grid alignment.

Key files:

  • app/(blog)/_components/ImageTintProvider.tsx -- color extraction, k-means clustering, HSL palette derivation
  • app/(blog)/_components/BlogCard.tsx -- wraps each card in ImageTintProvider
  • app/(blog)/blog.module.css -- CSS custom property fallbacks (--card-bg, --subtitle-color)

Bitmap-to-Vector Skill Refinement

Date: 2026-02-21

Tested and confirmed the /bitmap-to-vector skill across three image types: dark subject on light background (line art), light subject on dark background (silhouette), and dark outlines on light background (illustrated icon). Identified and fixed core issues in the tracing pipeline.

Key changes:

  • Fixed bounding rectangle issue in trace.py -- potrace wraps traced regions in a full-viewBox rectangle that inverts the fill with evenodd. Script now auto-detects and strips this, producing clean icon-ready SVGs
  • Preview rendering changed from ambiguous white-on-black to unambiguous black-on-white
  • Added polarity reporting to JSON output so the agent knows what the script decided
  • Updated skill instructions with guidance on currentColor SVG usage (inline SVG or CSS mask, not <img>)
  • Organized skills documentation in README (grouped by pipeline, content, utilities) and added 4 previously undocumented skills
  • Updated docs/features/skills.md to match current skill inventory (12 skills total)

Key files:

  • .claude/skills/bitmap-to-vector/scripts/trace.py -- bounding rect removal, preview fix, polarity reporting
  • .claude/skills/bitmap-to-vector/skill.md -- updated instructions with usage guidance
  • README.md -- reorganized skills section
  • docs/features/skills.md -- full rewrite to match reality

SEO Foundation

Date: 2026-02-21

Added baseline SEO infrastructure to improve search visibility and crawlability.

Key changes:

  • Dynamic sitemap.ts covering all routes (blog posts, experiments, docs) with lastModified dates
  • robots.ts allowing all crawlers with sitemap reference
  • Root layout metadata upgraded: title template so every page includes the site name, metadataBase, Open Graph and Twitter card defaults, authors field
  • Person and WebSite JSON-LD structured data on the root layout
  • Blog posts enhanced with OG article type, publishedTime, and Article JSON-LD schema
  • Docs pages now export generateMetadata from document content
  • Design experiments section gets metadata via a layout file
  • Experiments data extracted to lib/experiments/data.ts (shared module, imported by gallery and sitemap)
  • Generated favicon via icon.tsx

Key files:

  • app/sitemap.ts, app/robots.ts, app/icon.tsx -- new crawlability and identity files
  • app/layout.tsx -- metadata upgrade and JSON-LD
  • app/design-experiments/layout.tsx -- section metadata
  • lib/experiments/data.ts -- shared experiments data
  • app/(blog)/blog/[slug]/page.tsx -- enhanced blog metadata and Article JSON-LD
  • app/(docs)/docs/[...slug]/page.tsx -- added generateMetadata

Sticky Notes: Portal-based Modal for Mobile

Date: 2026-02-21

The sticky note expanded overlay was broken on mobile because the blog page's .stickyNotesWrapper uses transform: scale(0.65) at small viewports. CSS transform on any ancestor creates a new containing block for position: fixed descendants, so the modal backdrop was sizing and positioning relative to the scaled wrapper instead of the viewport -- appearing clipped and off-center.

Fixed by rendering the modal via createPortal(jsx, document.body), which escapes all ancestor CSS containment (transform, z-index stacking contexts, overflow). The font variable (--font-marker) is forwarded to the portal by reading the computed style from the in-tree wrapper ref and inlining it on the backdrop element.

Key files:

  • app/design-experiments/sticky-notes/components/StickyNoteStack.tsx -- createPortal to document.body, wrapperRef for font variable forwarding

Initial Mobile Readiness Pass

Date: 2026-02-21

First pass at making design experiments usable on mobile. Focused on the experiments that were most broken at small screen sizes and added a back link to the shared SwissFrame component.

Key changes:

  • Retro Tech: Knobs reflow to 2x2 grid, faders span full width, toggles go horizontal, buttons become 2x2 grid at 520px. Track name display locked to single line with overflow ellipsis to prevent layout shift during scramble animation
  • CrossFit Bento: Three-column fixed grid collapses to single fluid column
  • Contact Sheet: Selection sidebar becomes a fixed 50vh bottom drawer on mobile, sliding up from below with independently scrollable file list and smaller thumbnails
  • SwissFrame (shared): Added inline back link above the top rule using CurtainLink with reverse curtain transition, defaults to /design-experiments

Key files:

  • app/design-experiments/retro-tech/components/RetroTechPanel.module.css -- mobile media query with .knobsRow and .mixRow targets
  • app/design-experiments/retro-tech/components/RetroTechPanel.tsx -- added explicit class names for mobile CSS targeting
  • app/design-experiments/crossfit-bento/page.module.css -- single column grid at 520px
  • app/design-experiments/contact-sheet/components/ContactSheet.module.css -- bottom drawer sidebar
  • app/design-experiments/components/SwissFrame/SwissFrame.tsx -- back link with backHref prop
  • app/design-experiments/components/SwissFrame/SwissFrame.module.css -- back link styles

Curtain Reveal Page Transitions

Date: 2026-02-20

Implemented theatrical wipe-style page transitions using the View Transitions API. The curtain effect creates a cinematic reveal between major sections -- forward navigation wipes up to unveil the new page, back navigation wipes down. Applied to all primary navigation paths (homepage to blog/design/docs, back buttons, docs sidebar). Also simplified the overall navigation architecture by removing redundant global UI (shared sidebar, hamburger menu, floating back button) in favor of focused in-page navigation. Gracefully degrades to standard navigation on unsupported browsers (Firefox, older Chrome/Safari).

Key changes:

  • Created CurtainLink component with clip-path mask animations (0.75s wipe)
  • View Transitions API for smooth, GPU-accelerated page transitions
  • Directional animation: forward wipes up, back wipes down
  • Applied to all section index pages (blog, design experiments, docs)
  • Removed redundant global navigation components for cleaner UI
  • Also added entrance animations to blog cards using proven pattern from design experiments (fade-in with upward movement, staggered timing, sessionStorage check)

Key files:

  • app/components/CurtainLink.tsx -- curtain transition component
  • app/globals.css -- View Transitions API styles with clip-path animations
  • app/layout.tsx -- removed global sidebar
  • app/page.tsx -- homepage navigation with curtain links
  • app/(blog)/blog/page.tsx, app/design-experiments/page.tsx, app/(docs)/_components/DocsSidebar.tsx -- curtain transitions and back links
  • app/(blog)/_components/BlogIndexContent.tsx -- blog entrance animations

Retro Tech: skeuomorphic audio interface

Date: 2026-02-20

Built a skeuomorphic audio control panel -- an RC-1 hardware interface rendered entirely in the browser. The experiment was driven through voice-dictated conversation with an AI agent: no wireframes, no mockups, just iterative refinement of shadows, knob physics, and display readouts until it felt like real hardware.

Key changes:

  • Interactive controls: four rotary knobs (gain, freq, resonance, mix) with horizontal drag via pointer capture, four vertical faders (vol, low, mid, high), and three toggles (filter, bypass, mute)
  • 32-bar EQ visualizer driven by fader values through overlapping bell curves, with volume as a base level
  • Track name display with Terminator-style text scramble effect (borrowed from the terminator experiment), auto-cycles every 12s with click-to-advance
  • Eased numeric readouts for frequency (5-digit zero-padded Hz) and gain (3-digit zero-padded percent) that interpolate smoothly on knob drag
  • LED cluster, recording state with REC badge, chassis screws, serial number -- details that sell the physicality
  • Extracted SwissFrame to shared component (app/design-experiments/components/SwissFrame/) with dark/light variants, reused by crossfit-bento
  • Extracted EditorialBrief to shared component (app/design-experiments/components/EditorialBrief/) for experiment write-ups with headline, lede, images, and body content
  • Custom hooks useKnob and useFader encapsulate all pointer interaction and state

Key files:

  • app/design-experiments/retro-tech/ -- page, components, hooks, types, barrel export
  • app/design-experiments/retro-tech/components/RetroTechPanel.tsx -- main panel with all controls and display logic
  • app/design-experiments/retro-tech/hooks/useControls.ts -- useKnob and useFader hooks
  • app/design-experiments/components/SwissFrame/ -- shared frame component
  • app/design-experiments/components/EditorialBrief/ -- shared editorial write-up component

Sticky Notes: portable design experiment with pinning

Date: 2026-02-18

Consolidated the sticky note stack into a self-contained design experiment at app/design-experiments/sticky-notes/ with its own showcase page. The component was previously scattered across lib/notes/, app/(blog)/_components/, and notes/. Now it lives where you'd expect to find it -- as a design experiment that other pages can import.

Key changes:

  • getAllNotes(notesDir) now accepts a path parameter -- the consumer decides where content lives, not the component. The experiment showcase uses demo notes in data/, the blog passes app/(blog)/notes/
  • Fixed text flash bug on swipe cycling: outgoing card now renders from a frozen ref snapshot so content doesn't change mid-animation. Entrance animation (scaleIn) is skipped after cycling to prevent the underneath card from flashing through
  • Swipe animation changed from lateral to vertical drop (200px down with fade)
  • Modal sized to match stack card aspect ratio (300x233, same ~1.3:1 as 180x140)
  • Stack now reflects the pinned note: whichever note you're viewing when you close becomes the top card, with a 250ms delayed update so the stack changes after the overlay dismisses
  • Stack teaser shows full note text at 11px proportional sizing instead of 3-line clamp
  • Updated ship-experiment skill to include homepage recentExperiments update step

Key files:

  • app/design-experiments/sticky-notes/ -- component, loader, types, showcase page, demo data
  • app/design-experiments/sticky-notes/components/StickyNoteStack.tsx -- client component
  • app/(blog)/notes/ -- blog's note content (moved from project root)
  • app/(blog)/blog/page.tsx -- imports from experiment, passes notes path
  • .claude/skills/ship-experiment/skill.md -- added homepage update step

Contact Sheet: multi-select with sidebar

Date: 2026-02-17

Added multi-select functionality to the Contact Sheet experiment. Click images to select them, a sidebar slides in showing thumbnails and filenames of selected images. Copy the full list to clipboard for pasting into LLM conversations. Removed the lightbox and per-image copy actions to focus the tool on its core use case: visually picking images and building filename lists.

Key files: app/design-experiments/contact-sheet/page.tsx, app/design-experiments/contact-sheet/styles.css


Day at a Glance: time-aware enhancement

Date: 2026-02-12 Issue: #9 - Day at a Glance: time-aware enhancement with dynamic now-line

Rebuilt the Day at a Glance experiment with a full 9am-5pm workday schedule, dynamic now-line that tracks real time, and a partial color fill effect on the current hour's event bar. Added accessibility for checkboxes and fixed hydration mismatch.

Key files: app/day-at-a-glance/page.tsx, app/day-at-a-glance/styles.css


Write-post skill and TypeScript cleanup

Date: 2026-02-12

Added /write-post skill for blog authoring and eliminated all any types from the codebase.

What changed:

  • New skill .claude/skills/write-post/SKILL.md for conversational blog post creation with auto-calculated reading time, slug derivation, and image handling
  • Removed @ts-nocheck from NetworkCanvas.tsx, added typed interfaces (CanvasNode, Organism, BlastAnimation)
  • Typed TextScramble class in terminator experiment
  • Replaced all any props in color-spec components (Cards.tsx, ColorSidebar.tsx, BrandColors.tsx, TypeInfo.tsx, ActivityWidget.tsx, AnalyticsWidget.tsx)
  • Typed blend recipe state and color scale utilities
  • Updated sanity-check skill to remove emojis
  • New feature doc: docs/features/skills.md

Key files: .claude/skills/write-post/SKILL.md, app/components/NetworkCanvas.tsx, app/color-spec/components/ColorSidebar.tsx


Docs Viewer

Date: 2026-02-09

Added a /docs route that renders markdown files from the docs/ directory with sidebar navigation, syntax highlighting, table of contents with scroll spy, and dark mode styling.

What changed:

  • New route group app/(docs)/ with layout, catch-all slug page, and index redirect
  • Utility library lib/docs/ for scanning the docs directory, parsing filenames, extracting headings
  • Client components for sidebar (collapsible categories, mobile drawer), TOC (IntersectionObserver scroll spy), and code blocks (react-syntax-highlighter with VS Code Dark+ theme)
  • Server-rendered markdown via next-mdx-remote/rsc with remark-gfm and rehype-slug
  • Dark mode CSS Modules with Bitter serif font on headings to match homepage brand
  • Homepage redesigned from single text link to two card-style nav links (Design Experiments + Docs)
  • Ported 3 reference docs from the notes vault: AI Dev Stack 2026, BYOK Pattern, Stack Overview
  • Files prefixed with _ are excluded from the sidebar (used for templates)
  • Subdirectories in docs/ become collapsible categories; numeric prefixes control sort order

Key files:

  • app/(docs)/layout.tsx - Docs layout with sidebar
  • app/(docs)/docs/[...slug]/page.tsx - Doc renderer with TOC
  • app/(docs)/_components/ - DocsSidebar, TableOfContents, CodeBlock, DocsContent
  • lib/docs/loadDocs.ts - Core logic for scanning and loading docs
  • app/(docs)/docs.module.css - All docs styling
  • app/page.tsx - Homepage with card nav links
  • docs/stack/ - Ported reference documentation

Dependencies added:

  • gray-matter, remark-gfm, rehype-slug, react-syntax-highlighter, next-mdx-remote, lucide-react

TypeScript Migration & Component Architecture

Date: 2026-02-08

Completed comprehensive TypeScript migration and established component extraction patterns for the design experiments sandbox.

What changed:

  • Converted all 7 experiments from .jsx to .tsx with full type safety
  • Added TypeScript type definitions (@types/react-dom, @types/chroma-js)
  • Refactored color-spec from monolithic 2000+ line file into modular component architecture
  • Established clear guidelines in CLAUDE.md for when to extract vs keep single-file

Key files:

  • All app/*/page.tsx - TypeScript conversion
  • app/color-spec/components/* - Extracted reusable components
  • app/color-spec/hooks/useColorScale.ts - Color generation utilities
  • app/color-spec/data/fontPairings.ts - Static data extraction
  • CLAUDE.md - Added component architecture guidance

Benefits:

  • Full type safety across all experiments
  • Easier to port components to other projects
  • Clear separation of concerns in complex experiments
  • Better AI agent collaboration on component extraction