[e0bc418f] Video: v0.19.0 - our newest release (#333)

* [e0bc418f] add v019-release HyperFrames composition with vertical/square cuts, theme, props, and smoke tests

* [e0bc418f] fix(v019-release): remove local artifacts, replace em dashes, add captions.json and regression tests

* [e0bc418f] chore: delete .venv symlink and tighten .gitignore for symlink leakage

* [e0bc418f] chore(.gitignore): remove duplicate .pnpm-store line

* [e0bc418f] fix(v019-release): clean internal em dashes and add package-lock regression test

* [e0bc418f] chore(v019-release): remove committed vitest cache artifact from node_modules

* [e0bc418f] fix(.gitignore): restore .pnpm-store/ next to its GH001 comment and remove stray duplicate

* [e0bc418f] docs(motion): document v019-release composition and captions convention

* [e0bc418f] feat(release-recap): rebuild v0.19.0 video as panel-demo three-release recap

CEO rejected the text-card v019-release composition and rescoped this
to a recap of three releases (0.18.0-0.20.0) shipped in six days,
built in the panel-demo register per the CEO's brief. Brings
motion/kit/ and motion/compositions/panel-demo/ into this branch
(byte-identical to master, which merged them after this branch
diverged and cannot be rebased onto directly), deletes the obsolete
motion/compositions/v019-release/ text-card composition, and adds
motion/compositions/release-recap/: the panel frame, one intake typing
reveal, three release cards (v0.18.0/v0.19.0/v0.20.0) cycling through
a single shared kanban slot with their own progress-to-completed pill
flip, a cursor that glides in once and click-pulses at each flip, a
closing toast, and the roboco.tech outro, both at 1080x1920 and
1080x1080. Includes props.js, captions.json (X 136/280 chars, TikTok
365/2200 chars, no em dash), and a vitest smoke test (11/11 passing
across all three compositions in the package). Also refreshes
motion/README.md with the current design-bar/panel-demo-kit sections
and a release-recap example replacing the deleted v019-release one.

* [e0bc418f] fix(release-recap): polish pass — accumulate the three releases, fix facts, robust typewriter

Cards now stack and accumulate instead of cycling one absolute slot (the
three-release story never landed visually); duration tightened 20s to
15s; per-release copy corrected (0.18.0 fleet doctrine + design bar,
0.19.0 video engine + sandboxes, 0.20.0 root-owned coverage + the kit)
along with the same fixes in captions.json; one cursor with one glide
and one click landing at the pill edge instead of parking on pill text
for 17s; the per-char typing reveal (which partially froze) replaced by
a single width-steps mono typewriter sized to the supplied text.

* [e0bc418f] feat(release-recap): identity redesign — real logo, real-UI sidebar, explainers, stat beat

CEO review round 2: no logo, no similarity with the actual UI, explains
nothing. Rebuilt: branded cold open (the real logo on an app-icon tile
with a ring sweep, wordmark, tagline), a labeled sidebar mirroring the
real panel (nav labels + active state + search + Live dot + CEO chip),
a one-line explainer under each release title, a kinetic stat beat
(3 releases / 6 days / 1 human — full-frame interstitial on the square
cut where no side zone exists), and a logo lockup outro; 18s. The logo
PNG is vendored into motion/public (offline constraint, same pattern
as the fonts).

* [e0bc418f] fix(release-recap): darkmode logo hero — crisp 1024px mark replaces the pixelated 64px tile

The cold open now uses logos/roboco-logo-darkmode.png (vendored into
motion/public), built for dark backgrounds with the wordmark included:
full-size mark with a soft glow and the ring sweep, no white tile, no
separate typed wordmark. The small app-icon tiles in the sidebar and
outro keep the 64px icon where tiny sizes favor it.

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-09 18:48:46 +02:00
committed by GitHub
co-authored by UX/UI Developer 1 UX/UI Documenter UX/UI Developer 2 Renn F
parent a322dc8f9f
commit 6188cce58c
10 changed files with 883 additions and 3 deletions
+32 -2
View File
@@ -7,7 +7,7 @@ In-repo [HyperFrames](https://github.com/heygen-com/hyperframes) composition pac
Compositions live under `compositions/<composition_id>/`. Each composition is one directory containing the HTML/CSS/JS for that clip; one HTML file per orientation (the sidecar points `inputPath` at a single HTML file, so per-orientation branching belongs in its own file, not inside one file).
1. Create `compositions/<your_clip>/vertical.html` and `square.html` carrying the HyperFrames render params on `<html>`: `data-width`, `data-height`, `data-duration` (seconds), `data-fps`. Timed visible elements get `class="clip"` plus `data-start`, `data-duration`, and `data-track-index`.
2. Add a shared `theme.css` with `@font-face` declarations (fonts are vendored under `public/fonts/*.woff2` — never load from a CDN, the render is offline) and the color/type tokens for the clip.
2. Add a shared `theme.css` with `@font-face` declarations (fonts are vendored under `public/fonts/*.woff2` — never load from a CDN, the render is offline) and the color/type tokens for the clip. (Skip this if you're building in the panel-demo register below — `kit/kit.css` already owns the reset + fonts.)
3. Ship a `props.js` with default `window.__PROPS__` + `window.__ORIENTATION__` values for local preview. The sidecar OVERWRITES this file at render time with the real per-release values — your HTML loads it via `<script src="props.js"></script>` before any inline script that reads the globals.
4. Add `<your_clip>.test.js` (vitest) asserting the HTML-structure invariants — dimensions, props.js wiring, theme link, at least one `class="clip"` element, no CDN scripts. See `release-announcement.test.js` as a template.
5. `pnpm preview` (alias for `hyperframes preview <file>`) opens the local preview server; `pnpm lint` runs `hyperframes lint` over both orientations; `pnpm test` runs the smoke gate that CI runs.
@@ -40,4 +40,34 @@ This composition is the library's reference point — match its restraint, don't
`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.
`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.
## Release-specific example: `release-recap` (0.18.0 - 0.20.0)
`compositions/release-recap/` is a demo-register clip built on `kit/`, not the release-announcement text-card style — the CEO rejected an earlier text-card cut of this same occasion ("Build this in the panel-demo register... the video must show the product moving... Do not invent a new visual language"). It ships the same two orientations as every other composition — `vertical.html` (1080×1920) and `square.html` (1080×1080) — sharing `props.js` and the offline-render constraints, but no `theme.css` of its own since `kit/kit.css` owns the look.
The story is "three releases shipped in six days": a single intake types "3 releases in 6 days", then three release cards (v0.18.0, v0.19.0, v0.20.0) cycle through **the same kanban slot** — each card is absolutely positioned at the same spot inside the column and painted after the previous, so the later card's solid background fully covers the one before it, a beat swap that reuses `panel-demo`'s exact single-card geometry per orientation instead of stacking three cards' worth of height (which would collide with the toast in the square cut). Each beat gets its own status-pill flip (`in progress` -> `completed`) and its own cursor click at the same parked position (only the first click glides in; the other two are click-only, `x0==x1`/`y0==y1`), then one toast and the "roboco.tech" outro once all three land.
### Preview / test this composition
```bash
npx hyperframes preview compositions/release-recap/vertical.html
npx hyperframes preview compositions/release-recap/square.html
pnpm test # runs vitest on all *.test.js under motion/
```
### `captions.json`
Like every release composition, this one ships a tracked `captions.json` next to the HTML holding the X and TikTok captions the render pipeline proposes alongside the MP4, self-verifying character counts against each platform's limit:
```json
{
"composition_id": "release-recap",
"platforms": {
"x": { "caption": "...", "char_count": 136, "limit": 280, "within_limit": true },
"tiktok": { "caption": "...", "char_count": 365, "limit": 2200, "within_limit": true }
}
}
```
The smoke test (`release-recap.test.js`) asserts this schema, checks the counts, and regression-guards no em dashes in on-screen copy or captions (the design-bar violation QA caught on the prior text-card cut of this task).
@@ -0,0 +1,18 @@
{
"composition_id": "release-recap",
"occasion": "recap: three releases, 0.18.0 to 0.20.0",
"platforms": {
"x": {
"caption": "Three RoboCo releases in six days: 0.18.0 (Jul 4), 0.19.0 (Jul 8), 0.20.0 (Jul 9). An AI agent company that ships every day. roboco.tech",
"char_count": 136,
"limit": 280,
"within_limit": true
},
"tiktok": {
"caption": "RoboCo shipped three releases in six days. 0.18.0 (Jul 4) put a shared behavioral doctrine and a design bar on the whole fleet. 0.19.0 (Jul 8) shipped the video engine and per-agent dev sandboxes. 0.20.0 (Jul 9) landed root-owned acceptance criteria and the panel-demo kit this exact video is built with. Same AI agent company, six days, three real releases. roboco.tech",
"char_count": 370,
"limit": 2200,
"within_limit": true
}
}
}
@@ -0,0 +1,17 @@
// Default sample props for local preview (npx hyperframes preview). The
// video-renderer sidecar OVERWRITES this file at render time with whatever
// input_props were stamped via propose_video. Shared by both vertical.html
// and square.html - orientation is baked into which HTML file is rendered
// (see motion/README.md), so there is no runtime branch here.
//
// The three release cards (title/version/chip) are baked directly into the
// HTML, not templated here - this is a one-off recap of three releases that
// already shipped (0.18.0, 0.19.0, 0.20.0), not a reusable per-release
// template like release-announcement. Only the intro line and the closing
// toast are exposed as props.
window.__PROPS__ = {
introText: "3 releases in 6 days",
toastTitle: "3 releases shipped",
toastBody: "0.18.0 to 0.20.0 in six days.",
};
window.__ORIENTATION__ = "vertical";
@@ -0,0 +1,122 @@
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 release-recap composition - mirrors
// panel-demo.test.js (this clip extends the same pk-* kit). We parse the
// HTML as text and assert structural invariants: dimensions, props.js
// wiring, the kit stylesheet link, timed clips, the three-release beats
// (card + chip + pill flip each), the cursor, the toast/outro, the offline
// constraint, and the captions.json schema/limits/copy regressions the CEO's
// revision called out on the prior text-card composition.
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" },
];
const versions = ["v0.18.0", "v0.19.0", "v0.20.0"];
describe("release-recap 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("introText");
expect(src).toContain("toastTitle");
expect(src).toContain("toastBody");
expect(src).not.toMatch(/—/); // no em dash in on-screen copy
});
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="18"/);
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('<script src="props.js"></script>');
const kitJsIdx = html.indexOf('<script src="../../kit/kit.js"></script>');
const inlineIdx = html.indexOf("window.__timelines");
expect(propsIdx).toBeGreaterThan(-1);
expect(kitJsIdx).toBeGreaterThan(propsIdx);
expect(inlineIdx).toBeGreaterThan(kitJsIdx);
// Kit stylesheet linked (no theme.css of its own - this is the demo
// register, not the text-card register).
expect(html).toContain('href="../../kit/kit.css"');
// At least one timed clip element.
expect(html).toMatch(/class="[^"]*clip[^"]*"/);
// Panel frame chrome - "the video must show the product moving".
expect(html).toContain("pk-frame");
expect(html).toContain("pk-frame__sidebar");
expect(html).toContain("pk-frame__topbar");
// Typing reveal into intake.
expect(html).toContain('id="typeIntro"');
expect(html).toContain("pk-caret");
expect(html).toContain("typeReveal");
// Three release beats: a card + version chip + a progress -> completed
// pill flip each ("status pills flipping", plural).
const cardCount = (html.match(/pk-card clip/g) || []).length;
const progressCount = (html.match(/pk-pill--progress/g) || []).length;
const completedCount = (html.match(/pk-pill--completed/g) || []).length;
expect(cardCount).toBe(3);
expect(progressCount).toBe(3);
expect(completedCount).toBe(3);
for (const version of versions) {
expect(html).toContain(version);
}
// 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(/<script[^>]+src="https?:\/\//);
expect(html).not.toMatch(/href="https?:\/\//);
// No em dash in on-screen copy (design-bar regression from the prior
// text-card composition's QA failure on this same task).
expect(html).not.toMatch(/—/);
});
it("ships captions.json within X and TikTok platform limits", () => {
expect(existsSync(join(dir, "captions.json"))).toBe(true);
const captions = JSON.parse(read("captions.json"));
expect(captions.composition_id).toBe("release-recap");
expect(captions.platforms.x.char_count).toBe(captions.platforms.x.caption.length);
expect(captions.platforms.x.char_count).toBeLessThanOrEqual(captions.platforms.x.limit);
expect(captions.platforms.x.within_limit).toBe(true);
expect(captions.platforms.tiktok.char_count).toBe(captions.platforms.tiktok.caption.length);
expect(captions.platforms.tiktok.char_count).toBeLessThanOrEqual(captions.platforms.tiktok.limit);
expect(captions.platforms.tiktok.within_limit).toBe(true);
// No em dash in either caption.
expect(captions.platforms.x.caption).not.toMatch(/—/);
expect(captions.platforms.tiktok.caption).not.toMatch(/—/);
});
});
@@ -0,0 +1,347 @@
<!doctype html>
<html lang="en" data-composition-id="release-recap" data-composition-duration="18" data-fps="30">
<head>
<meta charset="utf-8" />
<title>Release Recap - square (1080×1080)</title>
<link rel="stylesheet" href="../../kit/kit.css" />
<script src="props.js"></script>
<script src="../../kit/kit.js"></script>
<style>
/* Composition-local layer on top of the kit: identity cold open (the
real logo tile + wordmark), a labeled sidebar that mirrors the actual
panel, per-release explainer sublines, a kinetic stat beat, and the
closing lockup. Kit components keep the card/pill/toast/cursor look. */
/* ---- Identity cold open -------------------------------------------- */
.rc-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 32px;
background: var(--pk-bg);
z-index: 30;
animation: rc-hero-out 0.6s var(--pk-ease) 3.2s forwards;
}
@keyframes rc-hero-out { to { opacity: 0; visibility: hidden; } }
.rc-hero__markwrap { position: relative; width: 440px; height: 440px; }
.rc-hero__mark {
width: 440px;
height: 440px;
transform: scale(0.82);
opacity: 0;
filter: drop-shadow(0 12px 60px rgba(120, 170, 210, 0.18));
animation: rc-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes rc-mark-in { to { transform: scale(1); opacity: 1; } }
/* One accent ring sweeps the tile once - the identity flourish. */
.rc-hero__ring {
position: absolute;
inset: 6px;
border-radius: 50%;
padding: 3px;
background: conic-gradient(from 0deg, transparent 0 78%, var(--pk-accent) 92%, transparent 100%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
animation: rc-ring-in 0.3s linear 0.9s forwards, rc-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes rc-ring-in { to { opacity: 1; } }
@keyframes rc-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.rc-hero__tag {
font-size: 30px;
color: var(--pk-text-muted);
opacity: 0;
transform: translateY(14px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.5s forwards;
}
/* ---- Panel chrome: labeled sidebar mirroring the real panel -------- */
.pk-frame { animation-delay: 3.2s; }
.pk-frame__sidebar {
width: 250px;
align-items: stretch;
gap: 6px;
padding: 28px 18px 0 18px;
transform: translateX(-40px);
opacity: 0;
animation: rc-side-in 0.7s var(--pk-ease) 3.4s forwards;
}
@keyframes rc-side-in { to { transform: translateX(0); opacity: 1; } }
.rc-side__logo {
width: 44px;
height: 44px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 22px 4px;
}
.rc-side__logo img { width: 34px; height: 34px; }
.rc-nav {
display: flex;
align-items: center;
height: 52px;
padding: 0 16px;
border-radius: 10px;
font-size: 21px;
color: var(--pk-text-muted);
}
.rc-nav--active { background: var(--pk-active-bg); color: var(--pk-active-fg); font-weight: 600; }
.pk-frame__topbar { left: 250px; }
.pk-frame__content { left: 250px; }
.rc-user {
display: flex;
align-items: center;
gap: 12px;
color: var(--pk-text-muted);
font-size: 20px;
}
.rc-user__chip {
background: var(--pk-surface-raised);
border: 1px solid var(--pk-border);
border-radius: 999px;
padding: 8px 18px;
color: var(--pk-text);
}
/* ---- Content placement --------------------------------------------- */
.pk-intake { --pk-intake-top: 20px; }
.pk-column { --pk-column-top: 168px; width: 660px; }
.pk-column .pk-card { position: static; margin-bottom: 12px; padding: 18px 26px; }
.rc-sub {
margin-top: 8px;
font-size: 18px;
line-height: 1.35;
color: var(--pk-text-muted);
}
/* Kinetic stat beat, right-anchored under the cards. */
.rc-stats {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
background: rgba(10, 10, 10, 0.88);
z-index: 20;
font-family: var(--pk-font-display);
opacity: 0;
animation: rc-stats-in 0.4s var(--pk-ease) 13s forwards, rc-stats-out 0.5s var(--pk-ease) 14.4s forwards;
}
@keyframes rc-stats-in { to { opacity: 1; } }
.rc-stats__line {
font-size: 84px;
line-height: 1.25;
color: var(--pk-text);
opacity: 0;
transform: translateY(24px) scale(0.96);
animation: pk-enter-rise-scale 0.55s var(--pk-ease) forwards;
}
.rc-stats__line--accent { color: var(--pk-accent); }
@keyframes rc-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 84px; width: 480px; }
/* Closing lockup: small tile + domain. */
.pk-outro {
--pk-outro-bottom: 28px;
display: flex;
align-items: center;
gap: 20px;
}
.rc-outro__logo {
width: 52px;
height: 52px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.rc-outro__logo img { width: 40px; height: 40px; }
/* Single-animation typewriter: the mono face makes ch-widths exact, so
a width reveal in steps(n) reads as typing and cannot partially
freeze the way per-char reveals can. */
.pk-intake__field { font-family: var(--pk-font-display); letter-spacing: 0.5px; }
#typeIntro { display: inline-block; overflow: hidden; white-space: pre; vertical-align: bottom; width: 0; }
@keyframes type-reveal { to { width: var(--type-ch, 20ch); } }
</style>
</head>
<body>
<div id="stage" data-composition-id="release-recap" data-width="1080" data-height="1080" data-start="0" data-duration="18" data-fps="30">
<!-- Beat 1: identity cold open. The tile settles, the accent ring sweeps
once, the wordmark and tag rise; the whole layer yields to the
panel at 3.2s. -->
<div class="rc-hero clip" data-start="0" data-duration="3.8" data-track-index="15">
<div class="rc-hero__markwrap">
<img class="rc-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="rc-hero__ring"></div>
</div>
<div class="rc-hero__tag">the AI company that ships itself</div>
</div>
<!-- Beat 2: the panel assembles - labeled sidebar like the real UI. -->
<div class="pk-frame clip" data-start="3.2" data-duration="14.8" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="rc-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="rc-nav">Overview</div>
<div class="rc-nav">Business</div>
<div class="rc-nav">Social</div>
<div class="rc-nav rc-nav--active">Tasks</div>
<div class="rc-nav">Kanban</div>
<div class="rc-nav">Git</div>
<div class="rc-nav">Agents</div>
<div class="rc-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="rc-user">
<div class="pk-frame__status clip" data-start="3.2" data-duration="14.8" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="rc-user__chip">CEO · Renzo</span>
</div>
</header>
<div class="pk-frame__content">
<!-- Beat 3: the recap line types into intake. -->
<div class="pk-intake clip" data-start="4.2" data-duration="13.8" data-track-index="2" style="animation-delay: 4.2s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">3 releases in 6 days</span><span class="pk-caret"></span></div>
</div>
<!-- Beat 4: three releases stack and accumulate, each with a
one-line explainer; each flips completed as the next ships. -->
<div class="pk-column">
<div class="pk-column__header">Shipped</div>
<div class="pk-card clip" data-start="6" data-duration="12" data-track-index="3" style="animation-delay: 6s;">
<div class="pk-card__title">v0.18.0 · Fleet doctrine</div>
<div class="rc-sub">One behavioral doctrine and one design bar across all 25 agents.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 4</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="6" data-duration="1.8" data-track-index="4" style="--pk-pill-enter-delay: 6s; --pk-pill-exit-delay: 7.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="7.8" data-duration="10.2" data-track-index="5" style="--pk-pill-enter-delay: 7.8s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="8.2" data-duration="9.8" data-track-index="6" style="animation-delay: 8.2s;">
<div class="pk-card__title">v0.19.0 · Video engine</div>
<div class="rc-sub">Releases become videos. Every agent gets its own dev sandbox.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 8</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.2" data-duration="1.8" data-track-index="7" style="--pk-pill-enter-delay: 8.2s; --pk-pill-exit-delay: 9.8s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10" data-duration="8" data-track-index="8" style="--pk-pill-enter-delay: 10s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="10.4" data-duration="7.6" data-track-index="9" style="animation-delay: 10.4s;">
<div class="pk-card__title">v0.20.0 · Root-owned coverage</div>
<div class="rc-sub">Acceptance criteria owned at the root. Plus the kit this video is built with.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 9</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.4" data-duration="2.4" data-track-index="10" style="--pk-pill-enter-delay: 10.4s; --pk-pill-exit-delay: 12.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="12.6" data-duration="5.4" data-track-index="11" style="--pk-pill-enter-delay: 12.6s;">completed</span>
</div>
</div>
</div>
<!-- One cursor, one glide, one click at the newest pill's edge. -->
<div class="pk-cursor clip" data-start="11" data-duration="7" data-track-index="12" style="--pk-cursor-delay: 11s; --pk-cursor-x0: 700px; --pk-cursor-y0: 340px; --pk-cursor-x1: 246px; --pk-cursor-y1: 920px;">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 12.4s;"></div>
</div>
<!-- Beat 5: the kinetic stat lines. -->
<div class="rc-stats clip" data-start="13" data-duration="5" data-track-index="16">
<div class="rc-stats__line" style="animation-delay: 13s;">3 releases</div>
<div class="rc-stats__line" style="animation-delay: 13.3s;">6 days</div>
<div class="rc-stats__line rc-stats__line--accent" style="animation-delay: 13.6s;">1 human</div>
</div>
<!-- Beat 6: confirmation toast, then the lockup outro. -->
<div class="pk-toast clip" data-start="14.8" data-duration="3.2" data-track-index="13" style="animation-delay: 14.8s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">3 releases shipped</div>
<div class="pk-toast__text" id="toastBody">0.18.0 to 0.20.0 in six days.</div>
</div>
</div>
<div class="pk-outro clip" data-start="15.4" data-duration="2.6" data-track-index="14" style="animation-delay: 15.4s;">
<span class="rc-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
<script>
// Register the composition timeline metadata. Motion is driven by CSS
// keyframes (no GSAP - the render is offline, no CDN), but HyperFrames
// requires the registry entry to consider the composition valid.
window.__timelines = window.__timelines || {};
window.__timelines["release-recap"] = {
id: "release-recap",
duration: 18,
fps: 30,
animations: []
};
// Read sidecar-supplied props (props.js is overwritten at render time)
// and populate the DOM. Defensive: a missing prop leaves the
// placeholder text already in the markup intact rather than throwing.
(function () {
var props = (window.__PROPS__ && typeof window.__PROPS__ === "object") ? window.__PROPS__ : {};
function setText(id, value) {
if (typeof value !== "string" || !value) return;
var el = document.getElementById(id);
if (el) el.textContent = value;
}
setText("toastTitle", props.toastTitle);
setText("toastBody", props.toastBody);
// typeReveal: width-steps typewriter sized to the actual text so a
// sidecar-supplied introText of any length types out fully.
function typeReveal(el) {
var n = (el.textContent || "").length || 1;
el.style.setProperty("--type-ch", (n + 1) + "ch");
el.style.animation = "type-reveal " + (n * 0.07).toFixed(2) + "s steps(" + n + ", end) 4.6s forwards";
}
var typeEl = document.getElementById("typeIntro");
if (typeEl) {
if (typeof props.introText === "string" && props.introText) {
typeEl.textContent = props.introText;
}
typeReveal(typeEl);
}
})();
</script>
</body>
</html>
@@ -0,0 +1,340 @@
<!doctype html>
<html lang="en" data-composition-id="release-recap" data-composition-duration="18" data-fps="30">
<head>
<meta charset="utf-8" />
<title>Release Recap - vertical (1080×1920)</title>
<link rel="stylesheet" href="../../kit/kit.css" />
<script src="props.js"></script>
<script src="../../kit/kit.js"></script>
<style>
/* Composition-local layer on top of the kit: identity cold open (the
real logo tile + wordmark), a labeled sidebar that mirrors the actual
panel, per-release explainer sublines, a kinetic stat beat, and the
closing lockup. Kit components keep the card/pill/toast/cursor look. */
/* ---- Identity cold open -------------------------------------------- */
.rc-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 44px;
background: var(--pk-bg);
z-index: 30;
animation: rc-hero-out 0.6s var(--pk-ease) 3.2s forwards;
}
@keyframes rc-hero-out { to { opacity: 0; visibility: hidden; } }
.rc-hero__markwrap { position: relative; width: 560px; height: 560px; }
.rc-hero__mark {
width: 560px;
height: 560px;
transform: scale(0.82);
opacity: 0;
filter: drop-shadow(0 12px 60px rgba(120, 170, 210, 0.18));
animation: rc-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes rc-mark-in { to { transform: scale(1); opacity: 1; } }
/* One accent ring sweeps the tile once - the identity flourish. */
.rc-hero__ring {
position: absolute;
inset: 6px;
border-radius: 50%;
padding: 3px;
background: conic-gradient(from 0deg, transparent 0 78%, var(--pk-accent) 92%, transparent 100%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
animation: rc-ring-in 0.3s linear 0.9s forwards, rc-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes rc-ring-in { to { opacity: 1; } }
@keyframes rc-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.rc-hero__tag {
font-size: 30px;
color: var(--pk-text-muted);
opacity: 0;
transform: translateY(14px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.5s forwards;
}
/* ---- Panel chrome: labeled sidebar mirroring the real panel -------- */
.pk-frame { animation-delay: 3.2s; }
.pk-frame__sidebar {
width: 250px;
align-items: stretch;
gap: 6px;
padding: 28px 18px 0 18px;
transform: translateX(-40px);
opacity: 0;
animation: rc-side-in 0.7s var(--pk-ease) 3.4s forwards;
}
@keyframes rc-side-in { to { transform: translateX(0); opacity: 1; } }
.rc-side__logo {
width: 44px;
height: 44px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 22px 4px;
}
.rc-side__logo img { width: 34px; height: 34px; }
.rc-nav {
display: flex;
align-items: center;
height: 52px;
padding: 0 16px;
border-radius: 10px;
font-size: 21px;
color: var(--pk-text-muted);
}
.rc-nav--active { background: var(--pk-active-bg); color: var(--pk-active-fg); font-weight: 600; }
.pk-frame__topbar { left: 250px; }
.pk-frame__content { left: 250px; }
.rc-user {
display: flex;
align-items: center;
gap: 12px;
color: var(--pk-text-muted);
font-size: 20px;
}
.rc-user__chip {
background: var(--pk-surface-raised);
border: 1px solid var(--pk-border);
border-radius: 999px;
padding: 8px 18px;
color: var(--pk-text);
}
/* ---- Content placement --------------------------------------------- */
.pk-intake { --pk-intake-top: 36px; }
.pk-column { --pk-column-top: 280px; width: 690px; }
.pk-column .pk-card { position: static; margin-bottom: 26px; }
.rc-sub {
margin-top: 12px;
font-size: 21px;
line-height: 1.35;
color: var(--pk-text-muted);
}
/* Kinetic stat beat, right-anchored under the cards. */
.rc-stats {
position: absolute;
right: 64px;
bottom: 330px;
text-align: right;
font-family: var(--pk-font-display);
}
.rc-stats__line {
font-size: 64px;
line-height: 1.22;
color: var(--pk-text);
opacity: 0;
transform: translateY(24px) scale(0.96);
animation: pk-enter-rise-scale 0.55s var(--pk-ease) forwards;
}
.rc-stats__line--accent { color: var(--pk-accent); }
/* The stat block yields before the toast/outro close. */
.rc-stats { animation: rc-stats-out 0.5s var(--pk-ease) 15.2s forwards; }
@keyframes rc-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 150px; }
/* Closing lockup: small tile + domain. */
.pk-outro {
--pk-outro-bottom: 64px;
display: flex;
align-items: center;
gap: 20px;
}
.rc-outro__logo {
width: 52px;
height: 52px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.rc-outro__logo img { width: 40px; height: 40px; }
/* Single-animation typewriter: the mono face makes ch-widths exact, so
a width reveal in steps(n) reads as typing and cannot partially
freeze the way per-char reveals can. */
.pk-intake__field { font-family: var(--pk-font-display); letter-spacing: 0.5px; }
#typeIntro { display: inline-block; overflow: hidden; white-space: pre; vertical-align: bottom; width: 0; }
@keyframes type-reveal { to { width: var(--type-ch, 20ch); } }
</style>
</head>
<body>
<div id="stage" data-composition-id="release-recap" data-width="1080" data-height="1920" data-start="0" data-duration="18" data-fps="30">
<!-- Beat 1: identity cold open. The tile settles, the accent ring sweeps
once, the wordmark and tag rise; the whole layer yields to the
panel at 3.2s. -->
<div class="rc-hero clip" data-start="0" data-duration="3.8" data-track-index="15">
<div class="rc-hero__markwrap">
<img class="rc-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="rc-hero__ring"></div>
</div>
<div class="rc-hero__tag">the AI company that ships itself</div>
</div>
<!-- Beat 2: the panel assembles - labeled sidebar like the real UI. -->
<div class="pk-frame clip" data-start="3.2" data-duration="14.8" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="rc-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="rc-nav">Overview</div>
<div class="rc-nav">Business</div>
<div class="rc-nav">Social</div>
<div class="rc-nav rc-nav--active">Tasks</div>
<div class="rc-nav">Kanban</div>
<div class="rc-nav">Git</div>
<div class="rc-nav">Agents</div>
<div class="rc-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="rc-user">
<div class="pk-frame__status clip" data-start="3.2" data-duration="14.8" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="rc-user__chip">CEO · Renzo</span>
</div>
</header>
<div class="pk-frame__content">
<!-- Beat 3: the recap line types into intake. -->
<div class="pk-intake clip" data-start="4.2" data-duration="13.8" data-track-index="2" style="animation-delay: 4.2s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">3 releases in 6 days</span><span class="pk-caret"></span></div>
</div>
<!-- Beat 4: three releases stack and accumulate, each with a
one-line explainer; each flips completed as the next ships. -->
<div class="pk-column">
<div class="pk-column__header">Shipped</div>
<div class="pk-card clip" data-start="6" data-duration="12" data-track-index="3" style="animation-delay: 6s;">
<div class="pk-card__title">v0.18.0 · Fleet doctrine</div>
<div class="rc-sub">One behavioral doctrine and one design bar across all 25 agents.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 4</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="6" data-duration="1.8" data-track-index="4" style="--pk-pill-enter-delay: 6s; --pk-pill-exit-delay: 7.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="7.8" data-duration="10.2" data-track-index="5" style="--pk-pill-enter-delay: 7.8s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="8.2" data-duration="9.8" data-track-index="6" style="animation-delay: 8.2s;">
<div class="pk-card__title">v0.19.0 · Video engine</div>
<div class="rc-sub">Releases become videos. Every agent gets its own dev sandbox.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 8</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.2" data-duration="1.8" data-track-index="7" style="--pk-pill-enter-delay: 8.2s; --pk-pill-exit-delay: 9.8s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10" data-duration="8" data-track-index="8" style="--pk-pill-enter-delay: 10s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="10.4" data-duration="7.6" data-track-index="9" style="animation-delay: 10.4s;">
<div class="pk-card__title">v0.20.0 · Root-owned coverage</div>
<div class="rc-sub">Acceptance criteria owned at the root. Plus the kit this video is built with.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">Jul 9</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.4" data-duration="2.4" data-track-index="10" style="--pk-pill-enter-delay: 10.4s; --pk-pill-exit-delay: 12.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="12.6" data-duration="5.4" data-track-index="11" style="--pk-pill-enter-delay: 12.6s;">completed</span>
</div>
</div>
</div>
<!-- One cursor, one glide, one click at the newest pill's edge. -->
<div class="pk-cursor clip" data-start="11" data-duration="7" data-track-index="12" style="--pk-cursor-delay: 11s; --pk-cursor-x0: 700px; --pk-cursor-y0: 500px; --pk-cursor-x1: 240px; --pk-cursor-y1: 1210px;">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 12.4s;"></div>
</div>
<!-- Beat 5: the kinetic stat lines. -->
<div class="rc-stats clip" data-start="13" data-duration="5" data-track-index="16">
<div class="rc-stats__line" style="animation-delay: 13s;">3 releases</div>
<div class="rc-stats__line" style="animation-delay: 13.3s;">6 days</div>
<div class="rc-stats__line rc-stats__line--accent" style="animation-delay: 13.6s;">1 human</div>
</div>
<!-- Beat 6: confirmation toast, then the lockup outro. -->
<div class="pk-toast clip" data-start="14.4" data-duration="3.6" data-track-index="13" style="animation-delay: 14.4s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">3 releases shipped</div>
<div class="pk-toast__text" id="toastBody">0.18.0 to 0.20.0 in six days.</div>
</div>
</div>
<div class="pk-outro clip" data-start="15.4" data-duration="2.6" data-track-index="14" style="animation-delay: 15.4s;">
<span class="rc-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
<script>
// Register the composition timeline metadata. Motion is driven by CSS
// keyframes (no GSAP - the render is offline, no CDN), but HyperFrames
// requires the registry entry to consider the composition valid.
window.__timelines = window.__timelines || {};
window.__timelines["release-recap"] = {
id: "release-recap",
duration: 18,
fps: 30,
animations: []
};
// Read sidecar-supplied props (props.js is overwritten at render time)
// and populate the DOM. Defensive: a missing prop leaves the
// placeholder text already in the markup intact rather than throwing.
(function () {
var props = (window.__PROPS__ && typeof window.__PROPS__ === "object") ? window.__PROPS__ : {};
function setText(id, value) {
if (typeof value !== "string" || !value) return;
var el = document.getElementById(id);
if (el) el.textContent = value;
}
setText("toastTitle", props.toastTitle);
setText("toastBody", props.toastBody);
// typeReveal: width-steps typewriter sized to the actual text so a
// sidecar-supplied introText of any length types out fully.
function typeReveal(el) {
var n = (el.textContent || "").length || 1;
el.style.setProperty("--type-ch", (n + 1) + "ch");
el.style.animation = "type-reveal " + (n * 0.07).toFixed(2) + "s steps(" + n + ", end) 4.6s forwards";
}
var typeEl = document.getElementById("typeIntro");
if (typeEl) {
if (typeof props.introText === "string" && props.introText) {
typeEl.textContent = props.introText;
}
typeReveal(typeEl);
}
})();
</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
allowBuilds:
'@google/genai': set this to true or false
esbuild: set this to true or false
onnxruntime-node: set this to true or false
protobufjs: set this to true or false
sharp: set this to true or false
Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB