mirror of
https://github.com/jezweb/claude-skills.git
synced 2026-09-19 01:07:27 +08:00
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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
Executable
+58
@@ -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
|
||||
@@ -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!"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user