diff --git a/motion/README.md b/motion/README.md index bcae7496..d823366b 100644 --- a/motion/README.md +++ b/motion/README.md @@ -34,4 +34,23 @@ This composition is the library's reference point — match its restraint, don't - **Type** — **Share Tech Mono** (a single static weight, 400 Regular — no bold/italic exists for this family) for the one big headline moment, paired with **Inter** as the clean workhorse body face for everything else. Both are vendored under `public/fonts/*.woff2` and loaded via `@font-face` in `theme.css` rather than fetched from a CDN — rendering never depends on network access or on whatever fonts happen to be installed on the render host. At least two weights of the body face, so hierarchy comes from more than just size. - **Motion** — entrances are CSS keyframe animations with a restrained easing (`cubic-bezier(0.22, 1, 0.36, 1)` — smooth settle, no overshoot, approximating Remotion's `spring({damping:17, mass:0.7, stiffness:140})` without the bounce), staggered across elements via `animation-delay` rather than all firing on frame 0. Keep one continuous ambient motion (here: the scanning accent line) so the frame is never fully static once entrances land. Every animation should earn its place — hierarchy, or a beat of pacing, not motion for its own sake. - **Layout** — anchor content asymmetrically (this clip sits in the lower two-thirds, left-aligned); avoid a perfectly centered card, which reads as a generic template rather than a designed frame. -- **AI tells to avoid** — no default AI-purple gradient wash, no centered-everything, no emoji as design elements, no one-font-one-size, no em dash in on-screen copy (voiceover script, highlight bullets, kicker text) or filler verbs ("Elevate", "Seamless", "Unleash", "Next-Gen"). \ No newline at end of file +- **AI tells to avoid** — no default AI-purple gradient wash, no centered-everything, no emoji as design elements, no one-font-one-size, no em dash in on-screen copy (voiceover script, highlight bullets, kicker text) or filler verbs ("Elevate", "Seamless", "Unleash", "Next-Gen"). + +## Panel-demo kit (`kit/`) + +`kit/` is a second register alongside the release-announcement's text-card +style: reusable `pk-`-namespaced CSS/HTML that recreates the control panel's +look (dark chrome, task cards, status pills, toasts, a typing reveal, a +cursor) so a composition can simulate the product actually being used, +instead of announcing it over a headline. Use the **text-card register** +(release-announcement's pattern) for version/feature announcements with no +product visuals; use the **demo register** (`kit/`) whenever the story is +"watch this happen in the app" — a task moving through the panel, a feature +being triggered, an agent doing something visible. + +`compositions/panel-demo/` is the reference composition: a task title types +into an intake field, a card materializes in a column, a cursor clicks it +done, a toast confirms, out on "roboco.tech". Start a new demo-register +composition from its structure and `kit.css`'s classes rather than +reinventing the panel's chrome per clip. See `kit/README.md` for the full +piece-by-piece reference. \ No newline at end of file diff --git a/motion/compositions/panel-demo/panel-demo.test.js b/motion/compositions/panel-demo/panel-demo.test.js new file mode 100644 index 00000000..b11516ed --- /dev/null +++ b/motion/compositions/panel-demo/panel-demo.test.js @@ -0,0 +1,88 @@ +import { readFileSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +// HTML-structure smoke test for the panel-demo composition — mirrors +// release-announcement.test.js. We parse the HTML as text and assert +// structural invariants: dimensions, props.js wiring, the kit stylesheet +// link, timed clips, the kit's pill/toast/typing/cursor pieces actually +// being used, and the offline-render constraint (no CDN, no network URLs). + +const here = dirname(fileURLToPath(import.meta.url)); +const dir = here; +const read = (name) => readFileSync(join(dir, name), "utf8"); + +const orientations = [ + { file: "vertical.html", height: "1920" }, + { file: "square.html", height: "1080" }, +]; + +describe("panel-demo composition", () => { + it("ships props.js (default preview values, sidecar overwrites at render)", () => { + expect(existsSync(join(dir, "props.js"))).toBe(true); + const src = read("props.js"); + expect(src).toContain("window.__PROPS__"); + expect(src).toContain("window.__ORIENTATION__"); + expect(src).toContain("taskTitle"); + expect(src).toContain("toastTitle"); + expect(src).toContain("toastBody"); + }); + + it.each(orientations)("$file parses with correct dimensions and wiring", ({ file, height }) => { + const html = read(file); + + // HyperFrames render params. + expect(html).toContain('data-width="1080"'); + expect(html).toContain(`data-height="${height}"`); + expect(html).toMatch(/data-duration="12"/); + expect(html).toMatch(/data-fps="30"/); + + // props.js loaded before the kit helper and before any inline script + // reads window.__PROPS__ / window.PanelKit. + const propsIdx = html.indexOf(''); + const kitJsIdx = html.indexOf(''); + const inlineIdx = html.indexOf("window.__timelines"); + expect(propsIdx).toBeGreaterThan(-1); + expect(kitJsIdx).toBeGreaterThan(propsIdx); + expect(inlineIdx).toBeGreaterThan(kitJsIdx); + + // Kit stylesheet linked (this composition has no theme.css of its own). + expect(html).toContain('href="../../kit/kit.css"'); + + // At least one timed clip element. + expect(html).toMatch(/class="[^"]*clip[^"]*"/); + + // Panel frame chrome. + expect(html).toContain("pk-frame"); + expect(html).toContain("pk-frame__sidebar"); + expect(html).toContain("pk-frame__topbar"); + + // Typing reveal: container + caret present, wired to the kit helper. + expect(html).toContain('id="typeTitle"'); + expect(html).toContain("pk-caret"); + expect(html).toContain("PanelKit.typeText"); + + // Task card + the progress -> completed pill swap. + expect(html).toContain("pk-card"); + expect(html).toContain("pk-pill--progress"); + expect(html).toContain("pk-pill--completed"); + + // Cursor glide + click. + expect(html).toContain("pk-cursor"); + expect(html).toContain("pk-cursor__ring"); + + // Toast. + expect(html).toContain("pk-toast"); + expect(html).toContain('id="toastTitle"'); + expect(html).toContain('id="toastBody"'); + + // Outro headline moment. + expect(html).toContain("pk-outro"); + expect(html).toContain("roboco.tech"); + + // No CDN / network fetch of any kind — the render is offline. + expect(html).not.toMatch(/ + + + +
+