[11e74cdf] Add HyperFrames composition for RoboCo v0.28.0 release video

This commit is contained in:
UX/UI Developer 1
2026-07-29 16:33:12 +00:00
parent 6a5fe6958b
commit 024428a511
7 changed files with 5767 additions and 0 deletions
+53
View File
@@ -262,3 +262,56 @@ Same schema as prior releases: one `captions.json` next to the HTML with self-ve
### Smoke-test invariants
`release-0.27.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with `data-duration="40"` and the HyperFrames params, the kit CSS/JS wiring is present, **four** feature cards each carry a progress-to-completed pill swap and include the exact titles ("WAF knows the real IP", "Telegram Mini App V6", "Codex and Gemini join", "Cost-tiered routing"), the stats overlay renders the three-line receipt (1 release / 4 providers / 0 bans), the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, no em dashes slip into on-screen copy or captions, and — the finding a prior revision cycle on this composition caught — the four card-beat gaps are asserted non-uniform (`new Set(gaps).size > 1`), regression-guarding against silently reusing `release-0.26.0`'s identical 5.0s/5.0s/5.0s spacing.
## Release-specific example: `release-0.28.0`
`compositions/release-0.28.0/` is a panel-demo kit clip for the RoboCo v0.28.0 release, mirroring the structure and pacing of `release-0.25.0` through `release-0.27.0`. It builds on the `kit/` register instead of the text-card style, so it has no `theme.css` of its own.
The composition runs **36 seconds** total — shorter than the prior three 40s cuts because this release names three shipped highlights, not four, and the tail (receipt, toast, outro) gets more breathing room rather than a fourth card padded in to match the usual count. The story is "the board finally works" (the release notes' own bolded phrase, and the hero tagline / panel intake text): the CEO types "The board finally works" into the panel intake at 3.6s, then three shipped feature cards enter the kanban column one per scene and flip from `in progress` to `completed`:
1. **Eval harness goes real** — enters at 5.0s, completes at 6.6s ("Real Docker spawns per turn now, no more mocks."). `python -m roboco.eval run` now drives a real `AgentOrchestrator.spawn_agent` per turn instead of the injectable scripted stub, with `_generate_mcp_config` guaranteeing spawned containers never reach production.
2. **12 programs, one board** — enters at 10.5s, completes at 12.1s ("Pest Control to Sentinel. The board finally ships."). The `BoardProgram` registry replaces the bespoke roadmap/spotlight loops with twelve named programs (Pest Control, Spackle, Scales, Dogfood, Periscope, Megaphone, Mirror, Barfly, War Room, Coroner, Librarian, Sentinel), every artifact still held for the CEO.
3. **Release posts stop parroting** — enters at 16.9s, completes at 18.5s ("Features lead the copy now, not security plumbing."). The X announcement drafter no longer quotes the changelog's first bold bullet verbatim; highlights reorder to marketing order and the fallback template can never quote a bullet.
Each card gets roughly five to six seconds of fully visible time before the next enters, with card-beat gaps varied (5.5s / 6.4s) per the craft bar's anti-metronome rule rather than a uniform interval. A cursor clicks the intake at 4.8s (submit), then witnesses each card completing without further clicks, the stats overlay shows "1 release / 12 programs / 0 copied bullets" (the third line calling back to the release-post fix) from 21.0s to 28.0s, the toast "v0.28.0 shipped / I approved once. 25 agents shipped it." runs from 28.5s, and the "roboco.tech" outro lands at 33.0s and holds through the end.
The composition reuses the same `pk-frame` chrome, `pk-column`/`pk-card`, `pk-pill`, `pk-cursor`, `pk-toast`, and `pk-outro` pieces from `kit/`, plus the typing reveal wired through `props.js`. Each feature card uses the `pk-pill--swap-out` / `pk-pill--swap-in` pattern to replace the `in progress` pill with `completed` on the same beat.
### Preview / test this composition
```bash
npx hyperframes preview compositions/release-0.28.0/vertical.html
npx hyperframes preview compositions/release-0.28.0/square.html
pnpm test # release-0.28.0.test.js is picked up by vitest
```
### `props.js` shape
```js
{
introText: string, // text that types into the panel intake field
toastTitle: string, // headline inside the shipping toast
toastBody: string, // sub-line inside the shipping toast
}
```
`window.__ORIENTATION__` is set for local preview only; the sidecar overwrites both globals at render time.
### `captions.json`
Same schema as prior releases: one `captions.json` next to the HTML with self-verified X and TikTok captions. The X caption totals **210 characters** (within the 280 limit); the TikTok caption **396 characters** (within the 2200 limit):
```json
{
"composition_id": "release-0.28.0",
"occasion": "release: RoboCo v0.28.0",
"platforms": {
"x": { "caption": "v0.28.0 is out.\nThe eval harness spawns real agents in real containers now. No more mocks.\nThe board finally works: 12 programs, from Pest Control to Sentinel.\nI approved once. 25 agents shipped it.\nroboco.tech", "char_count": 210, "limit": 280, "within_limit": true },
"tiktok": { "caption": "v0.28.0 is out.\n\nThe eval harness spawns real agents in real containers now. Docker required, no more mocks.\n\nThe board finally works. Twelve programs: Pest Control hunts bugs, Sentinel watches for drift, ten more in between.\n\nRelease announcements stopped copying the changelog word for word.\n\nI approved once. 25 agents shipped it.\n\nroboco.tech\n\n#buildinpublic #aiagents #devtools #indiehackers", "char_count": 396, "limit": 2200, "within_limit": true }
}
}
```
### Smoke-test invariants
`release-0.28.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with `data-duration="36"` and the HyperFrames params, the kit CSS/JS wiring is present, **three** feature cards each carry a progress-to-completed pill swap and include the exact titles ("Eval harness goes real", "12 programs, one board", "Release posts stop parroting"), the stats overlay renders the three-line receipt (1 release / 12 programs / 0 copied bullets), the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, no em dashes slip into on-screen copy or captions, and the three card-beat gaps are asserted non-uniform (`new Set(gaps).size > 1`).
@@ -0,0 +1,18 @@
{
"composition_id": "release-0.28.0",
"occasion": "release: RoboCo v0.28.0",
"platforms": {
"x": {
"caption": "v0.28.0 is out.\nThe eval harness spawns real agents in real containers now. No more mocks.\nThe board finally works: 12 programs, from Pest Control to Sentinel.\nI approved once. 25 agents shipped it.\nroboco.tech",
"char_count": 210,
"limit": 280,
"within_limit": true
},
"tiktok": {
"caption": "v0.28.0 is out.\n\nThe eval harness spawns real agents in real containers now. Docker required, no more mocks.\n\nThe board finally works. Twelve programs: Pest Control hunts bugs, Sentinel watches for drift, ten more in between.\n\nRelease announcements stopped copying the changelog word for word.\n\nI approved once. 25 agents shipped it.\n\nroboco.tech\n\n#buildinpublic #aiagents #devtools #indiehackers",
"char_count": 396,
"limit": 2200,
"within_limit": true
}
}
}
@@ -0,0 +1,9 @@
// Default sample props for local preview of the RoboCo v0.28.0 release clip.
// The video-renderer sidecar OVERWRITES this file at render time with the
// real per-request values + window.__ORIENTATION__.
window.__PROPS__ = {
introText: "The board finally works",
toastTitle: "v0.28.0 shipped",
toastBody: "I approved once. 25 agents shipped it.",
};
window.__ORIENTATION__ = "vertical";
@@ -0,0 +1,123 @@
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 RoboCo v0.28.0 release composition.
// Extends the panel-demo register: panel chrome, typing reveal, three feature
// cards with progress -> completed pill flips, cursor, stats overlay, toast,
// outro, offline constraint, and the captions.json schema/limits.
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("release-0.28.0 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(/—/);
});
it.each(orientations)("$file parses with correct dimensions and wiring", ({ file, height }) => {
const html = read(file);
expect(html).toContain('data-width="1080"');
expect(html).toContain(`data-height="${height}"`);
expect(html).toMatch(/data-duration="36"/);
expect(html).toMatch(/data-fps="30"/);
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);
expect(html).toContain('href="../../kit/kit.css"');
expect(html).toMatch(/class="[^"]*clip[^"]*"/);
expect(html).toContain("pk-frame");
expect(html).toContain("pk-frame__sidebar");
expect(html).toContain("pk-frame__topbar");
expect(html).toContain('id="typeIntro"');
expect(html).toContain("pk-caret");
expect(html).toContain("typeReveal");
const cardCount = (html.match(/class="pk-card"/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);
expect(html).toContain("v0.28.0");
expect(html).toContain("Eval harness goes real");
expect(html).toContain("12 programs, one board");
expect(html).toContain("Release posts stop parroting");
// Choreographed cursor: multi-waypoint path with at least one click
// beat (rings are generated at runtime by kit.js, not static HTML),
// and a camera that moves — a locked-off frame is an automatic bounce.
expect(html).toContain("pk-cursor");
const waypoints = html.match(/data-waypoints="([^"]+)"/);
expect(waypoints).not.toBeNull();
expect(waypoints[1].split(";").length).toBeGreaterThanOrEqual(6);
expect(waypoints[1]).toContain("click");
const shots = html.match(/data-shots="([^"]+)"/);
expect(shots).not.toBeNull();
expect(shots[1].split(";").length).toBeGreaterThanOrEqual(4);
expect(html).toContain("choreographAllCursors");
expect(html).toContain("choreographAllCameras");
expect(html).toContain("pk-toast");
expect(html).toContain('id="toastTitle"');
expect(html).toContain('id="toastBody"');
expect(html).toContain("pk-outro");
expect(html).toContain("roboco.tech");
expect(html).not.toMatch(/<script[^>]+src="https?:\/\//);
expect(html).not.toMatch(/href="https?:\/\//);
// on-screen copy only; the document <title> may carry an em dash as
// metadata (matches release-0.25.0's precedent) — it never renders.
const body = html.slice(html.indexOf("<body"));
expect(body).not.toMatch(/—/);
// Card-beat gaps must be genuinely varied, not a metronomic identical
// spacing (the craft bar's anti-metronome rule).
const cardDelays = [...html.matchAll(/class="pk-card" style="animation-delay: ([\d.]+)s;"/g)].map((m) =>
parseFloat(m[1]),
);
expect(cardDelays.length).toBe(3);
const gaps = cardDelays.slice(1).map((t, i) => Math.round((t - cardDelays[i]) * 100) / 100);
expect(new Set(gaps).size).toBeGreaterThan(1);
});
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-0.28.0");
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);
expect(captions.platforms.x.caption).not.toMatch(/—/);
expect(captions.platforms.tiktok.caption).not.toMatch(/—/);
});
});
@@ -0,0 +1,339 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.28.0" data-composition-duration="36" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.28.0 Release - square (1080×1080)</title>
<link rel="stylesheet" href="../../kit/kit.css" />
<script src="props.js"></script>
<script src="../../kit/kit.js"></script>
<style>
/* Square cut of the v0.28.0 release clip. Tighter spacing, smaller
hero, and the stats overlay covers the full frame for readability.
Total 36s, three feature cards. */
.r28-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
background: var(--pk-bg);
z-index: 30;
animation: r28-hero-out 0.6s var(--pk-ease) 2.6s forwards;
}
@keyframes r28-hero-out { to { opacity: 0; visibility: hidden; } }
.r28-hero__markwrap { position: relative; width: 400px; height: 400px; }
.r28-hero__mark {
width: 400px;
height: 400px;
transform: scale(0.82);
opacity: 0;
filter: drop-shadow(0 12px 60px rgba(255, 90, 31, 0.18));
animation: r28-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes r28-mark-in { to { transform: scale(1); opacity: 1; } }
.r28-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: r28-ring-in 0.3s linear 0.9s forwards, r28-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes r28-ring-in { to { opacity: 1; } }
@keyframes r28-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.r28-hero__version {
font-family: var(--pk-font-display);
font-size: 96px;
color: var(--pk-text);
opacity: 0;
transform: translateY(18px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.2s forwards;
}
.r28-hero__tag {
font-size: 26px;
color: var(--pk-text-muted);
opacity: 0;
transform: translateY(14px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.5s forwards;
}
.pk-frame { animation-delay: 2.6s; }
.pk-frame__sidebar {
width: 220px;
align-items: stretch;
gap: 6px;
padding: 24px 16px 0 16px;
transform: translateX(-40px);
opacity: 0;
animation: r28-side-in 0.7s var(--pk-ease) 2.8s forwards;
}
@keyframes r28-side-in { to { transform: translateX(0); opacity: 1; } }
.r28-side__logo {
width: 40px;
height: 40px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 18px 4px;
}
.r28-side__logo img { width: 30px; height: 30px; }
.r28-nav {
display: flex;
align-items: center;
height: 44px;
padding: 0 14px;
border-radius: 10px;
font-size: 18px;
color: var(--pk-text-muted);
}
.r28-nav--active { background: var(--pk-active-bg); color: var(--pk-active-fg); font-weight: 600; }
.pk-frame__topbar { left: 220px; height: 84px; padding: 0 32px; }
.pk-frame__content { left: 220px; top: 84px; }
.r28-user { font-size: 18px; }
.r28-user__chip { padding: 6px 14px; }
.pk-frame__search { width: 280px; height: 42px; font-size: 18px; }
.pk-intake { --pk-intake-top: 18px; }
.pk-intake__field { height: 60px; font-size: 28px; }
.pk-column { --pk-column-top: 130px; width: 560px; }
.pk-column .pk-card {
position: static;
margin-bottom: 12px;
padding: 18px 24px;
/* Springy entrance (see vertical.html); flat fades read as slides. */
animation-duration: 0.55s;
animation-timing-function: cubic-bezier(0.34, 1.4, 0.64, 1);
}
.pk-card__title { font-size: 28px; }
.r28-sub { font-size: 18px; }
.r28-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: r28-stats-in 0.4s var(--pk-ease) 21.0s forwards, r28-stats-out 0.5s var(--pk-ease) 28.0s forwards;
}
@keyframes r28-stats-in { to { opacity: 1; } }
.r28-stats__line {
font-size: 80px;
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;
}
.r28-stats__line--accent { color: var(--pk-accent); }
@keyframes r28-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 84px; width: 480px; }
.pk-outro { --pk-outro-bottom: 28px; }
.r28-outro__logo {
width: 48px;
height: 48px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.r28-outro__logo img { width: 36px; height: 36px; }
#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-0.28.0" data-width="1080" data-height="1080" data-start="0" data-duration="36" data-fps="30">
<div class="r28-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r28-hero__markwrap">
<img class="r28-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r28-hero__ring"></div>
</div>
<div class="r28-hero__version">v0.28.0</div>
<div class="r28-hero__tag">The board finally works</div>
</div>
<!-- Camera: tight open, push per card beat, dead-wide before the
full-screen stats overlay, gentle push for the toast, settle. -->
<div class="pk-camera"
data-shots="2.6 0 10 1.03; 4.8 0 6 1.035;
6.5 30 62 1.055; 9.6 26 56 1.05;
11.0 28 8 1.06; 15.1 24 4 1.055;
17.3 30 -46 1.06; 20.7 26 -50 1.055;
21.5 0 0 1.0; 27.5 0 0 1.0;
28.8 -20 -60 1.03; 31.0 -18 -58 1.028;
33.5 0 0 1.0; 35.6 0 4 1.004">
<div class="pk-frame clip" data-start="0" data-duration="36" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r28-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r28-nav">Overview</div>
<div class="r28-nav">Business</div>
<div class="r28-nav">Social</div>
<div class="r28-nav r28-nav--active">Tasks</div>
<div class="r28-nav">Kanban</div>
<div class="r28-nav">Git</div>
<div class="r28-nav">Agents</div>
<div class="r28-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r28-user">
<div class="pk-frame__status clip" data-start="0" data-duration="36" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="r28-user__chip">CEO · Renzo</span>
</div>
</header>
<div class="pk-frame__content">
<div class="pk-intake" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">The board finally works</span><span class="pk-caret"></span></div>
</div>
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.28.0</div>
<!-- Varied beat spacing (5.5s/6.4s gaps), not a metronomic
identical interval. -->
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">Eval harness goes real</div>
<div class="r28-sub">Real Docker spawns per turn now, no more mocks.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">eval harness</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card" style="animation-delay: 10.5s;">
<div class="pk-card__title">12 programs, one board</div>
<div class="r28-sub">Pest Control to Sentinel. The board finally ships.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">board programs</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 10.5s; --pk-pill-exit-delay: 11.9s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 12.1s;">completed</span>
</div>
</div>
<div class="pk-card" style="animation-delay: 16.9s;">
<div class="pk-card__title">Release posts stop parroting</div>
<div class="r28-sub">Features lead the copy now, not security plumbing.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">release posts</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 16.9s; --pk-pill-exit-delay: 18.3s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 18.5s;">completed</span>
</div>
</div>
</div>
<!-- Same hand as the vertical cut: submit click, witness each
card, exit right BEFORE the full-screen stats overlay lands
at 21.0s (the cursor sits under its backdrop). -->
<div class="pk-cursor"
data-waypoints="4.0 520 130; 4.5 452 80; 4.8 445 76 click;
5.7 250 185; 6.5 138 250; 7.9 152 262;
10.1 220 340; 11.0 148 412; 13.1 134 424;
16.4 215 505; 17.3 140 576; 19.0 155 588;
19.8 480 700; 20.6 920 780">
<div class="pk-cursor__glyph"></div>
</div>
<div class="r28-stats">
<div class="r28-stats__line" style="animation-delay: 21.0s;">1 release</div>
<div class="r28-stats__line r28-stats__line--accent" style="animation-delay: 21.3s;">12 programs</div>
<div class="r28-stats__line" style="animation-delay: 21.6s;">0 copied bullets</div>
</div>
<div class="pk-toast" style="animation-delay: 28.5s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.28.0 shipped</div>
<div class="pk-toast__text" id="toastBody">I approved once. 25 agents shipped it.</div>
</div>
</div>
<div class="pk-outro" style="animation-delay: 33.0s;">
<span class="r28-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["release-0.28.0"] = {
id: "release-0.28.0",
duration: 36,
fps: 30,
animations: []
};
(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);
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.0s forwards";
}
var typeEl = document.getElementById("typeIntro");
if (typeEl) {
if (typeof props.introText === "string" && props.introText) {
typeEl.textContent = props.introText;
}
typeReveal(typeEl);
}
window.PanelKit.choreographAllCursors();
window.PanelKit.choreographAllCameras();
})();
</script>
</body>
</html>
@@ -0,0 +1,369 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.28.0" data-composition-duration="36" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.28.0 Release - vertical (1080×1920)</title>
<link rel="stylesheet" href="../../kit/kit.css" />
<script src="props.js"></script>
<script src="../../kit/kit.js"></script>
<style>
/* v0.28.0 release clip - panel-demo register, mirroring the shape of
release-0.25.0 through 0.27.0: identity cold open, panel assembles,
three feature cards flip to completed one per scene, cursor click,
receipt stats, toast, lockup. Total 36s (three shipped highlights
this release instead of four, so the tail gets more breathing room
rather than padding a fourth card that isn't in the brief). */
/* ---- Identity cold open -------------------------------------------- */
.r28-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 38px;
background: var(--pk-bg);
z-index: 30;
animation: r28-hero-out 0.6s var(--pk-ease) 2.6s forwards;
}
@keyframes r28-hero-out { to { opacity: 0; visibility: hidden; } }
.r28-hero__markwrap { position: relative; width: 520px; height: 520px; }
.r28-hero__mark {
width: 520px;
height: 520px;
transform: scale(0.82);
opacity: 0;
filter: drop-shadow(0 12px 60px rgba(255, 90, 31, 0.18));
animation: r28-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes r28-mark-in { to { transform: scale(1); opacity: 1; } }
.r28-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: r28-ring-in 0.3s linear 0.9s forwards, r28-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes r28-ring-in { to { opacity: 1; } }
@keyframes r28-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.r28-hero__version {
font-family: var(--pk-font-display);
font-size: 120px;
color: var(--pk-text);
opacity: 0;
transform: translateY(18px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.2s forwards;
}
.r28-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 --------------------------------------------------- */
.pk-frame { animation-delay: 2.6s; }
.pk-frame__sidebar {
width: 250px;
align-items: stretch;
gap: 6px;
padding: 28px 18px 0 18px;
transform: translateX(-40px);
opacity: 0;
animation: r28-side-in 0.7s var(--pk-ease) 2.8s forwards;
}
@keyframes r28-side-in { to { transform: translateX(0); opacity: 1; } }
.r28-side__logo {
width: 44px;
height: 44px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 22px 4px;
}
.r28-side__logo img { width: 34px; height: 34px; }
.r28-nav {
display: flex;
align-items: center;
height: 52px;
padding: 0 16px;
border-radius: 10px;
font-size: 21px;
color: var(--pk-text-muted);
}
.r28-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; }
.r28-user {
display: flex;
align-items: center;
gap: 12px;
color: var(--pk-text-muted);
font-size: 20px;
}
.r28-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: 32px; }
.pk-column { --pk-column-top: 220px; width: 720px; }
.pk-column .pk-card {
position: static;
margin-bottom: 20px;
/* Springy entrance with a hint of overshoot; a flat fade on every
card is the metronome that makes the cut feel like a slideshow. */
animation-duration: 0.55s;
animation-timing-function: cubic-bezier(0.34, 1.4, 0.64, 1);
}
.r28-sub {
margin-top: 10px;
font-size: 22px;
line-height: 1.35;
color: var(--pk-text-muted);
}
/* Receipt stats overlay. */
.r28-stats {
position: absolute;
right: 64px;
bottom: 320px;
text-align: right;
font-family: var(--pk-font-display);
}
.r28-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;
}
.r28-stats__line--accent { color: var(--pk-accent); }
.r28-stats { animation: r28-stats-out 0.5s var(--pk-ease) 28.0s forwards; }
@keyframes r28-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 140px; }
/* Closing lockup. */
.pk-outro {
--pk-outro-bottom: 56px;
display: flex;
align-items: center;
gap: 20px;
}
.r28-outro__logo {
width: 52px;
height: 52px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.r28-outro__logo img { width: 40px; height: 40px; }
#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-0.28.0" data-width="1080" data-height="1920" data-start="0" data-duration="36" data-fps="30">
<!-- Beat 1: identity cold open. -->
<div class="r28-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r28-hero__markwrap">
<img class="r28-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r28-hero__ring"></div>
</div>
<div class="r28-hero__version">v0.28.0</div>
<div class="r28-hero__tag">The board finally works</div>
</div>
<!-- Beat 2: the panel assembles, and the camera lives on it: start a
touch tight, push toward each card as it completes, pull wide for
the receipt, ease onto the toast, settle for the lockup. -->
<div class="pk-camera"
data-shots="2.6 0 14 1.025; 4.8 0 8 1.03;
6.5 34 96 1.055; 9.6 30 88 1.05;
11.0 30 30 1.06; 15.1 26 24 1.055;
17.3 32 -52 1.065; 20.7 28 -58 1.06;
21.5 0 0 1.0; 27.5 0 0 1.0;
28.8 -26 -100 1.035; 31.0 -22 -96 1.03;
33.5 0 0 1.0; 35.6 0 6 1.005">
<div class="pk-frame clip" data-start="0" data-duration="36" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r28-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r28-nav">Overview</div>
<div class="r28-nav">Business</div>
<div class="r28-nav">Social</div>
<div class="r28-nav r28-nav--active">Tasks</div>
<div class="r28-nav">Kanban</div>
<div class="r28-nav">Git</div>
<div class="r28-nav">Agents</div>
<div class="r28-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r28-user">
<div class="pk-frame__status clip" data-start="0" data-duration="36" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="r28-user__chip">CEO · Renzo</span>
</div>
</header>
<div class="pk-frame__content">
<!-- Beat 3: the hook types into intake. -->
<div class="pk-intake" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">The board finally works</span><span class="pk-caret"></span></div>
</div>
<!-- Beat 4: three feature cards on varied beat spacing (5.5s/6.4s
gaps, not a metronomic identical interval), each fully visible
before the next enters. Only three cards this release since
the brief names three shipped highlights, not four. -->
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.28.0</div>
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">Eval harness goes real</div>
<div class="r28-sub">Real Docker spawns per turn now, no more mocks.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">eval harness</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card" style="animation-delay: 10.5s;">
<div class="pk-card__title">12 programs, one board</div>
<div class="r28-sub">Pest Control to Sentinel. The board finally ships.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">board programs</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 10.5s; --pk-pill-exit-delay: 11.9s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 12.1s;">completed</span>
</div>
</div>
<div class="pk-card" style="animation-delay: 16.9s;">
<div class="pk-card__title">Release posts stop parroting</div>
<div class="r28-sub">Features lead the copy now, not security plumbing.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">release posts</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 16.9s; --pk-pill-exit-delay: 18.3s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 18.5s;">completed</span>
</div>
</div>
</div>
<!-- The cursor is the CEO's hand: settle on the intake while it
types, ONE submit click just before the org takes over, then
travel to witness each card completing (the agents do the
clicking-free work), drift to the receipt, acknowledge the
toast, and leave the frame; never freeze, never blink out. -->
<div class="pk-cursor"
data-waypoints="4.0 640 210; 4.5 552 152; 4.8 545 148 click;
5.7 300 330; 6.5 160 405; 7.9 178 420;
10.1 260 520; 11.0 190 628; 13.1 172 642;
16.4 250 760; 17.3 162 848; 19.0 180 862;
20.5 420 1000; 21.3 560 1150; 27.0 585 1200;
28.0 615 1350; 28.3 612 1390 click;
30.0 700 1480; 32.0 940 1600">
<div class="pk-cursor__glyph"></div>
</div>
<!-- Beat 5: receipt stats. Third line calls back to the release-post
fix (no more copy-pasted changelog bullets). -->
<div class="r28-stats">
<div class="r28-stats__line" style="animation-delay: 21.0s;">1 release</div>
<div class="r28-stats__line r28-stats__line--accent" style="animation-delay: 21.3s;">12 programs</div>
<div class="r28-stats__line" style="animation-delay: 21.6s;">0 copied bullets</div>
</div>
<!-- Beat 6: confirmation toast, then lockup. -->
<div class="pk-toast" style="animation-delay: 28.5s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.28.0 shipped</div>
<div class="pk-toast__text" id="toastBody">I approved once. 25 agents shipped it.</div>
</div>
</div>
<div class="pk-outro" style="animation-delay: 33.0s;">
<span class="r28-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["release-0.28.0"] = {
id: "release-0.28.0",
duration: 36,
fps: 30,
animations: []
};
(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);
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.0s forwards";
}
var typeEl = document.getElementById("typeIntro");
if (typeEl) {
if (typeof props.introText === "string" && props.introText) {
typeEl.textContent = props.introText;
}
typeReveal(typeEl);
}
window.PanelKit.choreographAllCursors();
window.PanelKit.choreographAllCameras();
})();
</script>
</body>
</html>
+4856
View File
File diff suppressed because it is too large Load Diff