mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat: add init tailwind flag (#577)
## Problem Users who want Tailwind utilities in a plain HyperFrames composition currently have to know which Tailwind browser script to add and where to place it. The first pass added `--tailwind`, but review caught three production-facing gaps: the CDN version was major-only, the insertion helper could silently no-op on compact HTML, and the render pipeline did not explicitly wait for Tailwind's async browser compilation before capturing frame 0. There is also a version-specific agent risk: HyperFrames `init --tailwind` uses Tailwind v4.2 through `@tailwindcss/browser@4.2.4`, while `packages/studio` still uses Tailwind v3. Without a dedicated skill, agents can easily mix v3 `tailwind.config.js` / `@tailwind` patterns into v4 browser-runtime composition HTML. ## What this fixes - Adds `hyperframes init --tailwind`. - Pins the Tailwind browser runtime to `@tailwindcss/browser@4.2.4/dist/index.global.js` with SRI and `crossorigin="anonymous"`. - Injects a `window.__tailwindReady` promise next to the browser runtime. - Makes frame capture wait for `window.__tailwindReady` in both screenshot and BeginFrame capture modes before capturing frame 0. - Inserts Tailwind support before `</head>` case-insensitively, including single-line/minified heads, and falls back to prepending when there is no head tag. - Skips recursive Tailwind injection under `.git`, `dist`, and `node_modules`. - Tracks whether init used Tailwind in the existing `init_template` telemetry event. - Adds a first-party `/tailwind` skill for Tailwind v4.2 browser-runtime HyperFrames composition work. - Updates README, docs, generated project agent files, CLI skill guidance, and plugin metadata so the Tailwind skill is discoverable. - Documents the browser-runtime tradeoff and production/offline guidance. ## Root cause `scaffoldProject()` copied the selected example and patched media placeholders, then immediately wrote project metadata and `package.json`. There was no optional post-copy step for framework-specific HTML support. The initial Tailwind post-copy step also treated the browser runtime like a static script, but Tailwind compiles utilities asynchronously after scanning the DOM, so the capture engine needed an explicit readiness contract. On the agent side, the repo exposed HyperFrames, CLI, GSAP, registry, and runtime adapter skills, but had no Tailwind-specific instruction to separate the v4 browser-runtime composition path from Studio's v3 internal setup. ## Verification ### Local checks - `bunx vitest run packages/cli/src/commands/init.test.ts` - `bun run --filter @hyperframes/cli test src/commands/init.test.ts` - `bun run --filter @hyperframes/cli typecheck` - `bun run --filter @hyperframes/engine typecheck` - `bun run lint:skills` - `bun run lint` - `npx skills add . --list` showed 12 local skills, including `tailwind`. - `bunx oxfmt --check packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts packages/cli/src/telemetry/events.ts packages/engine/src/services/frameCapture.ts docs/packages/cli.mdx` - `bunx oxfmt --check README.md docs/quickstart.mdx docs/packages/cli.mdx CLAUDE.md packages/cli/src/templates/_shared/CLAUDE.md packages/cli/src/templates/_shared/AGENTS.md skills/hyperframes-cli/SKILL.md skills/tailwind/SKILL.md .codex-plugin/plugin.json .cursor-plugin/plugin.json` - `bunx oxlint packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts packages/cli/src/telemetry/events.ts packages/engine/src/services/frameCapture.ts` - `git diff --check` - Lefthook pre-commit: lint/format/typecheck for code commit; format for docs/skill commit - Lefthook commit-msg: commitlint Generated-project render proof at `/tmp/hf-tailwind-render-proof`: - `bun packages/cli/src/cli.ts init /tmp/hf-tailwind-render-proof --example blank --tailwind --non-interactive --skip-skills` - Added a temporary Tailwind-only card using `flex`, `h-full`, `w-full`, `items-center`, `justify-center`, `bg-slate-950`, `rounded-3xl`, `bg-white`, `px-20`, `py-12`, `text-8xl`, `font-black`, `text-black`, and `shadow-2xl`. - `bun packages/cli/src/cli.ts lint /tmp/hf-tailwind-render-proof` → 0 errors, 0 warnings. - `bun packages/cli/src/cli.ts validate /tmp/hf-tailwind-render-proof` → 0 errors, 0 regular warnings; the temp proof still reports validator contrast warnings even though the rendered/browser pixels show black text on white background. - `bun packages/cli/src/cli.ts render /tmp/hf-tailwind-render-proof --workers 1 --fps 24 --quality draft --output /tmp/hf-tailwind-render-proof-artifacts/output.mp4` - Render compiler inlined both GSAP and `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.2.4/dist/index.global.js`. - `ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,duration -of default=noprint_wrappers=1 /tmp/hf-tailwind-render-proof-artifacts/output.mp4` → H.264, 1920x1080, 24fps, 10s. - Extracted frame-0 proof: `/tmp/hf-tailwind-render-proof-artifacts/frame-000.png`. ### Browser verification - Started Studio preview for `/tmp/hf-tailwind-render-proof`. - Used `agent-browser` to open `http://localhost:5194`. - Verified the Tailwind-styled composition rendered in Studio preview. - Captured screenshot: `/tmp/hf-tailwind-render-proof-artifacts/browser/tailwind-preview.png`. - Captured agent-browser-driven recording: `/tmp/hf-tailwind-render-proof-artifacts/browser/tailwind-preview.webm`. - Served the PR worktree locally and used `agent-browser` to open the new Tailwind skill proof page. - Verified the browser-visible skill content includes `@tailwindcss/browser@4.2.4`. - Captured screenshot: `/Users/miguel07code/.codex/worktrees/pr-577-tailwind-comments/tmp/agent-browser-proof/tailwind-skill.png`. - Captured agent-browser-driven recording: `/Users/miguel07code/.codex/worktrees/pr-577-tailwind-comments/tmp/agent-browser-proof/tailwind-skill.webm`. ## Notes - This still intentionally uses Tailwind's browser runtime rather than adding a generated Tailwind build pipeline. That keeps `hyperframes init --tailwind` small and compatible with the current no-install generated project workflow. - The `/tailwind` skill cites official Tailwind v4 docs plus community skill references, but its instructions are HyperFrames-specific and tuned for the pinned v4.2 browser runtime. - Browser proof artifacts are local-only under `/tmp/hf-tailwind-render-proof-artifacts/` and `tmp/agent-browser-proof/` and intentionally not committed.
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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`) |
|
||||
@@ -173,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 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.
|
||||
|
||||
See [Examples](/examples) for full details.
|
||||
@@ -716,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`
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
<Note>
|
||||
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).
|
||||
|
||||
@@ -4,8 +4,11 @@ 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");
|
||||
const tailwindScript =
|
||||
'<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.2.4/dist/index.global.js" integrity="sha384-v5YF9xS+gLRWdvrQ0u/WRbCkjSIH0NjHIPe8tBL1ZRrmI7PiSH6LLdzs0aAIMCuh" crossorigin="anonymous"></script>';
|
||||
|
||||
// Spawns `bun` directly because the CLI entry is a .ts file that needs a
|
||||
// TypeScript-aware runtime. vitest runs under node, so `process.execPath`
|
||||
@@ -55,6 +58,80 @@ 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(tailwindScript);
|
||||
expect(html).toContain("window.__tailwindReady");
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf-8")) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
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("inserts Tailwind before uppercase closing head tags", () => {
|
||||
const html = [
|
||||
"<!doctype html>",
|
||||
"<html>",
|
||||
"<head>",
|
||||
' <SCRIPT src="./runtime.global.js"></SCRIPT>',
|
||||
"</HEAD>",
|
||||
"</html>",
|
||||
].join("\n");
|
||||
|
||||
const injected = injectTailwindBrowserScript(html);
|
||||
expect(injected.indexOf(' <SCRIPT src="./runtime.global.js"></SCRIPT>')).toBeLessThan(
|
||||
injected.indexOf(tailwindScript),
|
||||
);
|
||||
expect(injected.indexOf(tailwindScript)).toBeLessThan(injected.indexOf("</HEAD>"));
|
||||
});
|
||||
|
||||
it("inserts Tailwind into single-line HTML heads", () => {
|
||||
const html = "<!doctype html><html><head><title>x</title></head><body></body></html>";
|
||||
|
||||
expect(injectTailwindBrowserScript(html)).toContain(`${tailwindScript}\n</head>`);
|
||||
});
|
||||
|
||||
it("does not duplicate Tailwind support when it is already present", () => {
|
||||
const html = ["<!doctype html>", "<html>", "<head>", tailwindScript, "</head>", "</html>"].join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
expect(injectTailwindBrowserScript(html)).toBe(html);
|
||||
});
|
||||
|
||||
it("keeps the readiness shim free of render-loop APIs", () => {
|
||||
const html = "<!doctype html><html><head></head><body></body></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", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-init-test-"));
|
||||
const target = join(dir, "proj");
|
||||
|
||||
@@ -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 --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"],
|
||||
];
|
||||
@@ -54,6 +55,13 @@ const DEFAULT_META: VideoMeta = {
|
||||
videoCodec: "h264",
|
||||
};
|
||||
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -189,6 +197,17 @@ function hyperframesScript(command: string): string {
|
||||
return `npx --yes ${getHyperframesPackageSpecifier()} ${command}`;
|
||||
}
|
||||
|
||||
function buildPackageScripts(): Record<string, string> {
|
||||
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 +219,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 +228,60 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void {
|
||||
);
|
||||
}
|
||||
|
||||
function listHtmlFiles(dir: string): string[] {
|
||||
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 = [
|
||||
`<script>`,
|
||||
`window.__tailwindReady=new Promise(function(resolve){`,
|
||||
`var loaded=document.readyState==="complete";`,
|
||||
`var resolved=false;`,
|
||||
`var observer;`,
|
||||
`function readTailwindCss(){var styles=document.querySelectorAll("style");for(var i=styles.length-1;i>=0;i--){var text=styles[i].textContent||"";if(text.indexOf("tailwindcss v")!==-1)return text;}return "";}`,
|
||||
`function finish(){if(resolved||!loaded||!readTailwindCss())return;resolved=true;if(observer)observer.disconnect();resolve(true);}`,
|
||||
`observer=new MutationObserver(finish);`,
|
||||
`observer.observe(document.documentElement,{childList:true,subtree:true,characterData:true});`,
|
||||
`if(loaded){finish();}else{window.addEventListener("load",function(){loaded=true;finish();},{once:true});}`,
|
||||
`});`,
|
||||
`</script>`,
|
||||
`<script src="${TAILWIND_BROWSER_SRC}" integrity="${TAILWIND_BROWSER_INTEGRITY}" crossorigin="anonymous"></script>`,
|
||||
].join("\n");
|
||||
|
||||
if (/<\/head>/i.test(html)) {
|
||||
return html.replace(/<\/head>/i, (closingHead) => `\n${script}\n${closingHead}`);
|
||||
}
|
||||
|
||||
return `${script}\n${html}`;
|
||||
}
|
||||
|
||||
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 +426,7 @@ async function scaffoldProject(
|
||||
templateId: string,
|
||||
localVideoName: string | undefined,
|
||||
durationSeconds?: number,
|
||||
tailwind = false,
|
||||
): Promise<void> {
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
|
||||
@@ -369,6 +438,7 @@ async function scaffoldProject(
|
||||
await fetchRemoteTemplate(templateId, destDir);
|
||||
}
|
||||
patchVideoSrc(destDir, localVideoName, durationSeconds);
|
||||
if (tailwind) writeTailwindSupport(destDir);
|
||||
|
||||
writeFileSync(
|
||||
resolve(destDir, "meta.json"),
|
||||
@@ -466,6 +536,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 +557,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 +644,7 @@ export default defineCommand({
|
||||
templateId,
|
||||
localVideoName,
|
||||
videoDuration,
|
||||
tailwind,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
@@ -579,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);
|
||||
@@ -764,7 +840,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}`));
|
||||
}
|
||||
@@ -777,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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -260,6 +260,26 @@ async function applyVideoMetadataHints(
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForOptionalTailwindReady(page: Page, timeoutMs: number): Promise<void> {
|
||||
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<boolean>,
|
||||
new Promise<boolean>((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<void> {
|
||||
const { page, serverUrl } = session;
|
||||
|
||||
@@ -350,6 +370,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
}
|
||||
|
||||
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<void>
|
||||
|
||||
// Font check (no rAF dependency — uses fonts.ready API directly)
|
||||
await page.evaluate(`document.fonts?.ready`);
|
||||
await waitForOptionalTailwindReady(page, pageReadyTimeout);
|
||||
|
||||
// Stop warmup
|
||||
warmupRunning = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
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
|
||||
<style type="text/tailwindcss">
|
||||
@theme {
|
||||
--color-brand: oklch(0.68 0.2 252);
|
||||
--font-display: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
@utility headline-balance {
|
||||
text-wrap: balance;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
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
|
||||
<section
|
||||
class="clip absolute inset-0 grid place-items-center bg-zinc-950 text-white"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
>
|
||||
<div class="w-[1280px] max-w-[82vw] text-center">
|
||||
<p class="mb-6 text-xl font-medium uppercase tracking-[0.18em] text-cyan-300">
|
||||
Render-ready Tailwind
|
||||
</p>
|
||||
<h1 class="text-7xl font-black leading-none text-balance">
|
||||
Utility classes, deterministic frames.
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
For repeated items, prefer class lists plus CSS custom properties over generating class names dynamically:
|
||||
|
||||
```html
|
||||
<span class="inline-block translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 0"></span>
|
||||
<span class="inline-block translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 1"></span>
|
||||
<span class="inline-block translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 2"></span>
|
||||
```
|
||||
|
||||
## 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
|
||||
<div data-tone="blue" class="bg-blue-500 data-[tone=rose]:bg-rose-500"></div>
|
||||
```
|
||||
|
||||
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 Tailwind skills were reviewed for v4 gotchas and skill shape, but this skill keeps the durable contract in-repo and HyperFrames-specific.
|
||||
Reference in New Issue
Block a user