From 8b975e92487214944c30d767d42bb66c06fb8533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 14:49:54 -0400 Subject: [PATCH 1/6] feat: add init tailwind flag --- docs/packages/cli.mdx | 4 ++ packages/cli/src/commands/init.test.ts | 35 ++++++++++++++++ packages/cli/src/commands/init.ts | 55 ++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index a26e9b2c..45bbc7d8 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -148,6 +148,9 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ # Agent mode (default) — --example is required npx hyperframes init my-video --example blank --video video.mp4 + # Include Tailwind CSS browser-runtime support + npx hyperframes init my-video --example blank --tailwind + # Human mode — interactive prompts npx hyperframes init --human-friendly ``` @@ -157,6 +160,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | `--example, -e` | Example to scaffold (required in default mode, interactive in `--human-friendly`) | | `--video, -V` | Path to a video file (MP4, WebM, MOV) | | `--audio, -a` | Path to an audio file (MP3, WAV, M4A) | + | `--tailwind` | Add Tailwind CSS browser-runtime support to scaffolded HTML | | `--skip-skills` | Skip AI coding skills installation | | `--skip-transcribe` | Skip automatic whisper transcription | | `--model` | Whisper model for transcription (e.g. `small.en`, `medium.en`, `large-v3`) | diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index 7002b8c7..461d8ba7 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -55,6 +55,41 @@ describe("hyperframes init flag rename", () => { } }); + it("--tailwind enables Tailwind utilities in scaffolded HTML", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); + const target = join(dir, "proj"); + try { + const res = runInit([ + target, + "--example", + "blank", + "--tailwind", + "--non-interactive", + "--skip-skills", + ]); + expect(res.status).toBe(0); + + const html = readFileSync(join(target, "index.html"), "utf-8"); + expect(html).toContain( + '', + ); + + const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf-8")) as { + scripts?: Record; + }; + expect(pkg.scripts).toMatchObject({ + dev: "npx --yes hyperframes preview", + check: + "npx --yes hyperframes lint && npx --yes hyperframes validate && npx --yes hyperframes inspect", + render: "npx --yes hyperframes render", + publish: "npx --yes hyperframes publish", + }); + expect(Object.keys(pkg.scripts ?? {}).sort()).toEqual(["check", "dev", "publish", "render"]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("--template prints a rename hint and exits non-zero", () => { const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); const target = join(dir, "proj"); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index fb7a7d12..509eea12 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,6 +6,7 @@ export const examples: Example[] = [ ["Pick a starter example", "hyperframes init my-video --example warm-grain"], ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"], ["Start from an audio file", "hyperframes init my-video --audio track.mp3"], + ["Scaffold with Tailwind CSS", "hyperframes init my-video --tailwind"], ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"], ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"], ]; @@ -54,6 +55,8 @@ const DEFAULT_META: VideoMeta = { videoCodec: "h264", }; +const TAILWIND_BROWSER_SRC = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"; + // --------------------------------------------------------------------------- // ffprobe helper — shells out to ffprobe to avoid engine dependency // --------------------------------------------------------------------------- @@ -189,6 +192,17 @@ function hyperframesScript(command: string): string { return `npx --yes ${getHyperframesPackageSpecifier()} ${command}`; } +function buildPackageScripts(): Record { + return { + dev: hyperframesScript("preview"), + check: + `${hyperframesScript("lint")} && ${hyperframesScript("validate")} && ` + + `${hyperframesScript("inspect")}`, + render: hyperframesScript("render"), + publish: hyperframesScript("publish"), + }; +} + function writeDefaultPackageJson(destDir: string, projectName: string): void { const packageJsonPath = resolve(destDir, "package.json"); if (existsSync(packageJsonPath)) return; @@ -200,12 +214,7 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void { name: toPackageName(projectName), private: true, type: "module", - scripts: { - dev: hyperframesScript("preview"), - check: `${hyperframesScript("lint")} && ${hyperframesScript("validate")} && ${hyperframesScript("inspect")}`, - render: hyperframesScript("render"), - publish: hyperframesScript("publish"), - }, + scripts: buildPackageScripts(), }, null, 2, @@ -214,6 +223,30 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void { ); } +function listHtmlFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true, recursive: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(".html")) + .map((entry) => join(entry.parentPath ?? entry.path, entry.name)); +} + +function injectTailwindBrowserScript(html: string): string { + if (html.includes(TAILWIND_BROWSER_SRC)) return html; + + const script = ``; + if (/]/.test(html)) { + return html.replace(/(\n[ \t]*)])/, `$1${script}$1/i, `$1${script}$1`); +} + +function writeTailwindSupport(destDir: string): void { + for (const file of listHtmlFiles(destDir)) { + const html = readFileSync(file, "utf-8"); + writeFileSync(file, injectTailwindBrowserScript(html), "utf-8"); + } +} + function patchVideoSrc( dir: string, videoFilename: string | undefined, @@ -358,6 +391,7 @@ async function scaffoldProject( templateId: string, localVideoName: string | undefined, durationSeconds?: number, + tailwind = false, ): Promise { mkdirSync(destDir, { recursive: true }); @@ -369,6 +403,7 @@ async function scaffoldProject( await fetchRemoteTemplate(templateId, destDir); } patchVideoSrc(destDir, localVideoName, durationSeconds); + if (tailwind) writeTailwindSupport(destDir); writeFileSync( resolve(destDir, "meta.json"), @@ -466,6 +501,10 @@ export default defineCommand({ type: "boolean", description: "Skip AI coding skills installation", }, + tailwind: { + type: "boolean", + description: "Add Tailwind CSS browser-runtime support", + }, }, async run({ args }) { if (args.template !== undefined) { @@ -483,6 +522,7 @@ export default defineCommand({ const audioFlag = args.audio; const skipTranscribe = args["skip-transcribe"] === true; const skipSkills = args["skip-skills"] === true; + const tailwind = args.tailwind === true; const nonInteractive = args["non-interactive"] === true; const modelFlag = args.model; const languageFlag = args.language; @@ -569,6 +609,7 @@ export default defineCommand({ templateId, localVideoName, videoDuration, + tailwind, ); } catch (err) { console.error( @@ -764,7 +805,7 @@ export default defineCommand({ spin.start(`Downloading example ${c.accent(templateId)}...`); } try { - await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration); + await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration, tailwind); if (!isBundled) { spin.stop(c.success(`Downloaded ${templateId}`)); } From 51807835486e064c321e6348d1fcd3d6a288b1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 14:58:04 -0400 Subject: [PATCH 2/6] fix: handle uppercase script tags for tailwind init --- packages/cli/src/commands/init.test.ts | 19 +++++++++++++++++++ packages/cli/src/commands/init.ts | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index 461d8ba7..c3795c57 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -4,6 +4,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { injectTailwindBrowserScript } from "./init.js"; const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts"); @@ -90,6 +91,24 @@ describe("hyperframes init flag rename", () => { } }); + it("inserts Tailwind before uppercase script tags", () => { + const html = [ + "", + "", + "", + ' ', + "", + "", + ].join("\n"); + + expect(injectTailwindBrowserScript(html)).toContain( + [ + '', + ' ', + ].join("\n"), + ); + }); + it("--template prints a rename hint and exits non-zero", () => { const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); const target = join(dir, "proj"); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 509eea12..44474706 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -229,12 +229,12 @@ function listHtmlFiles(dir: string): string[] { .map((entry) => join(entry.parentPath ?? entry.path, entry.name)); } -function injectTailwindBrowserScript(html: string): string { +export function injectTailwindBrowserScript(html: string): string { if (html.includes(TAILWIND_BROWSER_SRC)) return html; const script = ``; - if (/]/.test(html)) { - return html.replace(/(\n[ \t]*)])/, `$1${script}$1]/i.test(html)) { + return html.replace(/(\n[ \t]*)(])/i, `$1${script}$1$2$3`); } return html.replace(/(\n[ \t]*)<\/head>/i, `$1${script}$1`); From b9071d749be23dbfecae20411151e35e5e02499f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 15:19:20 -0400 Subject: [PATCH 3/6] ci: avoid apt ffmpeg install in preview regression --- .github/workflows/preview-regression.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/preview-regression.yml b/.github/workflows/preview-regression.yml index 9d4324ff..ebd95465 100644 --- a/.github/workflows/preview-regression.yml +++ b/.github/workflows/preview-regression.yml @@ -62,10 +62,7 @@ jobs: run: bun run --cwd packages/producer parity:fixtures:ci - name: Install ffmpeg - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends ffmpeg - ffmpeg -version | head -n 1 + uses: FedericoCarboni/setup-ffmpeg@36c6454b5a2348e7794ba2d82a21506605921e3d # v3 - name: Set up Chrome id: setup-chrome From da37d5ef62d18ee5448988997be32587644c088a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 16:25:18 -0400 Subject: [PATCH 4/6] fix: harden tailwind init support --- docs/packages/cli.mdx | 2 + packages/cli/src/commands/init.test.ts | 43 +++++++++++---- packages/cli/src/commands/init.ts | 57 ++++++++++++++++---- packages/cli/src/telemetry/events.ts | 4 +- packages/engine/src/services/frameCapture.ts | 22 ++++++++ 5 files changed, 105 insertions(+), 23 deletions(-) diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 45bbc7d8..49a9873e 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -177,6 +177,8 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ In default (agent) mode, `--example` is required — the CLI errors with a usage example if missing. In `--human-friendly` mode, you choose interactively. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable). + `--tailwind` injects the pinned Tailwind browser runtime into scaffolded HTML and exposes a `window.__tailwindReady` promise that renders wait on before capturing frame 0. The browser runtime is still intended for scaffolded projects and quick iteration; for fully offline or locked-down production renders, compile Tailwind to CSS and include the stylesheet directly. + After scaffolding, the CLI installs AI coding skills for Claude Code, Gemini CLI, and Codex CLI (use `--skip-skills` to disable). See [`skills`](#skills) command. See [Examples](/examples) for full details. diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index c3795c57..1e905509 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url"; import { injectTailwindBrowserScript } from "./init.js"; const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts"); +const tailwindScript = + ''; // Spawns `bun` directly because the CLI entry is a .ts file that needs a // TypeScript-aware runtime. vitest runs under node, so `process.execPath` @@ -71,9 +73,8 @@ describe("hyperframes init flag rename", () => { expect(res.status).toBe(0); const html = readFileSync(join(target, "index.html"), "utf-8"); - expect(html).toContain( - '', - ); + expect(html).toContain(tailwindScript); + expect(html).toContain("window.__tailwindReady"); const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf-8")) as { scripts?: Record; @@ -91,22 +92,44 @@ describe("hyperframes init flag rename", () => { } }); - it("inserts Tailwind before uppercase script tags", () => { + it("inserts Tailwind before uppercase closing head tags", () => { const html = [ "", "", "", ' ', - "", + "", "", ].join("\n"); - expect(injectTailwindBrowserScript(html)).toContain( - [ - '', - ' ', - ].join("\n"), + const injected = injectTailwindBrowserScript(html); + expect(injected.indexOf(' ')).toBeLessThan( + injected.indexOf(tailwindScript), ); + expect(injected.indexOf(tailwindScript)).toBeLessThan(injected.indexOf("")); + }); + + it("inserts Tailwind into single-line HTML heads", () => { + const html = "x"; + + expect(injectTailwindBrowserScript(html)).toContain(`${tailwindScript}\n`); + }); + + it("does not duplicate Tailwind support when it is already present", () => { + const html = ["", "", "", tailwindScript, "", ""].join( + "\n", + ); + + expect(injectTailwindBrowserScript(html)).toBe(html); + }); + + it("keeps the readiness shim free of render-loop APIs", () => { + const html = ""; + const injected = injectTailwindBrowserScript(html); + + expect(injected).not.toContain("Date.now"); + expect(injected).not.toContain("requestAnimationFrame"); + expect(injected).not.toContain("setTimeout"); }); it("--template prints a rename hint and exits non-zero", () => { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 44474706..798d2076 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,7 +6,7 @@ export const examples: Example[] = [ ["Pick a starter example", "hyperframes init my-video --example warm-grain"], ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"], ["Start from an audio file", "hyperframes init my-video --audio track.mp3"], - ["Scaffold with Tailwind CSS", "hyperframes init my-video --tailwind"], + ["Scaffold with Tailwind CSS", "hyperframes init my-video --example blank --tailwind"], ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"], ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"], ]; @@ -55,7 +55,12 @@ const DEFAULT_META: VideoMeta = { videoCodec: "h264", }; -const TAILWIND_BROWSER_SRC = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"; +// Pin the browser runtime exactly so repeated renders do not drift as Tailwind +// ships JIT/preflight changes on the CDN. +const TAILWIND_BROWSER_VERSION = "4.2.4"; +const TAILWIND_BROWSER_SRC = `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@${TAILWIND_BROWSER_VERSION}/dist/index.global.js`; +const TAILWIND_BROWSER_INTEGRITY = + "sha384-v5YF9xS+gLRWdvrQ0u/WRbCkjSIH0NjHIPe8tBL1ZRrmI7PiSH6LLdzs0aAIMCuh"; // --------------------------------------------------------------------------- // ffprobe helper — shells out to ffprobe to avoid engine dependency @@ -224,20 +229,50 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void { } function listHtmlFiles(dir: string): string[] { - return readdirSync(dir, { withFileTypes: true, recursive: true }) - .filter((entry) => entry.isFile() && entry.name.endsWith(".html")) - .map((entry) => join(entry.parentPath ?? entry.path, entry.name)); + const files: string[] = []; + const ignoredDirs = new Set([".git", "dist", "node_modules"]); + + function walk(currentDir: string): void { + for (const entry of readdirSync(currentDir, { withFileTypes: true })) { + const entryPath = join(currentDir, entry.name); + if (entry.isDirectory()) { + if (!ignoredDirs.has(entry.name)) walk(entryPath); + continue; + } + if (entry.isFile() && entry.name.endsWith(".html")) { + files.push(entryPath); + } + } + } + + walk(dir); + return files; } export function injectTailwindBrowserScript(html: string): string { if (html.includes(TAILWIND_BROWSER_SRC)) return html; - const script = ``; - if (/]/i.test(html)) { - return html.replace(/(\n[ \t]*)(])/i, `$1${script}$1$2$3`); + const script = [ + ``, + ``, + ].join("\n"); + + if (/<\/head>/i.test(html)) { + return html.replace(/<\/head>/i, (closingHead) => `\n${script}\n${closingHead}`); } - return html.replace(/(\n[ \t]*)<\/head>/i, `$1${script}$1`); + return `${script}\n${html}`; } function writeTailwindSupport(destDir: string): void { @@ -620,7 +655,7 @@ export default defineCommand({ console.error(c.dim("Use --example blank for offline use.")); process.exit(1); } - trackInitTemplate(templateId); + trackInitTemplate(templateId, { tailwind }); const transcriptFile = resolve(destDir, "transcript.json"); if (existsSync(transcriptFile)) { await patchTranscript(destDir, transcriptFile); @@ -818,7 +853,7 @@ export default defineCommand({ ); process.exit(1); } - trackInitTemplate(templateId); + trackInitTemplate(templateId, { tailwind }); // 4b. Patch captions with transcript if available const transcriptFile = resolve(destDir, "transcript.json"); diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts index 57269361..b26d81a1 100644 --- a/packages/cli/src/telemetry/events.ts +++ b/packages/cli/src/telemetry/events.ts @@ -105,8 +105,8 @@ export function trackRenderError(props: { }); } -export function trackInitTemplate(templateId: string): void { - trackEvent("init_template", { template: templateId }); +export function trackInitTemplate(templateId: string, props?: { tailwind?: boolean }): void { + trackEvent("init_template", { template: templateId, tailwind: props?.tailwind }); } export function trackBrowserInstall(): void { diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 6571ca7e..7d948f0c 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -260,6 +260,26 @@ async function applyVideoMetadataHints( ); } +async function waitForOptionalTailwindReady(page: Page, timeoutMs: number): Promise { + const hasTailwindReady = await page.evaluate( + `(() => { const ready = window.__tailwindReady; return !!ready && typeof ready.then === "function"; })()`, + ); + if (!hasTailwindReady) return; + + const ready = await Promise.race([ + page.evaluate( + `Promise.resolve(window.__tailwindReady).then(() => true, () => false)`, + ) as Promise, + new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)), + ]); + + if (!ready) { + throw new Error( + `[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`, + ); + } +} + export async function initializeSession(session: CaptureSession): Promise { const { page, serverUrl } = session; @@ -350,6 +370,7 @@ export async function initializeSession(session: CaptureSession): Promise } await page.evaluate(`document.fonts?.ready`); + await waitForOptionalTailwindReady(page, pageReadyTimeout); // For PNG captures, force the page background fully transparent so the // captured screenshots carry a real alpha channel. Must run AFTER @@ -443,6 +464,7 @@ export async function initializeSession(session: CaptureSession): Promise // Font check (no rAF dependency — uses fonts.ready API directly) await page.evaluate(`document.fonts?.ready`); + await waitForOptionalTailwindReady(page, pageReadyTimeout); // Stop warmup warmupRunning = false; From cbd85e6cab80c6726f4516b1541e5e4112698e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 17:30:51 -0400 Subject: [PATCH 5/6] docs: add tailwind composition skill --- .codex-plugin/plugin.json | 5 +- .cursor-plugin/plugin.json | 3 +- CLAUDE.md | 2 +- README.md | 2 +- docs/packages/cli.mdx | 4 +- docs/quickstart.mdx | 2 +- packages/cli/src/templates/_shared/AGENTS.md | 2 +- packages/cli/src/templates/_shared/CLAUDE.md | 1 + skills/hyperframes-cli/SKILL.md | 3 + skills/tailwind/SKILL.md | 150 +++++++++++++++++++ 10 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 skills/tailwind/SKILL.md diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 644a500e..a7ed04b3 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hyperframes", "version": "0.1.0", - "description": "Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", "email": "hyperframes@heygen.com", @@ -14,6 +14,7 @@ "hyperframes", "video", "html", + "tailwind", "gsap", "lottie", "three", @@ -30,7 +31,7 @@ "interface": { "displayName": "HyperFrames by HeyGen", "shortDescription": "Write HTML, render video", - "longDescription": "Build videos from HTML with HyperFrames. Author compositions with HTML, CSS, GSAP, Anime.js, Lottie, Three.js, and WAAPI adapter patterns, use the CLI for init/preview/render/transcribe/tts, install reusable registry blocks and components, and turn any website into a video with the 7-step capture-to-video pipeline.", + "longDescription": "Build videos from HTML with HyperFrames. Author compositions with HTML, CSS, Tailwind v4 browser-runtime styles, GSAP, Anime.js, Lottie, Three.js, and WAAPI adapter patterns, use the CLI for init/preview/render/transcribe/tts, install reusable registry blocks and components, and turn any website into a video with the 7-step capture-to-video pipeline.", "developerName": "HeyGen", "category": "Design", "capabilities": ["Read", "Write"], diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index cff26ffc..8d0d060d 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -3,7 +3,7 @@ "name": "hyperframes", "displayName": "HyperFrames by HeyGen", "version": "0.1.0", - "description": "Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", "email": "hyperframes@heygen.com" @@ -18,6 +18,7 @@ "hyperframes", "video", "html", + "tailwind", "gsap", "lottie", "three", diff --git a/CLAUDE.md b/CLAUDE.md index e2ff4e67..fcfa2911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,4 +67,4 @@ will not match CI. Use it only for local-only experimentation. ## Skills -Composition authoring (not repo development) is guided by skills installed via `npx skills add heygen-com/hyperframes`. See `skills/` for source. Invoke `/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, or `/gsap` when authoring compositions. Use `/animejs`, `/css-animations`, `/lottie`, `/three`, or `/waapi` when a composition uses those first-party runtime adapters. When a user provides a website URL and wants a video, invoke `/website-to-hyperframes` — it runs the full 7-step capture-to-video pipeline. +Composition authoring (not repo development) is guided by skills installed via `npx skills add heygen-com/hyperframes`. See `skills/` for source. Invoke `/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, `/tailwind`, or `/gsap` when authoring compositions. Use `/tailwind` for projects created with `hyperframes init --tailwind` so agents follow the pinned Tailwind v4 browser-runtime contract instead of Studio's Tailwind v3 setup. Use `/animejs`, `/css-animations`, `/lottie`, `/three`, or `/waapi` when a composition uses those first-party runtime adapters. When a user provides a website URL and wants a video, invoke `/website-to-hyperframes` — it runs the full 7-step capture-to-video pipeline. diff --git a/README.md b/README.md index df35baea..9cf27eaf 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Install the HyperFrames skills, then describe the video you want: npx skills add heygen-com/hyperframes ``` -This teaches your agent (Claude Code, Cursor, Gemini CLI, Codex) how to write correct compositions, GSAP timelines, and first-party adapter animations. In Claude Code, the skills register as slash commands — invoke `/hyperframes` to author compositions, `/hyperframes-cli` for CLI commands, `/gsap` for timeline animation help, or the adapter skills (`/animejs`, `/css-animations`, `/lottie`, `/three`, `/waapi`) when a composition uses those runtimes. +This teaches your agent (Claude Code, Cursor, Gemini CLI, Codex) how to write correct compositions, GSAP timelines, Tailwind v4 browser-runtime styles, and first-party adapter animations. In Claude Code, the skills register as slash commands — invoke `/hyperframes` to author compositions, `/hyperframes-cli` for CLI commands, `/tailwind` for `init --tailwind` projects, `/gsap` for timeline animation help, or the adapter skills (`/animejs`, `/css-animations`, `/lottie`, `/three`, `/waapi`) when a composition uses those runtimes. For Claude Design, open [`docs/guides/claude-design-hyperframes.md`](https://github.com/heygen-com/hyperframes/blob/main/docs/guides/claude-design-hyperframes.md) on GitHub and click the download button (↓) to save it, then attach the file to your Claude Design chat. It produces a valid first draft; refine in any AI coding agent. See the [Claude Design guide](https://hyperframes.heygen.com/guides/claude-design). diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 49a9873e..c2b60118 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -177,7 +177,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ In default (agent) mode, `--example` is required — the CLI errors with a usage example if missing. In `--human-friendly` mode, you choose interactively. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable). - `--tailwind` injects the pinned Tailwind browser runtime into scaffolded HTML and exposes a `window.__tailwindReady` promise that renders wait on before capturing frame 0. The browser runtime is still intended for scaffolded projects and quick iteration; for fully offline or locked-down production renders, compile Tailwind to CSS and include the stylesheet directly. + `--tailwind` injects the pinned Tailwind v4 browser runtime into scaffolded HTML and exposes a `window.__tailwindReady` promise that renders wait on before capturing frame 0. Use the `/tailwind` skill when editing these projects so agents follow v4 CSS-first patterns instead of v3 `tailwind.config.js` and `@tailwind` directive patterns. The browser runtime is still intended for scaffolded projects and quick iteration; for fully offline or locked-down production renders, compile Tailwind to CSS and include the stylesheet directly. After scaffolding, the CLI installs AI coding skills for Claude Code, Gemini CLI, and Codex CLI (use `--skip-skills` to disable). See [`skills`](#skills) command. @@ -722,7 +722,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | `--codex` | Install to Codex CLI (`~/.codex/skills/`) | | `--cursor` | Install to Cursor (`.cursor/skills/` in current project) | - Skills are fetched from GitHub and include composition authoring, GSAP animation patterns, Anime.js, CSS animation, Lottie, Three.js, and WAAPI adapter patterns, registry block/component wiring, and other domain-specific knowledge. The `init` command also offers to install skills automatically after scaffolding a project. + Skills are fetched from GitHub and include composition authoring, Tailwind v4 browser-runtime guidance, GSAP animation patterns, Anime.js, CSS animation, Lottie, Three.js, and WAAPI adapter patterns, registry block/component wiring, and other domain-specific knowledge. The `init` command also offers to install skills automatically after scaffolding a project. #### Troubleshooting: `fatal: active post-checkout hook found during git clone` diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 38552f52..561bc0fd 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -13,7 +13,7 @@ Install the HyperFrames skills, then describe the video you want: npx skills add heygen-com/hyperframes ``` -This teaches your agent (Claude Code, Cursor, Gemini CLI, Codex) how to write correct compositions, GSAP timelines, and first-party adapter animations. In Claude Code the skills register as slash commands — `/hyperframes` for composition authoring, `/hyperframes-cli` for CLI commands, `/gsap` for timeline animation help, and `/animejs`, `/css-animations`, `/lottie`, `/three`, or `/waapi` when a composition uses those runtimes. Invoking the slash command loads the skill context explicitly, which produces correct output the first time. +This teaches your agent (Claude Code, Cursor, Gemini CLI, Codex) how to write correct compositions, GSAP timelines, Tailwind v4 browser-runtime styles, and first-party adapter animations. In Claude Code the skills register as slash commands — `/hyperframes` for composition authoring, `/hyperframes-cli` for CLI commands, `/tailwind` for `init --tailwind` projects, `/gsap` for timeline animation help, and `/animejs`, `/css-animations`, `/lottie`, `/three`, or `/waapi` when a composition uses those runtimes. Invoking the slash command loads the skill context explicitly, which produces correct output the first time. Claude Design uses a different entry path. Open [`docs/guides/claude-design-hyperframes.md`](https://github.com/heygen-com/hyperframes/blob/main/docs/guides/claude-design-hyperframes.md) on GitHub, click the download button (↓) to save it, then attach to your Claude Design chat. It produces a valid first draft you can refine in any AI coding agent. See the [Claude Design guide](/guides/claude-design). diff --git a/packages/cli/src/templates/_shared/AGENTS.md b/packages/cli/src/templates/_shared/AGENTS.md index 9374d67c..4f5e90ce 100644 --- a/packages/cli/src/templates/_shared/AGENTS.md +++ b/packages/cli/src/templates/_shared/AGENTS.md @@ -8,7 +8,7 @@ This project uses AI agent skills for framework-specific patterns. Install them npx skills add heygen-com/hyperframes ``` -Skills encode patterns like `window.__timelines` registration, `data-*` attribute semantics, and shader-compatible CSS rules that are not in generic web docs. Using them produces correct compositions from the start. +Skills encode patterns like `window.__timelines` registration, `data-*` attribute semantics, Tailwind v4 browser-runtime styling for `--tailwind` projects, and shader-compatible CSS rules that are not in generic web docs. Using them produces correct compositions from the start. ## Commands diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md index 849ba0e3..b414417e 100644 --- a/packages/cli/src/templates/_shared/CLAUDE.md +++ b/packages/cli/src/templates/_shared/CLAUDE.md @@ -10,6 +10,7 @@ | **hyperframes-cli** | `/hyperframes-cli` | CLI commands: init, lint, preview, render, transcribe, tts | | **hyperframes-registry** | `/hyperframes-registry` | Installing blocks and components via `hyperframes add` | | **website-to-hyperframes** | `/website-to-hyperframes` | Capturing a URL and turning it into a video — full website-to-video pipeline | +| **tailwind** | `/tailwind` | Tailwind v4 browser-runtime styles for projects created with `hyperframes init --tailwind` | | **gsap** | `/gsap` | GSAP animations for HyperFrames — tweens, timelines, easing, performance | | **animejs** | `/animejs` | Anime.js animations registered on `window.__hfAnime` | | **css-animations** | `/css-animations` | CSS keyframes that HyperFrames can pause and seek | diff --git a/skills/hyperframes-cli/SKILL.md b/skills/hyperframes-cli/SKILL.md index ef4c231a..3d47fe2c 100644 --- a/skills/hyperframes-cli/SKILL.md +++ b/skills/hyperframes-cli/SKILL.md @@ -25,6 +25,7 @@ npx hyperframes init my-video # interactive wizard npx hyperframes init my-video --example warm-grain # pick an example npx hyperframes init my-video --video clip.mp4 # with video file npx hyperframes init my-video --audio track.mp3 # with audio file +npx hyperframes init my-video --example blank --tailwind # with Tailwind v4 browser runtime npx hyperframes init my-video --non-interactive # skip prompts (CI/agents) ``` @@ -32,6 +33,8 @@ Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decisi `init` creates the right file structure, copies media, transcribes audio with Whisper, and installs AI coding skills. Use it instead of creating files by hand. +When using `--tailwind`, invoke the `tailwind` skill before editing classes or theme tokens. The scaffold uses Tailwind v4.2 via the browser runtime, not Studio's Tailwind v3 setup. + ## Linting ```bash diff --git a/skills/tailwind/SKILL.md b/skills/tailwind/SKILL.md new file mode 100644 index 00000000..de6fa0f8 --- /dev/null +++ b/skills/tailwind/SKILL.md @@ -0,0 +1,150 @@ +--- +name: tailwind +description: Tailwind CSS v4.2 browser-runtime patterns for HyperFrames compositions. Use when scaffolding or editing projects created with `hyperframes init --tailwind`, writing Tailwind utility classes in composition HTML, adding CSS-first Tailwind v4 theme tokens, debugging v3 vs v4 syntax, or deciding when to compile Tailwind to CSS instead of using the browser runtime. +--- + +# Tailwind CSS for HyperFrames + +HyperFrames `init --tailwind` uses the Tailwind browser runtime pinned to `@tailwindcss/browser@4.2.4`. Treat that as Tailwind v4, not v3. + +This skill is for composition HTML generated by the CLI. It is not for `packages/studio`, which still uses Tailwind v3 internally with `tailwind.config.js`, PostCSS, and `@tailwind` directives. + +## When To Use + +- The user asks for Tailwind in a HyperFrames composition. +- A project was created with `hyperframes init --tailwind`. +- You see `window.__tailwindReady` in `index.html`. +- You need utility classes, CSS-first theme tokens, custom utilities, or v3-to-v4 migration guidance. +- The render has missing styles and the project is relying on the browser runtime. + +## Version Contract + +- Pinned runtime: `@tailwindcss/browser@4.2.4`. +- Browser runtime script is injected by the CLI. Do not replace it with `cdn.tailwindcss.com`. +- HyperFrames waits for `window.__tailwindReady` before frame capture starts. +- The readiness shim must stay deterministic: no render-loop polling APIs, no clock-based retries, no runtime network fetches beyond the pinned Tailwind runtime script. +- For offline, locked-down, or production-stable renders, compile Tailwind to CSS and include the stylesheet directly instead of relying on the browser runtime. + +## v4 Rules + +Tailwind v4 is CSS-first: + +```html + +``` + +Avoid v3 setup patterns in browser-runtime compositions: + +```css +/* Do not use these in Tailwind v4 browser-runtime compositions. */ +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +Do not add a `tailwind.config.js` just to define colors, fonts, spacing, or utilities for a v4 browser-runtime composition. Use `@theme` and `@utility` in a `text/tailwindcss` style block. + +If you truly need an existing JavaScript config for a compiled v4 build, load it explicitly from CSS with `@config`, then validate in the browser. Do not assume v4 auto-detects v3 config files. + +## HyperFrames Composition Pattern + +Keep Tailwind responsible for static layout and visual style. Keep motion timing in GSAP or another seekable adapter. + +```html +
+
+

