add 3 new frontend skills + update CLAUDE.md and statusline

New skills: design-loop, design-system, walkthrough-video (frontend plugin).
CLAUDE.md updated: 59 -> 62 skills, inline-everything-critical guidance,
updated file structure docs. Statusline-npm improvements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcus Webb
2026-03-28 18:43:43 +11:00
parent 317b415e9e
commit 65d2a87cc3
13 changed files with 1824 additions and 55 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
},
{
"name": "frontend",
"description": "Tailwind v4 theming and shadcn/ui component installation, customisation, and recipes.",
"description": "Tailwind v4 theming, shadcn/ui, landing pages, design system extraction, autonomous multi-page site building, walkthrough videos, and React patterns.",
"source": "./plugins/frontend",
"category": "design"
},
+22 -12
View File
@@ -16,7 +16,7 @@ Production workflow skills for Claude Code CLI. Each skill guides Claude through
```
claude-skills/
├── plugins/ # 10 plugins, 59 skills
├── plugins/ # 10 plugins, 62 skills
│ ├── cloudflare/ # Cloudflare Workers, Hono, D1/Drizzle, Vite, TanStack Start
│ │ └── skills/
│ │ ├── cloudflare-worker-builder/
@@ -30,7 +30,7 @@ claude-skills/
│ ├── web-design/ # Local business SEO
│ │ └── skills/
│ │ └── seo-local-business/
│ ├── frontend/ # Tailwind v4 + shadcn/ui + landing pages + showcases + React
│ ├── frontend/ # Tailwind v4 + shadcn/ui + landing pages + showcases + React + design
│ │ └── skills/
│ │ ├── tailwind-theme-builder/
│ │ ├── shadcn-ui/
@@ -38,7 +38,10 @@ claude-skills/
│ │ ├── product-showcase/
│ │ ├── react-patterns/
│ │ ├── design-review/
│ │ ── react-native/
│ │ ── react-native/
│ │ ├── design-loop/
│ │ ├── design-system/
│ │ └── walkthrough-video/
│ ├── design-assets/ # Colour palettes, favicons, icons, image processing, AI images
│ │ └── skills/
│ │ ├── color-palette/
@@ -114,9 +117,10 @@ plugin-name/
│ └── plugin.json # name, description, author
└── skills/
└── skill-name/
├── SKILL.md # Frontmatter + instructions, under 500 lines
├── SKILL.md # Frontmatter + instructions (inline everything critical)
├── ERRATA.md # Optional: versioned corrections discovered during builds
├── references/ # Docs and example code loaded on demand by Claude
├── scripts/ # Executable scripts the agent RUNS (not reads)
├── references/ # Supplementary/variant docs (NOT critical path)
└── assets/ # Files used in output (templates, images)
```
@@ -153,17 +157,22 @@ Use [Anthropic's official skill-creator](https://github.com/anthropics/skills/bl
Key principle: **every skill must produce something.** If it's just reference material Claude already knows, it doesn't earn a place here.
### Skill Design: Patterns Over Scripts
### Skill Design: Inline Everything Critical
Skills should teach Claude the pattern so it can generate scripts adapted to the user's environment. Don't ship pre-built scripts unless the operation is genuinely complex and error-prone.
**If the agent skipping it would derail the workflow, it goes in SKILL.md.** Reference files are for genuinely optional material — variant-specific docs, supplementary examples, historical context. Anything on the critical path must be inline.
This was learned the hard way: an agent was told "see references/stitch-direct.md for the curl commands." It skipped the file entirely and tried to use the website in a browser instead. The critical commands were 20 lines away in a reference file. It never read them.
| Content type | Where it goes | Example |
|-------------|--------------|---------|
| Workflow steps (what to do) | SKILL.md body | "Resize the image, then convert to WebP" |
| Implementation patterns with gotchas | `references/` | RGBA-to-JPG compositing, API response parsing |
| Workflow steps, commands, scripts | **SKILL.md body (inline)** | curl commands, Python scripts, mapping tables |
| Executable helper scripts | `scripts/` | Agent runs them without reading (fine) |
| Variant/optional docs | `references/` | Platform-specific variants (AWS vs GCP) |
| Templates copied into user projects | `assets/` | React boilerplate, config files |
**Rule of thumb**: If Claude could generate a script from a 20-line description faster than it can find, read, and run a pre-built one, the description wins.
**Why not reference files for critical content?** When a skill loads, SKILL.md goes directly into context. The agent sees it immediately. Reference files require a deliberate choice to read another file — an extra decision point that LLMs deprioritise in favour of acting. The instruction to "go read file X" competes with the instruction to "do the task" and loses.
**No file size anxiety.** The old 500-line limit was a context economics rule from the 200K era. A 500-line skill is ~2500 tokens — 0.25% of 1M context, 1.25% of 200K. Even on smaller contexts, a working skill that's 800 lines beats a broken skill that's 300 lines with critical content in references the agent never reads.
### Frontmatter Validation
@@ -192,10 +201,11 @@ After installing, restart Claude Code to load new plugins.
Before committing a skill:
- [ ] SKILL.md has valid YAML frontmatter (name: kebab-case max 64 chars, description: max 1024 chars)
- [ ] Under 500 lines
- [ ] Everything on the critical path is inline in SKILL.md (no "see references/" for must-do steps)
- [ ] Produces tangible output (not just reference material)
- [ ] Tested by actually using it on a real task
- [ ] No pre-built scripts where a pattern description would suffice
- [ ] Rich enough that the agent doesn't need to improvise — include exact commands, scripts, mapping tables
- [ ] Not brutally summarised — detail is better than brevity when the detail prevents mistakes
## Skill Errata (ERRATA.md)
@@ -0,0 +1,324 @@
---
name: design-loop
description: "Autonomous multi-page site builder using a baton-passing loop pattern. Each iteration reads a task from .design/next-prompt.md, generates a page with Claude's HTML/CSS/Tailwind, integrates it into the site, verifies visually via browser automation, then writes the next task to keep the loop going. Drives complete website builds from a single starting prompt. Triggers: 'design loop', 'build the site', 'build all pages', 'autonomous site build', 'baton loop', 'next page', 'keep building pages'."
allowed-tools:
- Read
- Write
- Edit
- Glob
- Grep
- Bash
compatibility: claude-code-only
---
# Design Loop — Autonomous Site Builder
Build complete multi-page websites through an autonomous loop. Each iteration reads a task, generates a page, integrates it, verifies it visually, then writes the next task to keep going.
## Overview
The Design Loop uses a "baton" pattern — a file (`.design/next-prompt.md`) acts as a relay baton between iterations. Each cycle:
1. Reads the current task from the baton
2. Generates the page (via Claude or Google Stitch)
3. Integrates into the site structure (navigation, links)
4. Verifies visually via browser automation (if available)
5. Updates site documentation
6. Writes the NEXT task to the baton — keeping the loop alive
This is orchestration-agnostic. The loop can be driven by:
- **Human-in-loop**: User reviews each page, then says "next" or "keep going"
- **Fully autonomous**: Claude runs continuously until the site is complete
- **CI/CD**: Triggered on `.design/next-prompt.md` changes
## Generation Backends
The loop supports two generation backends:
| Backend | Setup | Quality | Speed | Best for |
|---------|-------|---------|-------|----------|
| **Claude** (default) | Zero dependencies | Great — production-ready HTML/Tailwind | Fast | Most projects, full code control |
| **Google Stitch** | `npm install @google/stitch-sdk` + API key | Higher fidelity AI designs | ~10-20s/screen | Design-heavy projects, visual polish |
### Detecting Stitch
At the start of each loop, check if Stitch is available:
1. Check if `@google/stitch-sdk` is installed: `ls node_modules/@google/stitch-sdk 2>/dev/null`
2. Check if `STITCH_API_KEY` is set in `.dev.vars` or environment
3. Check if `.design/metadata.json` exists (contains Stitch project ID)
If all three are present, use Stitch. Otherwise, fall back to Claude generation.
### Using Stitch SDK
See `references/stitch-sdk.md` for the full SDK reference. Quick usage:
```typescript
import { stitch } from "@google/stitch-sdk";
// Create or reference a project
const project = stitch.project(projectId);
// Generate a screen from the baton prompt
const screen = await project.generate(batonPrompt, "DESKTOP");
// Get the HTML and screenshot
const htmlUrl = await screen.getHtml();
const imageUrl = await screen.getImage();
// Download both
// HTML → .design/designs/{page}.html → then process into site/public/{page}.html
// Screenshot → .design/screenshots/{page}.png
```
When using Stitch, the generated HTML may need post-processing:
- Extract and reuse your project's header/nav/footer (Stitch generates standalone pages)
- Ensure Tailwind config matches your DESIGN.md
- Wire internal navigation links
### Stitch Project Persistence
Save Stitch identifiers to `.design/metadata.json` so future iterations can reference them:
```json
{
"projectId": "4044680601076201931",
"screens": {
"index": { "screenId": "d7237c7d78f44befa4f60afb17c818c1" },
"about": { "screenId": "bf6a3fe5c75348e58cf21fc7a9ddeafb" }
}
}
```
Use `screen.edit(prompt)` for iterative refinements on existing screens rather than regenerating from scratch.
## Getting Started
### First Run: Bootstrap the Project
If `.design/` doesn't exist yet, create the project scaffolding:
1. **Ask the user** for:
- Site name and purpose
- Target audience
- Desired aesthetic (minimal, bold, warm, etc.)
- List of pages they want
- Brand colours (or extract from existing site with `/design-system`)
2. **Create the project files**:
```
project/
├── .design/
│ ├── SITE.md # Vision, sitemap, roadmap — the project's long-term memory
│ ├── DESIGN.md # Visual design system — the source of truth for consistency
│ └── next-prompt.md # The baton — current task with page frontmatter
└── site/
└── public/ # Production pages live here
```
3. **Write SITE.md** from the template in `references/site-template.md`
4. **Write DESIGN.md** — either manually from user input, or use the `design-system` skill to extract from an existing site
5. **Write the first baton** (`.design/next-prompt.md`) for the homepage
### Subsequent Runs: Read the Baton
If `.design/next-prompt.md` already exists, parse it and continue the loop.
## The Baton File
`.design/next-prompt.md` has YAML frontmatter + a prompt body:
```markdown
---
page: about
layout: standard
---
An about page for Acme Plumbing describing the company's 20-year history in Newcastle.
**DESIGN SYSTEM:**
[Copied from .design/DESIGN.md Section 6]
**Page Structure:**
1. Header with navigation (consistent with index.html)
2. Hero with company photo and tagline
3. Story timeline showing company milestones
4. Team section with photo grid
5. CTA section: "Get a Free Quote"
6. Footer (consistent with index.html)
```
| Field | Required | Purpose |
|-------|----------|---------|
| `page` | Yes | Output filename (without .html) |
| `layout` | No | `standard`, `wide`, `sidebar` — defaults to `standard` |
## Execution Protocol
### Step 1: Read the Baton
```
Read .design/next-prompt.md
Extract: page name, layout, prompt body
```
### Step 2: Consult Context Files
Before generating, read:
| File | What to check |
|------|---------------|
| `.design/SITE.md` | Section 4 (Sitemap) — don't recreate existing pages |
| `.design/DESIGN.md` | Colour palette, typography, component styles |
| Existing pages in `site/public/` | Header/footer/nav patterns to match |
**Critical**: Read the most recent page's HTML to extract the exact header, navigation, and footer markup. New pages must use identical shared elements.
### Step 3: Generate the Page
#### Option A: Claude Generation (Default)
Generate a complete HTML file using Tailwind CSS (via CDN). The page must:
- **Match the design system** from `.design/DESIGN.md` exactly
- **Reuse the same header/nav/footer** from existing pages (copy verbatim)
- **Be self-contained** — single HTML file with Tailwind CDN, no build step
- **Be responsive** — mobile-first, works at all breakpoints
- **Include dark mode** if the design system specifies it
- **Use semantic HTML** — proper heading hierarchy, landmarks, alt text
- **Wire real navigation** — all nav links point to actual pages (existing or planned)
Write the generated file to `site/public/{page}.html`.
#### Option B: Stitch Generation (If Available)
If Stitch SDK is available:
1. Build the prompt by combining the baton body with the DESIGN.md system block
2. Call `project.generate(prompt, deviceType)` to generate the screen
3. Download the HTML from `screen.getHtml()` → save to `.design/designs/{page}.html`
4. Download the screenshot from `screen.getImage()` → save to `.design/screenshots/{page}.png`
5. Post-process the Stitch HTML:
- Replace the header/nav/footer with your project's shared elements
- Ensure consistent Tailwind config
- Wire internal navigation links
6. Save the processed file to `site/public/{page}.html`
7. Update `.design/metadata.json` with the new screen ID
For iterative edits on an existing Stitch screen, use `screen.edit(prompt)` instead of regenerating.
### Step 4: Integrate into the Site
After generating the new page:
1. **Update navigation across ALL existing pages** — add the new page to nav menus
2. **Fix placeholder links** — replace any `href="#"` with real page URLs
3. **Verify cross-page consistency** — header, footer, nav must be identical everywhere
4. **Check internal links** — no broken links between pages
### Step 5: Visual Verification (If Browser Available)
If Playwright CLI or Chrome MCP is available:
1. Start a local server: `npx serve site/public -p 3456`
2. Screenshot the new page at desktop (1280px) and mobile (375px) widths
3. Save screenshots to `.design/screenshots/{page}-desktop.png` and `{page}-mobile.png`
4. Compare visually against the design system
5. Fix any issues (broken layout, wrong colours, inconsistent nav)
6. Stop the server
If no browser automation is available, skip to Step 6.
### Step 6: Update Site Documentation
Edit `.design/SITE.md`:
- Mark the page as complete in Section 4 (Sitemap): `[x] {page}.html — {description}`
- Remove any consumed item from Section 5 (Roadmap) or Section 6 (Ideas)
- Add any new ideas discovered during generation
### Step 7: Write the Next Baton (CRITICAL)
**You MUST update `.design/next-prompt.md` before completing.** This keeps the loop alive.
1. **Choose the next page**:
- First: Check Section 5 (Roadmap) for pending high-priority items
- Second: Check Section 5 for medium-priority items
- Third: Pick from Section 6 (Ideas)
- Last resort: Invent something that fits the site vision
2. **Write the baton** with:
- YAML frontmatter (`page`, optional `layout`)
- Description of the page purpose and content
- Design system block copied from `.design/DESIGN.md` Section 6
- Detailed page structure (numbered sections)
3. **If the site is complete** (all roadmap items done, no more ideas):
- Write a baton with `page: _complete` and a summary of what was built
- This signals the loop is finished
## Loop Completion
The loop ends when:
- All pages in the roadmap are built (`[x]` in SITE.md Section 4)
- The user says to stop
- The baton contains `page: _complete`
On completion, output a summary:
- Pages built (with links)
- Screenshots (if captured)
- Any remaining ideas for future work
## Cross-Page Consistency Rules
The #1 risk in multi-page generation is **drift** — pages looking slightly different. Prevent this:
| Element | Rule |
|---------|------|
| **Header/Nav** | Copy exact HTML from the most recent page. Never regenerate. |
| **Footer** | Same — copy verbatim, only change active page indicator |
| **Tailwind config** | If using `<script>` config block, it must be identical across pages |
| **Colour values** | Always use the exact hex codes from DESIGN.md, never approximate |
| **Font imports** | Same Google Fonts `<link>` tag across all pages |
| **Spacing scale** | Consistent padding/margin values (document in DESIGN.md) |
## File Structure
```
project/
├── .design/
│ ├── SITE.md # Project vision, sitemap, roadmap
│ ├── DESIGN.md # Visual design system (source of truth)
│ ├── next-prompt.md # The baton — current/next task
│ └── screenshots/ # Visual verification captures
│ ├── index-desktop.png
│ ├── index-mobile.png
│ ├── about-desktop.png
│ └── about-mobile.png
├── site/
│ └── public/ # Production pages
│ ├── index.html
│ ├── about.html
│ ├── services.html
│ └── contact.html
└── .gitignore # Add .design/screenshots/
```
## Tips
- **Start with the homepage** — it establishes the visual language for everything else
- **Read existing pages before generating** — consistency comes from copying, not recreating
- **One page per iteration** — don't try to generate multiple pages at once
- **Include the design system in every baton** — Claude needs it fresh each time
- **Use the roadmap** — don't generate pages randomly; follow the user's priority order
- **Wire navigation early** — even link to pages that don't exist yet (they will soon)
## Common Pitfalls
- ❌ Forgetting to update `.design/next-prompt.md` (breaks the loop)
- ❌ Recreating a page that already exists in the sitemap
- ❌ Regenerating the header/nav instead of copying from existing pages
- ❌ Not including the design system block in the baton prompt
- ❌ Leaving `href="#"` placeholder links instead of real page URLs
- ❌ Inconsistent Tailwind config across pages
@@ -0,0 +1,68 @@
# Design Mappings & Descriptors
Use these mappings to transform vague user requests into precise design instructions.
## UI/UX Keyword Refinement
| Vague Term | Professional Terminology |
|:---|:---|
| "menu at the top" | "sticky navigation bar with logo and menu items" |
| "big photo" | "full-width hero section with focal-point imagery" |
| "list of things" | "responsive card grid with hover states and subtle elevation" |
| "button" | "primary call-to-action button with hover transition" |
| "form" | "form with labelled input fields, validation states, and submit button" |
| "picture area" | "hero section with background image or video" |
| "sidebar" | "collapsible side navigation with icon-label pairings" |
| "popup" | "modal dialog with overlay and smooth entry animation" |
| "footer stuff" | "footer with sitemap links, contact info, and legal notices" |
| "cards" | "content cards with consistent padding, rounded corners, and shadow" |
| "tabs" | "tabbed interface with active indicator and smooth content transition" |
| "search" | "search input with icon, placeholder text, and results dropdown" |
| "pricing" | "pricing comparison cards with highlighted recommended tier" |
| "testimonials" | "testimonial carousel or grid with avatar, quote, and attribution" |
## Atmosphere & Vibe Descriptors
| Basic Vibe | Enhanced Description |
|:---|:---|
| "Modern" | "Clean, minimal, generous whitespace, high-contrast typography" |
| "Professional" | "Sophisticated, trustworthy, subtle shadows, restricted premium palette" |
| "Fun / Playful" | "Vibrant, rounded corners, bold accent colours, bouncy animations" |
| "Dark Mode" | "High-contrast accents on deep slate or near-black backgrounds" |
| "Luxury" | "Elegant, spacious, fine lines, serif headers, high-fidelity photography" |
| "Tech / Cyber" | "Futuristic, neon accents, glassmorphism, monospaced typography" |
| "Warm / Friendly" | "Soft colours, rounded shapes, handwritten accents, inviting imagery" |
| "Bold / Industrial" | "Strong typography, high contrast, geometric shapes, dark backgrounds" |
| "Organic / Natural" | "Earth tones, soft textures, organic shapes, nature photography" |
| "Editorial" | "Magazine-like layouts, strong typographic hierarchy, generous leading" |
## Geometry & Shape Language
| Description | Tailwind Class | Visual Effect |
|:---|:---|:---|
| Pill-shaped | `rounded-full` | Buttons, tags, badges |
| Softly rounded | `rounded-xl` (12px) | Cards, containers, modals |
| Gently rounded | `rounded-lg` (8px) | Inputs, smaller elements |
| Sharp / precise | `rounded-none` or `rounded-sm` | Technical, brutalist aesthetic |
| Glassmorphism | `backdrop-blur-md bg-white/10 border border-white/20` | Overlays, nav bars |
| Frosted | `backdrop-blur-sm bg-white/80` | Subtle glass effect |
## Depth & Elevation
| Level | Description | Tailwind |
|:---|:---|:---|
| Flat | No shadows, focus on colour blocking and borders | `shadow-none` |
| Whisper-soft | Diffused, barely visible lift | `shadow-sm` |
| Subtle | Gentle shadow for card elevation | `shadow-md` |
| Floating | High-offset, soft shadow — element appears lifted | `shadow-lg` or `shadow-xl` |
| Dramatic | Strong shadow for hero elements or modals | `shadow-2xl` |
| Inset | Inner shadow for pressed or nested elements | `shadow-inner` |
## Section Spacing Scale
| Density | Description | Tailwind |
|:---|:---|:---|
| Tight | Compact, information-dense | `py-8 md:py-12` |
| Balanced | Standard section spacing | `py-12 md:py-16` |
| Generous | Breathing room, premium feel | `py-16 md:py-24` |
| Dramatic | Statement spacing, luxury/editorial | `py-24 md:py-32` |
@@ -0,0 +1,182 @@
# Site Template
Use these templates when bootstrapping a new design loop project.
## SITE.md Template
```markdown
# Project Vision
> **AGENT INSTRUCTION:** Read this file before every iteration. It is the project's long-term memory.
## 1. Core Identity
| Field | Value |
|-------|-------|
| **Project Name** | [Name] |
| **Mission** | [What the site achieves] |
| **Target Audience** | [Who uses this site] |
| **Voice & Tone** | [Personality descriptors — warm, professional, playful, etc.] |
| **Region** | [Australia / US / UK — affects spelling, phone format, imagery] |
## 2. Visual Language
Reference these when writing baton prompts.
- **Primary Vibe**: [Main aesthetic — e.g. "Clean and modern"]
- **Secondary Vibe**: [Supporting aesthetic — e.g. "Warm and approachable"]
- **Anti-Vibes**: [What to avoid — e.g. "Not corporate, not cluttered"]
## 3. Technical Setup
- **Output Directory**: `site/public/`
- **CSS**: Tailwind CSS via CDN (no build step)
- **Dark Mode**: [Yes/No] — if yes, via class toggle
- **Fonts**: [Google Fonts import URL]
## 4. Live Sitemap
Update this when a page is successfully generated.
- [x] `index.html` — Homepage with hero, features, CTA
- [ ] `about.html` — Company story and team
- [ ] `services.html` — Service offerings with pricing
- [ ] `contact.html` — Contact form and location map
## 5. Roadmap (Backlog)
Pick the next task from here. Remove items as they're completed.
### High Priority
- [ ] Build about page with team section
- [ ] Build services page with pricing cards
### Medium Priority
- [ ] Build contact page with form
- [ ] Build FAQ page
### Low Priority
- [ ] Blog index page
- [ ] Individual blog post template
## 6. Creative Freedom
When the roadmap is empty, follow these guidelines to add pages:
1. **Stay on-brand** — new pages must fit the established vibe
2. **Enhance the core** — support the site mission
3. **Naming convention** — lowercase, descriptive filenames (e.g. `team.html`)
### Ideas to Explore
- [ ] `testimonials.html` — Customer reviews and case studies
- [ ] `gallery.html` — Project portfolio with image grid
- [ ] `faq.html` — Frequently asked questions with accordion
## 7. Rules of Engagement
1. Do NOT recreate pages already marked `[x]` in Section 4
2. ALWAYS update `.design/next-prompt.md` before completing an iteration
3. Remove consumed ideas from Section 6
4. Copy header/nav/footer from existing pages — never regenerate
5. All internal links must point to real pages
```
## DESIGN.md Template
Generate this using the `design-system` skill, or create manually:
```markdown
# Design System: [Project Name]
## 1. Visual Theme & Atmosphere
[Describe the mood, density, and aesthetic philosophy. Use evocative language.]
Example: "Airy and modern with generous whitespace. Warm undertones soften the
minimal layout. Typography does the heavy lifting — large, confident headings
with understated body text."
## 2. Colour Palette & Roles
| Role | Name | Value | Usage |
|------|------|-------|-------|
| Primary | [Descriptive Name] | `#hexcode` | Buttons, links, active states |
| Primary Foreground | [Name] | `#hexcode` | Text on primary backgrounds |
| Secondary | [Name] | `#hexcode` | Supporting elements, badges |
| Background | [Name] | `#hexcode` | Page background |
| Surface | [Name] | `#hexcode` | Cards, containers |
| Text Primary | [Name] | `#hexcode` | Headings, body text |
| Text Secondary | [Name] | `#hexcode` | Captions, metadata |
| Border | [Name] | `#hexcode` | Dividers, input borders |
| Accent | [Name] | `#hexcode` | Highlights, notifications |
### Dark Mode (if applicable)
| Role | Light Value | Dark Value |
|------|-------------|------------|
| Background | `#hexcode` | `#hexcode` |
| Surface | `#hexcode` | `#hexcode` |
| Text Primary | `#hexcode` | `#hexcode` |
## 3. Typography
| Element | Font | Weight | Size | Line Height |
|---------|------|--------|------|-------------|
| H1 | [Font Family] | 700 | 3rem / 48px | 1.1 |
| H2 | [Font Family] | 600 | 2rem / 32px | 1.2 |
| H3 | [Font Family] | 600 | 1.5rem / 24px | 1.3 |
| Body | [Font Family] | 400 | 1rem / 16px | 1.6 |
| Small | [Font Family] | 400 | 0.875rem / 14px | 1.5 |
Google Fonts import:
```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
```
## 4. Component Styles
### Buttons
- **Primary**: [Background colour], [text colour], [border-radius], [padding]
- **Secondary**: [Outline style], [hover behaviour]
- **Hover**: [Transition description — e.g. "darken 10%, subtle lift shadow"]
### Cards
- **Background**: [Surface colour]
- **Border**: [1px border-colour or none]
- **Border Radius**: [e.g. 12px / rounded-xl]
- **Shadow**: [e.g. "whisper-soft diffused shadow" or "none"]
- **Padding**: [e.g. 1.5rem]
### Navigation
- **Style**: [Sticky/static], [background treatment]
- **Active indicator**: [Underline, background, colour change]
- **Mobile**: [Hamburger menu, slide-out drawer, bottom nav]
### Forms
- **Input style**: [Border, background, border-radius, focus ring]
- **Labels**: [Position, weight, colour]
- **Validation**: [Error colour, success colour, message placement]
## 5. Layout Principles
- **Max content width**: [e.g. 1200px / max-w-7xl]
- **Section padding**: [e.g. py-16 md:py-24]
- **Grid**: [e.g. 12-column, gap-8]
- **Whitespace philosophy**: [Generous / compact / balanced]
## 6. Design System Notes for Generation
**Copy this entire block into every baton prompt:**
**DESIGN SYSTEM (REQUIRED):**
- Platform: Web, Desktop-first, responsive
- Theme: [Light/Dark], [descriptors]
- Background: [Description] (#hex)
- Surface: [Description] (#hex)
- Primary: [Description] (#hex) for [role]
- Text: [Description] (#hex)
- Font: [Font name] via Google Fonts
- Corners: [Description — e.g. "Softly rounded, 12px"]
- Shadows: [Description — e.g. "Whisper-soft diffused shadows"]
- Spacing: [Description — e.g. "Generous whitespace, py-16 sections"]
```
@@ -0,0 +1,139 @@
# Google Stitch SDK Reference
`@google/stitch-sdk` — generate UI screens from text prompts and extract HTML + screenshots.
## Install
```bash
npm install @google/stitch-sdk
```
Set `STITCH_API_KEY` in your environment or `.dev.vars`.
## Quick Start
```typescript
import { stitch } from "@google/stitch-sdk";
// Create a project
const result = await stitch.callTool("create_project", { title: "My Site" });
// Reference an existing project
const project = stitch.project("4044680601076201931");
// Generate a screen
const screen = await project.generate("A modern landing page with hero section", "DESKTOP");
// Get assets
const htmlUrl = await screen.getHtml(); // Download URL for HTML
const imageUrl = await screen.getImage(); // Download URL for screenshot
// Edit an existing screen
const edited = await screen.edit("Make the background dark and enlarge the CTA button");
// Generate variants
const variants = await screen.variants("Try different colour schemes", {
variantCount: 3,
creativeRange: "EXPLORE", // "REFINE" | "EXPLORE" | "REIMAGINE"
aspects: ["COLOR_SCHEME"], // "LAYOUT" | "COLOR_SCHEME" | "IMAGES" | "TEXT_FONT" | "TEXT_CONTENT"
});
```
## Device Types
`"MOBILE"` | `"DESKTOP"` | `"TABLET"` | `"AGNOSTIC"`
## Model Selection
```typescript
// Default model
const screen = await project.generate(prompt, "DESKTOP");
// Specific model
const screen = await project.generate(prompt, "DESKTOP", "GEMINI_3_PRO");
// Options: "GEMINI_3_PRO" | "GEMINI_3_FLASH"
```
## Project Management
```typescript
// List all projects
const projects = await stitch.projects();
// Get screens in a project
const screens = await project.screens();
// Get a specific screen
const screen = await project.getScreen("screenId");
```
## Tool Client (Low-Level)
For direct MCP tool access:
```typescript
const tools = await stitch.listTools();
const result = await stitch.callTool("generate_screen_from_text", {
projectId: "123",
prompt: "A dashboard with charts",
deviceType: "DESKTOP",
});
```
## Downloading Assets
The `getHtml()` and `getImage()` methods return download URLs. Fetch with curl or node:
```bash
# Download HTML
curl -L -o .design/designs/index.html "$(htmlUrl)"
# Download screenshot (append =w{width} for full resolution)
curl -L -o .design/screenshots/index.png "${imageUrl}=w1280"
```
## Authentication
| Variable | Required | Description |
|----------|----------|-------------|
| `STITCH_API_KEY` | Yes (or OAuth) | API key |
| `STITCH_ACCESS_TOKEN` | No | OAuth access token (alternative) |
| `GOOGLE_CLOUD_PROJECT` | With OAuth | GCP project ID |
## Error Handling
```typescript
import { StitchError } from "@google/stitch-sdk";
try {
const screen = await project.generate(prompt);
} catch (error) {
if (error instanceof StitchError) {
console.error(error.code); // AUTH_FAILED, NOT_FOUND, RATE_LIMITED, etc.
console.error(error.message);
}
}
```
## Vercel AI SDK Integration
```typescript
import { generateText, stepCountIs } from "ai";
import { google } from "@ai-sdk/google";
import { stitchTools } from "@google/stitch-sdk/ai";
const { text, steps } = await generateText({
model: google("gemini-2.5-flash"),
tools: stitchTools(),
prompt: "Create a project and generate a modern dashboard",
stopWhen: stepCountIs(5),
});
```
## Tips for Design Loop Integration
1. **Persist project ID** in `.design/metadata.json` — don't create a new project each iteration
2. **Use `screen.edit()`** for refinements rather than full regeneration
3. **Post-process Stitch HTML** — replace headers/footers with your shared elements
4. **Screenshot URLs need width suffix** — append `=w1280` for full resolution (Google CDN serves thumbnails by default)
5. **Include DESIGN.md context in prompts** — Stitch generates better results with explicit design system instructions
@@ -0,0 +1,229 @@
---
name: design-system
description: "Extract a complete design system from an existing website or screenshot into a DESIGN.md file. Analyses colours, typography, component styles, spacing, and atmosphere through browser automation and HTML inspection. Produces a semantic design system document optimised for consistent page generation. Triggers: 'extract design system', 'design system', 'create DESIGN.md', 'analyse the design', 'what design does this site use', 'extract styles from', 'reverse engineer the design'."
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
compatibility: claude-code-only
---
# Design System Extractor
Analyse an existing website, HTML file, or screenshot and synthesise a semantic design system into a `DESIGN.md` file. The output is optimised for use with the `design-loop` skill and general page generation.
## When to Use
- Starting a new project based on an existing site's visual language
- Documenting a site's design system that was never formally written down
- Preparing `.design/DESIGN.md` before running the design loop
- Extracting brand guidelines from a client's existing website
- Creating consistency documentation for a multi-page project
- Extracting design tokens from a Google Stitch project
## Workflow
### Step 1: Identify the Source
Ask the user for one of:
| Source | Method |
|--------|--------|
| **Live URL** | Browse via Playwright CLI or scraper, screenshot + extract HTML |
| **Local HTML file** | Read the file directly |
| **Screenshot image** | Analyse visually (limited — no exact hex extraction) |
| **Existing project** | Scan `site/public/` for HTML files to analyse |
| **Stitch project** | Use `@google/stitch-sdk` to fetch screen HTML + design theme |
### Step 2: Extract Raw Design Data
#### From a Live URL
1. **Browse the site** using Playwright CLI:
```
playwright-cli -s=design open {url}
playwright-cli -s=design screenshot --filename=.design/screenshots/source-desktop.png
```
2. **Extract the full HTML** — either via scraper MCP or by reading the page source
3. **Resize and screenshot mobile** (375px):
```
playwright-cli -s=design resize 375 812
playwright-cli -s=design screenshot --filename=.design/screenshots/source-mobile.png
```
4. Close the session: `playwright-cli -s=design close`
#### From a Local HTML File
Read the file directly and extract design tokens from the source.
#### From a Screenshot Only
Analyse the image visually. Note: colour extraction will be approximate without HTML source. Flag this limitation in the output.
#### From a Google Stitch Project
If `@google/stitch-sdk` is installed and `STITCH_API_KEY` is set:
```typescript
import { stitch } from "@google/stitch-sdk";
// List projects to find the target
const projects = await stitch.projects();
// Get project details (includes designTheme)
const project = stitch.project(projectId);
const screens = await project.screens();
// Get HTML from the main screen
const screen = screens[0]; // or find by title
const htmlUrl = await screen.getHtml();
const imageUrl = await screen.getImage();
```
The Stitch `designTheme` object provides structured tokens directly:
```json
{
"colorMode": "DARK",
"font": "INTER",
"roundness": "ROUND_EIGHT",
"customColor": "#40baf7",
"saturation": 3
}
```
Map these to DESIGN.md sections:
- `colorMode` → Theme (Light/Dark)
- `font` → Typography font family
- `roundness` → Component border-radius (`ROUND_EIGHT` = 8px, `ROUND_SIXTEEN` = 16px, etc.)
- `customColor` → Primary brand colour
- `saturation` → Colour vibrancy (1-5 scale)
Then also download and analyse the HTML for the full palette (Stitch's theme object only has the primary colour — the full palette is in the generated CSS).
### Step 3: Analyse Design Tokens
Extract these from the HTML/CSS source:
#### Colours
Look in these locations (priority order):
1. **CSS custom properties** — `:root { --primary: #hex; }` or `@theme` blocks
2. **Tailwind config** — `<script>` block with `tailwind.config` or `@theme` in `<style>`
3. **Inline styles** — `style="color: #hex"` or `style="background: #hex"`
4. **Tailwind classes** — `bg-blue-600`, `text-gray-900` (map to palette)
5. **Computed from screenshot** — last resort, approximate
For each colour found, determine its **role**:
| Role | How to identify |
|------|-----------------|
| Primary | Buttons, links, active states, brand elements |
| Background | `<body>` or `<html>` background |
| Surface | Cards, containers, elevated elements |
| Text Primary | `<h1>`, `<h2>`, main body text |
| Text Secondary | Captions, metadata, muted text |
| Border | Dividers, input borders, card borders |
| Accent | Badges, notifications, highlights |
#### Typography
Extract:
| Token | Where to find |
|-------|---------------|
| Font families | Google Fonts `<link>`, `@import`, `font-family` in CSS |
| Heading weights | `font-bold`, `font-semibold`, or explicit `font-weight` |
| Body size | Base `font-size` on `<body>` or root |
| Line height | `leading-*` classes or `line-height` CSS |
| Letter spacing | `tracking-*` classes or `letter-spacing` CSS |
#### Components
Identify patterns for:
- **Buttons** — shape (rounded-full, rounded-lg), colours, padding, hover states
- **Cards** — background, border, shadow, border-radius, padding
- **Navigation** — sticky/static, background treatment, active indicator
- **Forms** — input style, focus ring, label positioning
- **Hero sections** — layout pattern, overlay treatment, CTA placement
#### Spacing & Layout
- **Max content width** — look for `max-w-*` or explicit `max-width`
- **Section padding** — typical vertical padding between sections
- **Grid system** — column count, gap values
- **Whitespace philosophy** — tight, balanced, generous, or dramatic
### Step 4: Synthesise into Natural Language
**Critical**: The DESIGN.md should describe the design in **semantic, natural language** supported by exact values. This is not a CSS dump — it's a document a designer or AI can read to understand and reproduce the visual language.
| Don't write | Write instead |
|-------------|---------------|
| `rounded-xl` | "Softly rounded corners (12px)" |
| `shadow-md` | "Subtle elevation with diffused shadow" |
| `#1E40AF` | "Deep Ocean Blue (#1E40AF) for primary actions" |
| `py-16` | "Generous section spacing with breathing room" |
### Step 5: Write DESIGN.md
Output the file to `.design/DESIGN.md` (or user-specified path).
Follow the structure from the `design-loop` skill's `references/site-template.md` — specifically the DESIGN.md Template section. The key sections are:
1. **Visual Theme & Atmosphere** — mood, vibe, philosophy
2. **Colour Palette & Roles** — table with role, name, hex, usage
3. **Typography** — font families, weights, sizes, line heights
4. **Component Styles** — buttons, cards, nav, forms
5. **Layout Principles** — max width, spacing, grid, whitespace
6. **Design System Notes for Generation** — the copy-paste block for baton prompts
### Step 6: Verify Accuracy
If browser automation is available:
1. Generate a small test section (e.g. a card + button + heading) using the extracted design system
2. Screenshot it alongside the original
3. Compare visually — adjust any values that don't match
### Step 7: Report to User
Present:
- Summary of extracted tokens (colour count, fonts, component patterns)
- The generated DESIGN.md location
- Any tokens that were approximate (flagged with ⚠️)
- Suggestions for manual review (colours from screenshots, ambiguous typography)
## Handling Multiple Pages
If the site has multiple pages with different styles:
1. Analyse the **homepage first** — it usually has the most complete design language
2. Spot-check 2-3 inner pages for consistency
3. Note any **page-specific overrides** in the Component Styles section
4. If pages are wildly different, ask the user which page to use as the canonical source
## Tips
- **Tailwind sites are easiest** — the config block has everything
- **Google Fonts links are gold** — they specify exact families and weights
- **CSS custom properties are reliable** — they represent intentional design tokens
- **Inline Tailwind classes need interpretation** — `bg-slate-900` needs mapping to a role
- **Screenshots are last resort** — accurate hex extraction from images is unreliable
- **Dark mode**: Check for `.dark` class overrides or `prefers-color-scheme` media queries
## Common Pitfalls
- ❌ Listing raw CSS values without semantic description
- ❌ Missing the dark mode palette (check for `.dark` class or media query)
- ❌ Ignoring component patterns (just listing colours isn't enough)
- ❌ Not including Section 6 (the copy-paste generation block)
- ❌ Approximate colours from screenshots without flagging the uncertainty
@@ -0,0 +1,388 @@
---
name: walkthrough-video
description: "Generate professional walkthrough videos from app screenshots or live sites using Remotion. Smooth transitions, zoom effects, text overlays, and optional voiceover narration. Produces MP4 videos for demos, product showcases, or documentation. Triggers: 'walkthrough video', 'demo video', 'product video', 'create a video walkthrough', 'remotion video', 'screen recording', 'app demo', 'showcase video', 'generate video from screenshots'."
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
compatibility: claude-code-only
---
# Walkthrough Video Generator
Create professional walkthrough videos from app screenshots or live sites using Remotion. Produces smooth, polished MP4 videos with transitions, zoom effects, and text overlays.
## Overview
This skill takes a set of screenshots (or captures them from a running app) and orchestrates them into a Remotion video composition with:
- **Smooth transitions** between screens (fade, slide, wipe)
- **Zoom effects** to highlight specific UI areas
- **Text overlays** with titles, descriptions, and callouts
- **Progress indicators** showing position in the walkthrough
- **Optional voiceover** narration track
## Prerequisites
- **Node.js** 18+ installed
- **Screenshots** of the app (or a running app to screenshot)
- No Remotion experience needed — the skill generates all code
## Workflow
### Step 1: Gather Screenshots
Choose one approach:
#### Option A: From Existing Screenshots
If the user already has screenshots (e.g. from `design-loop` or `product-showcase`):
```
Read screenshots from:
- .design/screenshots/
- .jez/screenshots/
- User-specified directory
```
Sort them in walkthrough order (alphabetically by filename, or as user specifies).
#### Option B: Capture from Running App
If the app is running locally:
1. Start Playwright CLI session
2. Navigate through each screen in sequence
3. Screenshot at consistent dimensions (1280x720 recommended for video)
4. Save to `video/public/screens/`
```bash
playwright-cli -s=walkthrough open http://localhost:3000
playwright-cli -s=walkthrough resize 1280 720
playwright-cli -s=walkthrough screenshot --filename=video/public/screens/01-home.png
# Navigate to next page...
playwright-cli -s=walkthrough screenshot --filename=video/public/screens/02-dashboard.png
```
#### Option C: From Live URL
Same as Option B but with a public URL. Screenshot each key page.
### Step 2: Create Screen Manifest
Build a `screens.json` describing the walkthrough:
```json
{
"projectName": "My App Walkthrough",
"fps": 30,
"width": 1280,
"height": 720,
"screens": [
{
"id": "home",
"title": "Welcome to MyApp",
"description": "The landing page introduces the core value proposition",
"imagePath": "screens/01-home.png",
"durationSeconds": 4,
"transition": "fade",
"zoomTarget": null
},
{
"id": "dashboard",
"title": "Your Dashboard",
"description": "See all your projects at a glance",
"imagePath": "screens/02-dashboard.png",
"durationSeconds": 5,
"transition": "slide-left",
"zoomTarget": { "x": 100, "y": 200, "width": 400, "height": 300, "delay": 2 }
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique screen identifier |
| `title` | string | Text overlay title |
| `description` | string | Subtitle or narration text |
| `imagePath` | string | Path relative to `video/public/` |
| `durationSeconds` | number | How long to show this screen |
| `transition` | string | `fade`, `slide-left`, `slide-right`, `slide-up`, `wipe`, `none` |
| `zoomTarget` | object/null | If set, zoom into this region after `delay` seconds |
### Step 3: Scaffold Remotion Project
If no Remotion project exists:
```bash
mkdir -p video
cd video
npm init -y
npm install remotion @remotion/cli @remotion/transitions react react-dom
npm install -D typescript @types/react
```
Create the project structure:
```
video/
├── src/
│ ├── Root.tsx # Remotion entry point
│ ├── WalkthroughComposition.tsx # Main composition
│ ├── components/
│ │ ├── ScreenSlide.tsx # Individual screen display
│ │ ├── TextOverlay.tsx # Title/description overlay
│ │ ├── ProgressBar.tsx # Walkthrough progress indicator
│ │ └── ZoomEffect.tsx # Zoom into regions
│ └── config.ts # Load screens.json, calculate durations
├── public/
│ └── screens/ # Screenshot assets
│ ├── 01-home.png
│ └── 02-dashboard.png
├── screens.json # Screen manifest
├── remotion.config.ts
├── tsconfig.json
└── package.json
```
### Step 4: Generate Remotion Components
Generate each component file. Key patterns:
#### Root.tsx
```tsx
import { Composition } from "remotion";
import { WalkthroughComposition } from "./WalkthroughComposition";
import { screens, totalDurationInFrames, FPS, WIDTH, HEIGHT } from "./config";
export const RemotionRoot = () => (
<Composition
id="Walkthrough"
component={WalkthroughComposition}
durationInFrames={totalDurationInFrames}
fps={FPS}
width={WIDTH}
height={HEIGHT}
defaultProps={{ screens }}
/>
);
```
#### ScreenSlide.tsx Pattern
```tsx
import { AbsoluteFill, Img, spring, useCurrentFrame, useVideoConfig } from "remotion";
interface ScreenSlideProps {
imageSrc: string;
title: string;
description: string;
}
export const ScreenSlide: React.FC<ScreenSlideProps> = ({ imageSrc, title, description }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Fade in
const opacity = spring({ frame, fps, config: { damping: 20 } });
// Subtle zoom (Ken Burns effect)
const scale = 1 + frame * 0.0002;
return (
<AbsoluteFill style={{ backgroundColor: "#000" }}>
<Img
src={imageSrc}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
opacity,
transform: `scale(${scale})`,
}}
/>
{/* Text overlay at bottom */}
<div style={{
position: "absolute",
bottom: 40,
left: 40,
right: 40,
opacity: spring({ frame: frame - 15, fps, config: { damping: 20 } }),
}}>
<h2 style={{ color: "#fff", fontSize: 32, fontWeight: 700, textShadow: "0 2px 8px rgba(0,0,0,0.8)" }}>
{title}
</h2>
<p style={{ color: "#ccc", fontSize: 18, textShadow: "0 1px 4px rgba(0,0,0,0.8)" }}>
{description}
</p>
</div>
</AbsoluteFill>
);
};
```
#### Transitions Between Screens
Use `@remotion/transitions` for transitions:
```tsx
import { TransitionSeries } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
// In WalkthroughComposition:
<TransitionSeries>
{screens.map((screen, i) => (
<TransitionSeries.Sequence
key={screen.id}
durationInFrames={screen.durationSeconds * FPS}
>
<ScreenSlide {...screen} />
</TransitionSeries.Sequence>
// Add transition between screens (not after last)
{i < screens.length - 1 && (
<TransitionSeries.Transition
presentation={getTransition(screens[i + 1].transition)}
timing={springTiming({ config: { damping: 20 }, durationInFrames: 15 })}
/>
)}
))}
</TransitionSeries>
```
### Step 5: Preview and Refine
```bash
cd video
npx remotion studio
```
This opens a browser-based preview. Check:
- Timing feels right for each screen
- Transitions are smooth
- Text overlays are readable
- Zoom targets hit the right area
- Progress bar (if included) is accurate
### Step 6: Render the Video
```bash
cd video
npx remotion render Walkthrough output.mp4 --codec h264
```
For higher quality:
```bash
npx remotion render Walkthrough output.mp4 --codec h264 --quality 90
```
For web-optimised (smaller file):
```bash
npx remotion render Walkthrough output.webm --codec vp8
```
## Advanced Features
### Zoom to Region
Zoom into a specific area of the screen to highlight a feature:
```tsx
// In ZoomEffect.tsx — interpolate scale and translate
const zoomScale = interpolate(frame, [delayFrames, delayFrames + 30], [1, 2.5], {
extrapolateRight: "clamp",
});
const translateX = interpolate(frame, [delayFrames, delayFrames + 30], [0, -targetX], {
extrapolateRight: "clamp",
});
```
### Animated Callout Circles
Draw attention to UI elements:
```tsx
// Pulsing circle that appears at a specific point
const scale = spring({ frame: frame - delay, fps, config: { damping: 8, stiffness: 80 } });
<div style={{
position: "absolute",
left: x - 20, top: y - 20,
width: 40, height: 40,
borderRadius: "50%",
border: "3px solid #3B82F6",
transform: `scale(${scale})`,
opacity: Math.min(1, scale),
}} />
```
### Background Music
Add a subtle background track:
```tsx
import { Audio } from "remotion";
<Audio src={staticFile("music/background.mp3")} volume={0.15} />
```
### Intro and Outro Slides
Add title card at start and CTA at end:
```tsx
// First sequence: Title card (3 seconds)
<TransitionSeries.Sequence durationInFrames={90}>
<TitleCard projectName="MyApp" tagline="The future of project management" />
</TransitionSeries.Sequence>
// ... screen sequences ...
// Last sequence: CTA card (4 seconds)
<TransitionSeries.Sequence durationInFrames={120}>
<CtaCard url="myapp.com" text="Try it free" />
</TransitionSeries.Sequence>
```
## Transition Reference
| Name | Effect | Best for |
|------|--------|----------|
| `fade` | Cross-fade dissolve | Default, works everywhere |
| `slide-left` | New screen slides in from right | Sequential flow (next page) |
| `slide-right` | New screen slides in from left | Going back |
| `slide-up` | New screen slides in from bottom | Drill-down into detail |
| `wipe` | Wipe transition | Dramatic reveal |
| `none` | Hard cut | Quick comparison |
## Output Options
| Format | Command | Use case |
|--------|---------|----------|
| MP4 (H.264) | `--codec h264` | Universal compatibility |
| WebM (VP8) | `--codec vp8` | Web embedding, smaller files |
| GIF | `--image-format png` then `ffmpeg` | Short loops, social media |
| PNG sequence | `--image-format png --sequence` | Post-production editing |
## Tips
- **1280x720 is ideal** for web walkthrough videos (good quality, reasonable file size)
- **3-5 seconds per screen** feels natural — longer for complex screens
- **Fade is the safest transition** — use others sparingly for emphasis
- **Text overlays need contrast** — use text-shadow or semi-transparent background
- **Ken Burns effect** (subtle zoom) prevents static screenshots from feeling dead
- **Preview before rendering** — `npx remotion studio` saves time vs full renders
- **Keep it under 90 seconds** — attention drops sharply after that
## Common Pitfalls
- ❌ Using screenshots at different dimensions (causes scaling issues)
- ❌ Too many transition types (pick 1-2 and stay consistent)
- ❌ Text overlays that are too small or lack contrast
- ❌ No intro/outro — video feels abrupt
- ❌ Rendering before previewing (wastes time on fixable issues)
- ❌ Forgetting `staticFile()` for assets in `public/` directory
@@ -0,0 +1,286 @@
# Remotion Patterns Reference
## Project Config Files
### remotion.config.ts
```ts
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);
```
### tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}
```
### package.json scripts
```json
{
"scripts": {
"studio": "remotion studio",
"render": "remotion render Walkthrough output.mp4 --codec h264",
"render:web": "remotion render Walkthrough output.webm --codec vp8",
"render:gif": "remotion render Walkthrough frames/ --image-format png --sequence"
}
}
```
## config.ts — Load Manifest and Calculate Durations
```tsx
import screensData from "../screens.json";
export interface ScreenConfig {
id: string;
title: string;
description: string;
imagePath: string;
durationSeconds: number;
transition: "fade" | "slide-left" | "slide-right" | "slide-up" | "wipe" | "none";
zoomTarget: { x: number; y: number; width: number; height: number; delay: number } | null;
}
export const FPS = screensData.fps || 30;
export const WIDTH = screensData.width || 1280;
export const HEIGHT = screensData.height || 720;
export const screens: ScreenConfig[] = screensData.screens;
const TRANSITION_FRAMES = 15; // frames per transition
export const totalDurationInFrames = screens.reduce(
(total, s) => total + s.durationSeconds * FPS,
0
) + (screens.length > 1 ? (screens.length - 1) * TRANSITION_FRAMES : 0)
+ 90 // intro
+ 120; // outro
```
## Transition Helper
```tsx
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
type TransitionType = "fade" | "slide-left" | "slide-right" | "slide-up" | "wipe" | "none";
export function getTransition(type: TransitionType) {
switch (type) {
case "slide-left":
return slide({ direction: "from-right" });
case "slide-right":
return slide({ direction: "from-left" });
case "slide-up":
return slide({ direction: "from-bottom" });
case "wipe":
return wipe({ direction: "from-left" });
case "fade":
default:
return fade();
}
}
```
## Title Card Component
```tsx
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
interface TitleCardProps {
projectName: string;
tagline?: string;
backgroundColor?: string;
}
export const TitleCard: React.FC<TitleCardProps> = ({
projectName,
tagline,
backgroundColor = "#0f172a",
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const titleOpacity = spring({ frame, fps, config: { damping: 20 } });
const taglineOpacity = spring({ frame: frame - 20, fps, config: { damping: 20 } });
return (
<AbsoluteFill
style={{
backgroundColor,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<h1
style={{
color: "#fff",
fontSize: 56,
fontWeight: 800,
fontFamily: "Inter, system-ui, sans-serif",
opacity: titleOpacity,
transform: `translateY(${(1 - titleOpacity) * 20}px)`,
}}
>
{projectName}
</h1>
{tagline && (
<p
style={{
color: "#94a3b8",
fontSize: 24,
fontWeight: 400,
marginTop: 16,
opacity: taglineOpacity,
}}
>
{tagline}
</p>
)}
</AbsoluteFill>
);
};
```
## CTA Card Component
```tsx
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
interface CtaCardProps {
text: string;
url: string;
backgroundColor?: string;
}
export const CtaCard: React.FC<CtaCardProps> = ({
text,
url,
backgroundColor = "#0f172a",
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = spring({ frame, fps, config: { damping: 20 } });
return (
<AbsoluteFill
style={{
backgroundColor,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 24,
}}
>
<p
style={{
color: "#fff",
fontSize: 40,
fontWeight: 700,
opacity,
}}
>
{text}
</p>
<p
style={{
color: "#3b82f6",
fontSize: 28,
fontWeight: 500,
opacity: spring({ frame: frame - 15, fps }),
}}
>
{url}
</p>
</AbsoluteFill>
);
};
```
## Progress Bar Component
```tsx
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
interface ProgressBarProps {
totalScreens: number;
currentScreen: number;
color?: string;
}
export const ProgressBar: React.FC<ProgressBarProps> = ({
totalScreens,
currentScreen,
color = "#3b82f6",
}) => {
const progress = (currentScreen + 1) / totalScreens;
return (
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 4,
backgroundColor: "rgba(255,255,255,0.1)",
}}
>
<div
style={{
height: "100%",
width: `${progress * 100}%`,
backgroundColor: color,
transition: "width 0.3s ease",
}}
/>
</div>
);
};
```
## GIF Conversion (Post-Render)
For short walkthrough GIFs (social media, README):
```bash
# Render as PNG sequence first
npx remotion render Walkthrough frames/ --image-format png --sequence
# Convert to GIF with ffmpeg (good quality, reasonable size)
ffmpeg -framerate 30 -i frames/frame%d.png -vf "fps=15,scale=640:-1:flags=lanczos" -loop 0 walkthrough.gif
# Or use gifski for better quality (install: brew install gifski)
gifski --fps 15 --width 640 -o walkthrough.gif frames/frame*.png
```
## Video Dimensions by Platform
| Platform | Dimensions | Aspect | Notes |
|----------|-----------|--------|-------|
| YouTube / general | 1920x1080 | 16:9 | Standard HD |
| Web embed | 1280x720 | 16:9 | Good balance of quality/size |
| Twitter/X | 1280x720 | 16:9 | Max 2:20 length |
| LinkedIn | 1920x1080 | 16:9 | Max 10 min |
| Instagram feed | 1080x1080 | 1:1 | Square format |
| Instagram stories | 1080x1920 | 9:16 | Vertical |
| Mobile demo | 390x844 | ~9:19.5 | iPhone viewport |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "contextbricks",
"version": "4.0.0",
"version": "4.1.0",
"description": "Git-aware statusline for Claude Code CLI with context brick visualization",
"keywords": [
"claude",
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Context awareness hook for Claude Code
# Reads context level from statusline's persisted file and injects a note
# when crossing 100k thresholds.
#
# Install as a UserPromptSubmit hook in settings.json:
# "hooks": { "UserPromptSubmit": [{ "matcher": "", "hooks": [
# { "type": "command", "command": "bash ~/.claude/context-hook.sh" }
# ]}]}
CONTEXT_FILE="$HOME/.claude/context-level.json"
LAST_THRESHOLD_FILE="$HOME/.claude/context-last-threshold"
# Read stdin (required by hook protocol)
cat > /dev/null
# Check if context file exists and is recent (< 60 seconds old)
if [[ ! -f "$CONTEXT_FILE" ]]; then
exit 0
fi
# Parse the JSON
FREE_100K=$(python3 -c "import json; print(json.load(open('$CONTEXT_FILE'))['free100k'])" 2>/dev/null)
USAGE_PCT=$(python3 -c "import json; print(json.load(open('$CONTEXT_FILE'))['usagePct'])" 2>/dev/null)
TOTAL_K=$(python3 -c "import json; print(json.load(open('$CONTEXT_FILE'))['totalK'])" 2>/dev/null)
if [[ -z "$FREE_100K" ]]; then
exit 0
fi
# Read last reported threshold
LAST_THRESHOLD=""
if [[ -f "$LAST_THRESHOLD_FILE" ]]; then
LAST_THRESHOLD=$(cat "$LAST_THRESHOLD_FILE")
fi
# Only inject when crossing a new 100k boundary (or first time)
if [[ "$FREE_100K" == "$LAST_THRESHOLD" ]]; then
exit 0
fi
# Save current threshold
echo "$FREE_100K" > "$LAST_THRESHOLD_FILE"
# Build the message based on how much is left
if [[ "$FREE_100K" -ge 700 ]]; then
echo "<context-status>Context: ~${FREE_100K}k tokens remaining (${USAGE_PCT}% used of ${TOTAL_K}k). Plenty of room — work freely.</context-status>"
elif [[ "$FREE_100K" -ge 400 ]]; then
echo "<context-status>Context: ~${FREE_100K}k tokens remaining (${USAGE_PCT}% used of ${TOTAL_K}k). Comfortable — continue normally.</context-status>"
elif [[ "$FREE_100K" -ge 200 ]]; then
echo "<context-status>Context: ~${FREE_100K}k tokens remaining (${USAGE_PCT}% used of ${TOTAL_K}k). Getting fuller — consider delegating heavy reads to sub-agents.</context-status>"
elif [[ "$FREE_100K" -ge 100 ]]; then
echo "<context-status>Context: ~${FREE_100K}k tokens remaining (${USAGE_PCT}% used of ${TOTAL_K}k). Running low — be surgical with file reads, delegate to sub-agents, checkpoint progress.</context-status>"
else
echo "<context-status>Context: ~${FREE_100K}k tokens remaining (${USAGE_PCT}% used of ${TOTAL_K}k). Critical — wrap up current task, save learnings, prepare for compaction.</context-status>"
fi
exit 0
+15
View File
@@ -44,6 +44,15 @@ mkdir -p "$HOME/.claude"
cp "$STATUSLINE_SCRIPT" "$INSTALL_PATH"
chmod +x "$INSTALL_PATH"
echo " Installed: $INSTALL_PATH"
# Copy context awareness hook
HOOK_SCRIPT="$SCRIPT_DIR/context-hook.sh"
HOOK_INSTALL_PATH="$HOME/.claude/context-hook.sh"
if [[ -f "$HOOK_SCRIPT" ]]; then
cp "$HOOK_SCRIPT" "$HOOK_INSTALL_PATH"
chmod +x "$HOOK_INSTALL_PATH"
echo " Installed: $HOOK_INSTALL_PATH"
fi
echo ""
# Update settings.json
@@ -88,4 +97,10 @@ echo " Line 1: Model, repo:branch, git status, agent name, worktree"
echo " Line 2: Latest commit + lines changed"
echo " Line 3: Context bricks, percentage, free tokens, duration, cost"
echo ""
echo "Context awareness hook installed at ~/.claude/context-hook.sh"
echo "To enable it, add to your ~/.claude/settings.json:"
echo ' "hooks": { "UserPromptSubmit": [{ "matcher": "", "hooks": ['
echo ' { "type": "command", "command": "bash ~/.claude/context-hook.sh" }'
echo ' ]}]}'
echo ""
echo "Restart Claude Code to see your new status line!"
+111 -41
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env node
// Claude Code Custom Status Line — Context Bricks
// v4.0.0 - Cross-platform Node.js version (no jq/bash dependencies)
// v4.1.0 - Rate limits, git caching, context awareness hook
//
// Line 1: [Model:style] repo:branch status | @agent | wt
// Line 2: [commit] message | +lines/-lines
// Line 3: [■■■■□□□□] 73%! | 52k free | 2h15m | $12.50
// Line 3: [■■■■□□□□] 73%! | 52k free | 2h15m | $12.50 | 5h:23% 7d:41%
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Read JSON from stdin
const chunks = [];
@@ -44,7 +46,22 @@ const c = {
cyanDim: '\x1b[0;36m',
};
// Safe git command execution using execFileSync (no shell injection risk)
// ── Git caching (5-second stale pattern from Claude Code docs) ──
const GIT_CACHE_FILE = '/tmp/contextbricks-git-cache.json';
const GIT_CACHE_MAX_AGE = 5; // seconds
function gitCacheIsStale() {
try {
if (!fs.existsSync(GIT_CACHE_FILE)) return true;
const age = (Date.now() / 1000) - fs.statSync(GIT_CACHE_FILE).mtimeMs / 1000;
return age > GIT_CACHE_MAX_AGE;
} catch {
return true;
}
}
// Safe git command execution
function git(...args) {
try {
return execFileSync('git', args, { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
@@ -53,15 +70,65 @@ function git(...args) {
}
}
function getGitInfo(currentDir) {
// Check cache first
if (!gitCacheIsStale()) {
try {
const cached = JSON.parse(fs.readFileSync(GIT_CACHE_FILE, 'utf8'));
// Only use cache if same directory
if (cached.dir === currentDir) return cached;
} catch { /* cache corrupt, refresh */ }
}
// Fresh git data
const isGit = git('rev-parse', '--git-dir') !== '';
const info = { dir: currentDir, isGit, repoName: '', branch: '', commitShort: '', commitMsg: '', gitStatus: '', inWorktree: false };
if (!isGit) {
try { fs.writeFileSync(GIT_CACHE_FILE, JSON.stringify(info)); } catch {}
return info;
}
const toplevel = git('rev-parse', '--show-toplevel');
info.repoName = toplevel ? toplevel.split('/').pop().split('\\').pop() : '';
info.branch = git('branch', '--show-current') || 'detached';
info.commitShort = git('rev-parse', '--short', 'HEAD');
info.commitMsg = git('log', '-1', '--pretty=%s').slice(0, 50);
// Worktree detection
const gitDir = git('rev-parse', '--git-dir');
info.inWorktree = gitDir.includes('/worktrees/') || gitDir.includes('\\worktrees\\');
// Status
const porcelain = git('status', '--porcelain');
if (porcelain) info.gitStatus = '*';
// Ahead/behind
const upstream = git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}');
if (upstream) {
const ahead = parseInt(git('rev-list', '--count', `${upstream}..HEAD`)) || 0;
const behind = parseInt(git('rev-list', '--count', `HEAD..${upstream}`)) || 0;
if (ahead > 0) info.gitStatus += `\u2191${ahead}`;
if (behind > 0) info.gitStatus += `\u2193${behind}`;
}
// Cache it
try { fs.writeFileSync(GIT_CACHE_FILE, JSON.stringify(info)); } catch {}
return info;
}
function main(data) {
// Parse Claude data
const model = (data.model?.display_name || 'Claude').replace('Claude ', '');
// Prefer project_dir (launch dir) over cwd (which can change)
const projectDir = data.workspace?.project_dir || data.workspace?.current_dir || process.env.PWD || process.cwd();
const currentDir = data.workspace?.current_dir || process.env.PWD || process.cwd();
const linesAdded = data.cost?.total_lines_added || 0;
const linesRemoved = data.cost?.total_lines_removed || 0;
const agentName = data.agent?.name || '';
const outputStyle = data.output_style?.name || '';
const exceeds200k = data.context_window?.exceeds_200k_tokens || false;
const exceeds200k = data.exceeds_200k_tokens || false;
// Style abbreviation
let styleAbbrev = '';
@@ -70,37 +137,11 @@ function main(data) {
styleAbbrev = map[outputStyle] || outputStyle.slice(0, 4);
}
// Change to workspace directory
// Change to workspace directory for git commands
try { process.chdir(currentDir); } catch { /* stay where we are */ }
// Git info
const isGit = git('rev-parse', '--git-dir') !== '';
let repoName = '', branch = '', commitShort = '', commitMsg = '', gitStatus = '', inWorktree = false;
if (isGit) {
const toplevel = git('rev-parse', '--show-toplevel');
repoName = toplevel ? toplevel.split('/').pop().split('\\').pop() : '';
branch = git('branch', '--show-current') || 'detached';
commitShort = git('rev-parse', '--short', 'HEAD');
commitMsg = git('log', '-1', '--pretty=%s').slice(0, 50);
// Worktree detection
const gitDir = git('rev-parse', '--git-dir');
inWorktree = gitDir.includes('/worktrees/') || gitDir.includes('\\worktrees\\');
// Status
const porcelain = git('status', '--porcelain');
if (porcelain) gitStatus = '*';
// Ahead/behind
const upstream = git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}');
if (upstream) {
const ahead = parseInt(git('rev-list', '--count', `${upstream}..HEAD`)) || 0;
const behind = parseInt(git('rev-list', '--count', `HEAD..${upstream}`)) || 0;
if (ahead > 0) gitStatus += `\u2191${ahead}`;
if (behind > 0) gitStatus += `\u2193${behind}`;
}
}
// Git info (cached)
const gi = getGitInfo(currentDir);
// ── Line 1: Session identity ──────────────────────────────
let line1 = '';
@@ -108,20 +149,20 @@ function main(data) {
? `${c.cyan}[${model}:${styleAbbrev}]${c.reset}`
: `${c.cyan}[${model}]${c.reset}`;
if (repoName) {
line1 += ` ${c.green}${repoName}${c.reset}`;
if (branch) line1 += `:${c.blue}${branch}${c.reset}`;
if (gi.repoName) {
line1 += ` ${c.green}${gi.repoName}${c.reset}`;
if (gi.branch) line1 += `:${c.blue}${gi.branch}${c.reset}`;
}
if (gitStatus) line1 += ` ${c.red}${gitStatus}${c.reset}`;
if (gi.gitStatus) line1 += ` ${c.red}${gi.gitStatus}${c.reset}`;
if (agentName) line1 += ` | ${c.magenta}@${agentName}${c.reset}`;
if (inWorktree) line1 += ` | ${c.yellow}wt${c.reset}`;
if (gi.inWorktree) line1 += ` | ${c.yellow}wt${c.reset}`;
// ── Line 2: Git state ─────────────────────────────────────
let line2 = '';
if (commitShort) {
line2 += `${c.dim}[${c.reset}${c.yellowDim}${commitShort}${c.reset}${c.dim}]${c.reset}`;
if (commitMsg) line2 += ` ${commitMsg}`;
if (gi.commitShort) {
line2 += `${c.dim}[${c.reset}${c.yellowDim}${gi.commitShort}${c.reset}${c.dim}]${c.reset}`;
if (gi.commitMsg) line2 += ` ${gi.commitMsg}`;
}
if (linesAdded > 0 || linesRemoved > 0) {
@@ -180,6 +221,35 @@ function main(data) {
line3 += ` | ${c.yellowDim}$${costUsd.toFixed(2)}${c.reset}`;
}
// Rate limits (Pro/Max subscribers)
const rl5h = data.rate_limits?.five_hour?.used_percentage;
const rl7d = data.rate_limits?.seven_day?.used_percentage;
if (rl5h != null || rl7d != null) {
let rlParts = [];
if (rl5h != null) {
const rlColor = rl5h >= 80 ? c.red : rl5h >= 50 ? c.yellowDim : c.dim;
rlParts.push(`${rlColor}5h:${Math.round(rl5h)}%${c.reset}`);
}
if (rl7d != null) {
const rlColor = rl7d >= 80 ? c.red : rl7d >= 50 ? c.yellowDim : c.dim;
rlParts.push(`${rlColor}7d:${Math.round(rl7d)}%${c.reset}`);
}
line3 += ` | ${rlParts.join(' ')}`;
}
// Persist context level for hooks (rounded to nearest 100k)
try {
const contextFile = path.join(process.env.HOME || '', '.claude', 'context-level.json');
const free100k = Math.floor(freeTokens / 100000) * 100; // e.g. 700 for 760k
fs.writeFileSync(contextFile, JSON.stringify({
freeK: freeK,
free100k: free100k,
usagePct: usagePct,
totalK: Math.floor(totalTokens / 1000),
ts: Date.now()
}));
} catch { /* non-critical — don't break the statusline */ }
// Output
console.log(line1);
console.log(line2);