This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
npm run dev # Start development server
npm run build # Build for Vercel deployment
npm run typecheck # Run TypeScript type checking (no emit)There is no test suite. Use typecheck to validate changes.
ColorArchive is a Next.js app (App Router) deployed to Vercel. Backend API runs on a DigitalOcean Droplet (Express + SQLite).
src/data/colors.ts— Generates all 5,446 colors algorithmically (48 hue roots × 14 lightness bands × 8 chroma bands + 5 neutral gray groups × 14). Never stored externally.src/lib/color-utils.ts— Pure functions for HSL↔RGB↔HEX conversion, color family classification, filtering, sorting, and finding color relationships (analogous, complementary, tonal companions).src/types/color.ts— CoreColorRecordinterface and enums (ColorFamily,SortOption).
All 5,446 color IDs are generated algorithmically. Do NOT invent color IDs. Only use IDs that match these exact patterns:
Chromatic colors (5,376): {root}-{lightness}-{chroma}
- 48 roots: Crimson, Scarlet, Ruby, Vermillion, Ember, Tangerine, Coral, Apricot, Saffron, Amber, Canary, Citrine, Honey, Chartreuse, Olive, Lime, Moss, Leaf, Clover, Emerald, Mint, Seafoam, Celadon, Jade, Teal, Lagoon, Cyan, Aqua, Cerulean, Azure, Steel, Sapphire, Cobalt, Indigo, Iris, Amethyst, Violet, Orchid, Plum, Mulberry, Magenta, Fuchsia, Mauve, Peony, Rose, Blush, Garnet, Merlot
- 14 lightness bands: Veil (98), Whisper (94), Mist (90), Pearl (84), Bloom (76), Silk (68), Tone (60), Radiant (54), Core (48), Velvet (42), Dusk (34), Shadow (28), Nocturne (20), Ink (14)
- 8 chroma bands: Faint (10), Muted (18), Dust (26), Soft (34), Clear (54), Vivid (74), Bright (84), Pure (92)
- Examples:
amber-pearl-muted,cobalt-shadow-vivid,steel-bloom-dust,scarlet-core-bright
Neutral grays (70): {root}-{lightness} — NO chroma suffix
- 5 roots: Warm Gray, Taupe Gray, True Gray, Sage Gray, Cool Gray
- 14 lightness bands: same as above
- Examples:
warm-gray-whisper,cool-gray-shadow,taupe-gray-tone,sage-gray-bloom
Common mistakes to avoid:
- ❌
warm-gray-whisper-soft— neutrals have NO chroma suffix → ✅warm-gray-whisper - ❌
ivory-pearl-soft— "Ivory" is not a root → ✅ usecoral-veil-faintoramber-veil-faint - ❌
amber-deep-core— "Deep" is not a lightness band → ✅amber-shadow-clear - ❌
apricot-vivid-core— order is root-lightness-chroma, not root-chroma-lightness → ✅apricot-core-vivid
When creating collections, always verify color IDs exist by checking against this naming scheme. The build will fail with Unknown color id if any ID is invalid.
Pages in app/ are Next.js Server Components. Each page imports a corresponding *-page.tsx component from src/components/ that holds the actual UI and is marked "use client". This keeps the App Router data/metadata layer separate from interactive client logic.
Dynamic routes (e.g., app/colors/[slug]/page.tsx) use generateStaticParams() to pre-render all 3,066 color pages at build time.
src/lib/favorites.ts and src/lib/recent-colors.ts manage browser localStorage. Both use a subscription pattern (subscribeTo*() returns an unsubscribe function) with custom events for cross-component reactivity and StorageEvent for cross-tab sync.
src/lib/collections.ts— 68+ curated palette collections (editorial metadata + color IDs).src/lib/palette-packs.ts— 7 product pack definitions (USD $9–$129) + All Access bundle.src/lib/checkout-config.ts— Stripe Checkout config + Pro subscription pricing (¥499/mo, ¥3,999/yr).src/lib/auth-client.ts— Client API for auth, projects, usage stats, referral, API keys.src/lib/word-color.ts— Deterministic word→color hash algorithm (string hash → hue/saturation/lightness → 5 color variants).
Tailwind CSS 4 with utility-first classes. Key design patterns:
- Frosted glass panels via backdrop blur utilities
- Inset
swatch-shadowclass defined inapp/globals.cssfor color card depth sm:andlg:breakpoints for responsive layout
next.config.ts sets trailingSlash: true. The site is deployed to Vercel automatically on push to main. Backend API at api.colorarchive.me runs on a DigitalOcean Droplet via PM2.
This repo uses two concurrent Claude Code session types that must not run simultaneously:
- Autopilot: Automated scheduled runs (prefixed
[autopilot]) - Remote Control: Human-driven interactive sessions
Coordination uses .claude/session-lock.json. Every session MUST follow this protocol:
- Run
git fetch origin && git pull --rebase origin mainto sync with remote. - Read
.claude/session-lock.json - Check the lock state:
- If
activeisnull→ acquire the lock (see below). - If
lockedByis a different session type → STOP. Do not proceed. Print: "⏸ Session locked by {lockedBy} since {lockedAt}. Waiting." and exit without making changes. - If
lockedByis the same session type (e.g. autopilot sees an autopilot lock) → checklockedAttimestamp. If the lock is older than 20 minutes, it is considered stale (previous run likely crashed). Force-release the stale lock, rungit pull --rebase origin mainagain, then acquire a fresh lock. If the lock is less than 20 minutes old, STOP — the previous run is likely still active.
- If
Use Bash (not the Write tool) to write .claude/session-lock.json — this avoids permission prompts:
echo '{ "active": true, "lockedBy": "autopilot", "lockedAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", "message": "description" }' > .claude/session-lock.jsonEvery session MUST automatically release the lock after its final commit+push. This is not optional — do it immediately after pushing, before ending the session. Use Bash to reset .claude/session-lock.json:
echo '{ "active": null, "lockedBy": null, "lockedAt": null, "message": null }' > .claude/session-lock.jsonThen include this file in your single commit (see batching rule below).
Autopilot sessions:
- MUST run
git pull --rebase origin mainFIRST before doing anything - MUST check the lock SECOND
- If locked by "remote" → do nothing, exit gracefully, make zero changes
- If locked by "autopilot" and lock age > 20 min → stale lock, force-release and proceed
- If locked by "autopilot" and lock age ≤ 20 min → do nothing, exit gracefully (previous run still active)
- When starting, write lock with
"lockedBy": "autopilot"and"message"describing the planned run - After all work is done → release the lock and commit everything in a single commit+push (see batching rule below)
Remote Control sessions:
- MUST run
git pull --rebase origin mainFIRST before doing anything - MUST check the lock SECOND
- If locked by "autopilot" → inform user: "
⚠️ Autopilot is currently running: {message} (since {lockedAt}). Please wait for it to finish or manually clear.claude/session-lock.json." - If locked by "autopilot" and lock age > 20 min → inform user the lock looks stale, offer to force-release it
- When starting, write lock with
"lockedBy": "remote" - After commit+push → automatically release the lock (write null values + commit+push)
Every push to main triggers a full Vercel deployment that rebuilds 3,000+ static pages. To minimize wasted builds:
Autopilot MUST make exactly ONE commit and ONE push per run. The workflow is:
- Write
.claude/session-lock.jsonto acquire lock — do NOT commit or push yet - Do all content work (newsletters, guides, collections, aliases, etc.)
git addall content files- Update
autopilot-log.md,docs/autopilot-log.md,docs/human-todo.md— stage them - Write the null lock (release) to
.claude/session-lock.json— stage it - Single
git commitwith all changes (content + logs + lock release) - Single
git push→ triggers exactly ONE Vercel deployment
Do NOT make separate commits for lock acquire, content, logs, and lock release. This wastes 3 extra deployments per run.
A vercel.json ignoreCommand is configured as a safety net — if a push only contains metadata files (lock, logs, human-todo), Vercel will skip the deployment automatically.