diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 7d989db..e2e77c6 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -1360,6 +1360,11 @@ Cost tier: `free` - `noTranscribe` _(boolean)_ — Skip Whisper word-level transcribe step (no transcript-.json emitted) - `transcribeLanguage` _(string)_ — BCP-47 language code passed to Whisper (e.g. en, ko) - `force` _(boolean)_ — Overwrite an existing compositions/scene-.html +- `lottie` _(string)_ — Lottie animation file (.json/.lottie) to overlay on the scene +- `lottiePosition` _(string)_ _(full \| center \| top-left \| top-right \| bottom-left \| bottom-right)_ _(default: `"full"`)_ — Lottie position: full, center, top-left, top-right, bottom-left, bottom-right +- `lottieScale` _(number)_ — Lottie overlay scale (0.01-2) +- `lottieOpacity` _(number)_ _(default: `1`)_ — Lottie overlay opacity (0-1) +- `lottieNoLoop` _(boolean)_ — Do not loop the Lottie animation - `dryRun` _(boolean)_ — Preview parameters without writing files or calling APIs #### `vibe scene compose-prompts` diff --git a/packages/cli/src/commands/__snapshots__/envelope-snapshots.test.ts.snap b/packages/cli/src/commands/__snapshots__/envelope-snapshots.test.ts.snap index f0feb4c..0137dd1 100644 --- a/packages/cli/src/commands/__snapshots__/envelope-snapshots.test.ts.snap +++ b/packages/cli/src/commands/__snapshots__/envelope-snapshots.test.ts.snap @@ -3459,6 +3459,36 @@ exports[`CLI --describe schemas (drift detection) > vibe scene add --describe 1` "description": "Small label above the headline (explainer / product-shot)", "type": "string", }, + "lottie": { + "description": "Lottie animation file (.json/.lottie) to overlay on the scene", + "type": "string", + }, + "lottieNoLoop": { + "description": "Do not loop the Lottie animation", + "type": "boolean", + }, + "lottieOpacity": { + "default": 1, + "description": "Lottie overlay opacity (0-1)", + "type": "number", + }, + "lottiePosition": { + "default": "full", + "description": "Lottie position: full, center, top-left, top-right, bottom-left, bottom-right", + "enum": [ + "full", + "center", + "top-left", + "top-right", + "bottom-left", + "bottom-right", + ], + "type": "string", + }, + "lottieScale": { + "description": "Lottie overlay scale (0.01-2)", + "type": "number", + }, "name": { "description": "Scene name (slugified into the composition id)", "type": "string", diff --git a/packages/cli/src/commands/_shared/scene-html-emit.test.ts b/packages/cli/src/commands/_shared/scene-html-emit.test.ts index 678c098..6374297 100644 --- a/packages/cli/src/commands/_shared/scene-html-emit.test.ts +++ b/packages/cli/src/commands/_shared/scene-html-emit.test.ts @@ -630,3 +630,115 @@ describe("emitSceneHtml — robust defaults across input variability", () => { } }); }); + +// ── Lottie overlay layer ──────────────────────────────────────────────────── +// +// Regression coverage for the bug where the emit path pointed at a +// non-existent CDN package (`@nicepkg/dotlottie-wc`), so `` +// never registered and the overlay silently did not render. These tests +// pin the loader to the real package (`@lottiefiles/dotlottie-wc`), +// require an explicit `setWasmUrl(...)` call (the runtime needs its +// WASM player or it 404s), and lock in the per-scene id so multiple +// scenes with overlays don't collide. + +describe("emitSceneHtml — Lottie overlay layer", () => { + const lottieBase = { + ...baseInput, + preset: "simple" as const, + lottie: { + src: "assets/lottie-intro.json", + position: "full" as const, + }, + }; + + it("renders nothing Lottie-related when no overlay is provided", () => { + const html = emitSceneHtml({ ...baseInput, preset: "simple" }); + expect(html).not.toContain("dotlottie-wc"); + expect(html).not.toContain("setWasmUrl"); + }); + + it("loads the runtime from the real @lottiefiles/dotlottie-wc package on jsdelivr", () => { + const html = emitSceneHtml(lottieBase); + // The custom element + a module script that registers it must both ship. + expect(html).toContain(" { + const html = emitSceneHtml(lottieBase); + expect(html).toContain("setWasmUrl"); + expect(html).toContain("@lottiefiles/dotlottie-web"); + expect(html).toMatch(/dotlottie-web@0\.71\.\d+\/dist\/dotlottie-player\.wasm/); + }); + + it("scopes the overlay id per scene to avoid collisions between scenes", () => { + const a = emitSceneHtml({ ...lottieBase, id: "intro" }); + const b = emitSceneHtml({ ...lottieBase, id: "outro" }); + expect(a).toContain('id="lottie-overlay-intro"'); + expect(b).toContain('id="lottie-overlay-outro"'); + // No leftover unscoped id from the original implementation. + expect(a).not.toContain('id="lottie-overlay"'); + }); + + it("emits autoplay + loop by default and drops loop when loop:false", () => { + const looping = emitSceneHtml(lottieBase); + expect(looping).toMatch(/]*\sautoplay\s+loop\b/); + + const oneShot = emitSceneHtml({ + ...lottieBase, + lottie: { ...lottieBase.lottie, loop: false }, + }); + expect(oneShot).toMatch(/]*\sautoplay\b/); + expect(oneShot).not.toMatch(/]*\sloop\b/); + }); + + it("defaults non-full positions to scale 0.25 (matches html-clips.ts)", () => { + const html = emitSceneHtml({ + ...lottieBase, + lottie: { src: "assets/x.json", position: "bottom-right" }, + }); + // Default corner scale is 0.25 → 25% width/height (not 100% as the prior bug emitted). + expect(html).toContain("width:25%"); + expect(html).toContain("height:25%"); + }); + + it("full position ignores scale and fills the scene", () => { + const html = emitSceneHtml({ + ...lottieBase, + lottie: { src: "assets/x.json", position: "full", scale: 0.5 }, + }); + expect(html).toContain("inset:0"); + expect(html).toContain("width:100%"); + expect(html).toContain("height:100%"); + }); + + it("clamps scale to [0.01, 2] and opacity to [0, 1]", () => { + const overscale = emitSceneHtml({ + ...lottieBase, + lottie: { src: "assets/x.json", position: "top-left", scale: 5, opacity: 5 }, + }); + expect(overscale).toContain("width:200%"); + expect(overscale).toContain("opacity:1"); + + const underscale = emitSceneHtml({ + ...lottieBase, + lottie: { src: "assets/x.json", position: "top-left", scale: -1, opacity: -1 }, + }); + expect(underscale).toContain("width:1%"); + expect(underscale).toContain("opacity:0"); + }); + + it("escapes the lottie src to prevent HTML injection via the file path", () => { + const html = emitSceneHtml({ + ...lottieBase, + lottie: { src: 'evil">', position: "full" }, + }); + expect(html).not.toContain("`; + return { markup, script }; +} + /** * Emit the full per-scene HTML. Returns a complete `