+ Render-ready Tailwind +

+

+ Utility classes, deterministic frames. +

+
+
+``` + +For repeated items, prefer class lists plus CSS custom properties over generating class names dynamically: + +```html + + + +``` + +## Dynamic Class Safety + +Tailwind's browser runtime scans the current document and generates CSS for class names it can see. Do not build render-critical class names only at seek time: + +```js +// Risky: Tailwind may not see every generated class before capture. +element.className = `bg-${color}-500`; +``` + +Use complete class names in HTML, data attributes, or explicit CSS instead: + +```html +
+``` + +If a generated class is unavoidable, make sure the full class token appears in a `text/tailwindcss` block before validation. + +## Video-Specific Guardrails + +- Use stable dimensions: `w-[...]`, `h-[...]`, `aspect-video`, `grid`, `flex`, and fixed padding for video layouts. +- Prefer transforms and opacity for animated properties. +- Keep Tailwind transitions out of render-critical timing unless a seekable runtime owns the state. +- Avoid hover, focus, scroll, viewport, or pointer variants for content that must render deterministically. +- Use explicit border colors. Tailwind v4 changed the default border behavior from v3, so `border border-white/20` is safer than bare `border`. +- Use v4 utility names: `shadow-xs`, `rounded-xs`, `outline-hidden`, `shrink-*`, and `grow-*` where those replacements apply. +- Be careful with modern CSS utilities if the output needs older browser support. Tailwind v4 targets modern browsers. + +## Validation + +After editing a Tailwind-enabled composition: + +```bash +npx hyperframes lint +npx hyperframes validate +npx hyperframes inspect +``` + +For a render proof: + +```bash +npx hyperframes render . --workers 1 --quality draft --output tailwind-proof.mp4 +``` + +The validation path should show no missing-style flashes on frame 0. If styles appear in preview but not render, check that `window.__tailwindReady` exists and resolves before capture. + +## Quick Debug Checklist + +1. Confirm the project was scaffolded with `hyperframes init --tailwind`. +2. Confirm the script points to `@tailwindcss/browser@4.2.4`. +3. Confirm `window.__tailwindReady` is present. +4. Replace v3 `@tailwind` directives with v4 browser-runtime CSS. +5. Move custom tokens from `tailwind.config.js` to `@theme`. +6. Replace dynamically assembled classes with complete static tokens. +7. Run `npx hyperframes validate` and render a short proof. + +## Credits And References + +- Tailwind CSS official v4 installation, upgrade, and compatibility docs: https://tailwindcss.com/docs +- Tailwind CSS v4 release notes: https://tailwindcss.com/blog/tailwindcss-v4 +- Community skill references reviewed for v4 gotchas and skill shape: + - https://skills.sh/tlq5l/tailwindcss-v4-skill/tailwindcss-v4 + - https://agent-skills.md/skills/bout3fiddy/agents/tailwindcss From d8843fb0b805a8b50afe452e87b950de03f9ae13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 17:45:24 -0400 Subject: [PATCH 6/6] docs: tighten tailwind skill references --- skills/tailwind/SKILL.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skills/tailwind/SKILL.md b/skills/tailwind/SKILL.md index de6fa0f8..6453f2d2 100644 --- a/skills/tailwind/SKILL.md +++ b/skills/tailwind/SKILL.md @@ -36,7 +36,7 @@ Tailwind v4 is CSS-first: --font-display: "Inter", sans-serif; } - @utility text-balance-tight { + @utility headline-balance { text-wrap: balance; letter-spacing: 0; } @@ -145,6 +145,4 @@ The validation path should show no missing-style flashes on frame 0. If styles a - Tailwind CSS official v4 installation, upgrade, and compatibility docs: https://tailwindcss.com/docs - Tailwind CSS v4 release notes: https://tailwindcss.com/blog/tailwindcss-v4 -- Community skill references reviewed for v4 gotchas and skill shape: - - https://skills.sh/tlq5l/tailwindcss-v4-skill/tailwindcss-v4 - - https://agent-skills.md/skills/bout3fiddy/agents/tailwindcss +- Community Tailwind skills were reviewed for v4 gotchas and skill shape, but this skill keeps the durable contract in-repo and HyperFrames-specific.