[c71959c3] Video: release 0.27.0 (#698)

* [c71959c3] feat(motion): add release-0.27.0 panel-demo clip

Extends the panel-demo kit register (kit/) with a v0.27.0 release
composition: identity cold open, four feature cards (WAF real-IP
resolution, Telegram Mini App V6, Codex/Gemini providers, cost-tiered
routing) flipping from in-progress to completed, a camera that pushes
per beat via pk-camera data-shots, a cursor that clicks and travels
via pk-cursor data-waypoints, a receipt stats overlay, a shipped
toast, and the roboco.tech outro. Ships vertical.html, square.html,
props.js, captions.json, and a vitest smoke test mirroring
release-0.26.0's invariants.

* [c71959c3] fix(motion): implement genuinely non-uniform card-beat gaps in release-0.27.0 clip

The four .pk-card animation-delay values were uniform 5.0s/10.0s/15.0s/20.0s
in both vertical.html and square.html -- byte-for-byte identical to
release-0.26.0's metronomic spacing the craft bar bans -- despite the prior
decision_log claiming 5.5s/6.4s/5.3s varied gaps had been added. Card delays
are now 5.0/10.5/16.9/22.2 (real 5.5s/6.4s/5.3s gaps). Camera data-shots,
cursor data-waypoints, and the stats/toast/outro tail in both files are
re-warped with a shared piecewise-linear time function anchored at each
card's new beat, so nothing desyncs and the tail still fits inside the
fixed 40s runtime. Added a regression test asserting the four card gaps
are not all identical.

* [c71959c3] docs(motion): add release-0.27.0 composition section to README

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
This commit is contained in:
roboco-app[bot]
2026-07-25 03:39:07 +00:00
committed by GitHub
co-authored by UX/UI Developer 1 UX/UI Documenter
parent 069bb621f4
commit cbddfc7cb3
6 changed files with 938 additions and 0 deletions
+54
View File
@@ -208,3 +208,57 @@ Same schema as prior releases: one `captions.json` next to the HTML with self-ve
### Smoke-test invariants
`release-0.26.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 ("Off the public net", "3 forges, 1 API", "Telegram cockpit", "Guard mode: active"), the stats overlay renders the three-line receipt (1 release / 3 forges / 0 leaks), the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, and no em dashes slip into on-screen copy or captions.
## Release-specific example: `release-0.27.0`
`compositions/release-0.27.0/` is a panel-demo kit clip for the RoboCo v0.27.0 release, mirroring the structure and pacing of `release-0.25.0`/`release-0.26.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 **40 seconds** total. The story is "four brains, zero bans" (the hero tagline and the panel intake text): the CEO types "Four brains, zero bans" into the panel intake at 3.6s, then four shipped feature cards enter the kanban column one per scene and flip from `in progress` to `completed`:
1. **WAF knows the real IP** — enters at 5.0s, completes at 6.6s ("Tailscale hops don't get banned anymore."). The WAF now resolves the real client behind host-proxy hops via `ClientIpResolutionMiddleware`, instead of banning the operator's own Tailscale-tunneled device.
2. **Telegram Mini App V6** — enters at 10.5s, completes at 12.1s ("Real chat scopes, wallet-style metrics."). The Mini App gets a native-type premium overhaul: borderless cards, a wallet-style metrics tab, honest Chat scopes, and a live Board decide sheet.
3. **Codex and Gemini join** — enters at 16.9s, completes at 18.5s ("Four model providers, one gateway now."). `ModelProvider.OPENAI` (Codex CLI) and `ModelProvider.GEMINI` (Gemini CLI) join Anthropic and Grok on the agent-provider roster.
4. **Cost-tiered routing** — enters at 22.2s, completes at 23.8s ("Save a routing mix, restore it anytime."). Cost-tiered model routing plus saved routing presets let an operator snapshot and restore a full routing configuration.
Unlike `release-0.25.0`/`release-0.26.0`'s uniform 5.0s card-beat gaps, this composition deliberately varies the spacing (5.5s/6.4s/5.3s between successive card entrances — delays 5.0/10.5/16.9/22.2s) per the craft bar's anti-metronome rule in "Beat density and pacing variation" above; the camera shots, cursor waypoints, and the stats/toast/outro tail are all re-warped with a shared piecewise-linear time function so nothing desyncs against the shifted beats. A cursor clicks the intake at 4.8s (submit), then witnesses each card completing without further clicks, the stats overlay shows "1 release / 4 providers / 0 bans" from 25.7s to 33.0s, the toast "v0.27.0 shipped / I approved once. 25 agents shipped it." runs from 31.0s, and the "roboco.tech" outro lands at 36.3s 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.27.0/vertical.html
npx hyperframes preview compositions/release-0.27.0/square.html
pnpm test # release-0.27.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 **234 characters** (within the 280 limit); the TikTok caption **499 characters** (within the 2200 limit):
```json
{
"composition_id": "release-0.27.0",
"occasion": "release: RoboCo v0.27.0",
"platforms": {
"x": { "caption": "v0.27.0 is out.\nThe WAF used to ban my own phone behind Tailscale. Now it knows the real client.\nTelegram Mini App V6, Codex and Gemini joined the roster, cost-tiered routing shipped.\nI approved once. 25 agents shipped it.\nroboco.tech", "char_count": 234, "limit": 280, "within_limit": true },
"tiktok": { "caption": "v0.27.0 is out.\n\nThe WAF used to ban my own phone behind a Tailscale hop. Now it resolves the real client. Zero false bans.\n\nTelegram Mini App V6 shipped: real chat scopes, wallet-style metrics, a live Board sheet.\n\nCodex and Gemini just joined Claude and Grok on the model roster. Four providers, one gateway.\n\nCost-tiered routing plus saved presets: snapshot a routing mix, restore it whenever.\n\nI approved once. 25 agents shipped it.\n\nroboco.tech\n\n#buildinpublic #aiagents #devtools #indiehackers", "char_count": 499, "limit": 2200, "within_limit": true }
}
}
```
### 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.
@@ -0,0 +1,18 @@
{
"composition_id": "release-0.27.0",
"occasion": "release: RoboCo v0.27.0",
"platforms": {
"x": {
"caption": "v0.27.0 is out.\nThe WAF used to ban my own phone behind Tailscale. Now it knows the real client.\nTelegram Mini App V6, Codex and Gemini joined the roster, cost-tiered routing shipped.\nI approved once. 25 agents shipped it.\nroboco.tech",
"char_count": 234,
"limit": 280,
"within_limit": true
},
"tiktok": {
"caption": "v0.27.0 is out.\n\nThe WAF used to ban my own phone behind a Tailscale hop. Now it resolves the real client. Zero false bans.\n\nTelegram Mini App V6 shipped: real chat scopes, wallet-style metrics, a live Board sheet.\n\nCodex and Gemini just joined Claude and Grok on the model roster. Four providers, one gateway.\n\nCost-tiered routing plus saved presets: snapshot a routing mix, restore it whenever.\n\nI approved once. 25 agents shipped it.\n\nroboco.tech\n\n#buildinpublic #aiagents #devtools #indiehackers",
"char_count": 499,
"limit": 2200,
"within_limit": true
}
}
}
@@ -0,0 +1,9 @@
// Default sample props for local preview of the RoboCo v0.27.0 release clip.
// The video-renderer sidecar OVERWRITES this file at render time with the
// real per-request values + window.__ORIENTATION__.
window.__PROPS__ = {
introText: "Four brains, zero bans",
toastTitle: "v0.27.0 shipped",
toastBody: "I approved once. 25 agents shipped it.",
};
window.__ORIENTATION__ = "vertical";
@@ -0,0 +1,125 @@
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.27.0 release composition.
// Extends the panel-demo register: panel chrome, typing reveal, four 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.27.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="40"/);
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(4);
expect(progressCount).toBe(4);
expect(completedCount).toBe(4);
expect(html).toContain("v0.27.0");
expect(html).toContain("WAF knows the real IP");
expect(html).toContain("Telegram Mini App V6");
expect(html).toContain("Codex and Gemini join");
expect(html).toContain("Cost-tiered routing");
// 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 the metronomic identical
// 5.0s/5.0s/5.0s spacing the craft bar bans (a real prior revision cycle
// on this composition).
const cardDelays = [...html.matchAll(/class="pk-card" style="animation-delay: ([\d.]+)s;"/g)].map((m) =>
parseFloat(m[1]),
);
expect(cardDelays.length).toBe(4);
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.27.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,353 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.27.0" data-composition-duration="40" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.27.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.27.0 release clip. Tighter spacing, smaller
hero, and the stats overlay covers the full frame for readability.
Total 40s. */
.r27-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: r27-hero-out 0.6s var(--pk-ease) 2.6s forwards;
}
@keyframes r27-hero-out { to { opacity: 0; visibility: hidden; } }
.r27-hero__markwrap { position: relative; width: 400px; height: 400px; }
.r27-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: r27-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes r27-mark-in { to { transform: scale(1); opacity: 1; } }
.r27-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: r27-ring-in 0.3s linear 0.9s forwards, r27-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes r27-ring-in { to { opacity: 1; } }
@keyframes r27-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.r27-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;
}
.r27-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: r27-side-in 0.7s var(--pk-ease) 2.8s forwards;
}
@keyframes r27-side-in { to { transform: translateX(0); opacity: 1; } }
.r27-side__logo {
width: 40px;
height: 40px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 18px 4px;
}
.r27-side__logo img { width: 30px; height: 30px; }
.r27-nav {
display: flex;
align-items: center;
height: 44px;
padding: 0 14px;
border-radius: 10px;
font-size: 18px;
color: var(--pk-text-muted);
}
.r27-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; }
.r27-user { font-size: 18px; }
.r27-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; }
.r27-sub { font-size: 18px; }
.r27-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: r27-stats-in 0.4s var(--pk-ease) 25.7s forwards, r27-stats-out 0.5s var(--pk-ease) 33.0s forwards;
}
@keyframes r27-stats-in { to { opacity: 1; } }
.r27-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;
}
.r27-stats__line--accent { color: var(--pk-accent); }
@keyframes r27-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 84px; width: 480px; }
.pk-outro { --pk-outro-bottom: 28px; }
.r27-outro__logo {
width: 48px;
height: 48px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.r27-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.27.0" data-width="1080" data-height="1080" data-start="0" data-duration="40" data-fps="30">
<div class="r27-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r27-hero__markwrap">
<img class="r27-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r27-hero__ring"></div>
</div>
<div class="r27-hero__version">v0.27.0</div>
<div class="r27-hero__tag">Four brains, zero bans</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;
22.5 28 -96 1.06; 24.3 22 -98 1.05;
25.5 0 0 1.0; 33.3 0 0 1.0;
34.4 -20 -60 1.03; 35.9 -18 -58 1.028;
37.6 0 0 1.0; 39.6 0 4 1.004">
<div class="pk-frame clip" data-start="0" data-duration="40" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r27-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r27-nav">Overview</div>
<div class="r27-nav">Business</div>
<div class="r27-nav">Social</div>
<div class="r27-nav r27-nav--active">Tasks</div>
<div class="r27-nav">Kanban</div>
<div class="r27-nav">Git</div>
<div class="r27-nav">Agents</div>
<div class="r27-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r27-user">
<div class="pk-frame__status clip" data-start="0" data-duration="40" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="r27-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">Four brains, zero bans</span><span class="pk-caret"></span></div>
</div>
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.27.0</div>
<!-- Varied beat spacing (5.5s/6.4s/5.3s gaps), not the
metronomic identical 5.0s the craft bar bans. -->
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">WAF knows the real IP</div>
<div class="r27-sub">Tailscale hops don't get banned anymore.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">fastapi-guard</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">Telegram Mini App V6</div>
<div class="r27-sub">Real chat scopes, wallet-style metrics.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">telegram</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">Codex and Gemini join</div>
<div class="r27-sub">Four model providers, one gateway now.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">providers</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 class="pk-card" style="animation-delay: 22.2s;">
<div class="pk-card__title">Cost-tiered routing</div>
<div class="r27-sub">Save a routing mix, restore it anytime.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">routing</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 22.2s; --pk-pill-exit-delay: 23.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 23.8s;">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 24s (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;
21.8 220 668; 22.5 150 740; 23.9 136 752;
24.8 480 780; 25.6 920 820">
<div class="pk-cursor__glyph"></div>
</div>
<div class="r27-stats">
<div class="r27-stats__line" style="animation-delay: 25.7s;">1 release</div>
<div class="r27-stats__line r27-stats__line--accent" style="animation-delay: 26.0s;">4 providers</div>
<div class="r27-stats__line" style="animation-delay: 26.3s;">0 bans</div>
</div>
<div class="pk-toast" style="animation-delay: 31.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.27.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: 36.3s;">
<span class="r27-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.27.0"] = {
id: "release-0.27.0",
duration: 40,
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,379 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.27.0" data-composition-duration="40" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.27.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.27.0 release clip - panel-demo register, same shape as
release-0.26.0: identity cold open, panel assembles, four feature
cards flip to completed one per scene, cursor click, receipt
stats, toast, lockup. Total 40s. */
/* ---- Identity cold open -------------------------------------------- */
.r27-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: r27-hero-out 0.6s var(--pk-ease) 2.6s forwards;
}
@keyframes r27-hero-out { to { opacity: 0; visibility: hidden; } }
.r27-hero__markwrap { position: relative; width: 520px; height: 520px; }
.r27-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: r27-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes r27-mark-in { to { transform: scale(1); opacity: 1; } }
.r27-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: r27-ring-in 0.3s linear 0.9s forwards, r27-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes r27-ring-in { to { opacity: 1; } }
@keyframes r27-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.r27-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;
}
.r27-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: r27-side-in 0.7s var(--pk-ease) 2.8s forwards;
}
@keyframes r27-side-in { to { transform: translateX(0); opacity: 1; } }
.r27-side__logo {
width: 44px;
height: 44px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 22px 4px;
}
.r27-side__logo img { width: 34px; height: 34px; }
.r27-nav {
display: flex;
align-items: center;
height: 52px;
padding: 0 16px;
border-radius: 10px;
font-size: 21px;
color: var(--pk-text-muted);
}
.r27-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; }
.r27-user {
display: flex;
align-items: center;
gap: 12px;
color: var(--pk-text-muted);
font-size: 20px;
}
.r27-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);
}
.r27-sub {
margin-top: 10px;
font-size: 22px;
line-height: 1.35;
color: var(--pk-text-muted);
}
/* Receipt stats overlay. */
.r27-stats {
position: absolute;
right: 64px;
bottom: 320px;
text-align: right;
font-family: var(--pk-font-display);
}
.r27-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;
}
.r27-stats__line--accent { color: var(--pk-accent); }
.r27-stats { animation: r27-stats-out 0.5s var(--pk-ease) 33.0s forwards; }
@keyframes r27-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;
}
.r27-outro__logo {
width: 52px;
height: 52px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.r27-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.27.0" data-width="1080" data-height="1920" data-start="0" data-duration="40" data-fps="30">
<!-- Beat 1: identity cold open. -->
<div class="r27-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r27-hero__markwrap">
<img class="r27-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r27-hero__ring"></div>
</div>
<div class="r27-hero__version">v0.27.0</div>
<div class="r27-hero__tag">Four brains, zero bans</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;
22.6 30 -150 1.065; 24.5 26 -156 1.06;
26.6 0 0 1.0; 30.3 0 0 1.0;
31.7 -26 -120 1.035; 34.0 -22 -116 1.03;
37.2 0 0 1.0; 39.6 0 6 1.005">
<div class="pk-frame clip" data-start="0" data-duration="40" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r27-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r27-nav">Overview</div>
<div class="r27-nav">Business</div>
<div class="r27-nav">Social</div>
<div class="r27-nav r27-nav--active">Tasks</div>
<div class="r27-nav">Kanban</div>
<div class="r27-nav">Git</div>
<div class="r27-nav">Agents</div>
<div class="r27-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r27-user">
<div class="pk-frame__status clip" data-start="0" data-duration="40" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="r27-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">Four brains, zero bans</span><span class="pk-caret"></span></div>
</div>
<!-- Beat 4: four feature cards on varied beat spacing (5.5s/6.4s/5.3s
gaps, not the metronomic identical 5.0s the craft bar bans),
each fully visible before the next enters. -->
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.27.0</div>
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">WAF knows the real IP</div>
<div class="r27-sub">Tailscale hops don't get banned anymore.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">fastapi-guard</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">Telegram Mini App V6</div>
<div class="r27-sub">Real chat scopes, wallet-style metrics.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">telegram</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">Codex and Gemini join</div>
<div class="r27-sub">Four model providers, one gateway now.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">providers</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 class="pk-card" style="animation-delay: 22.2s;">
<div class="pk-card__title">Cost-tiered routing</div>
<div class="r27-sub">Save a routing mix, restore it anytime.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">routing</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 22.2s; --pk-pill-exit-delay: 23.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 23.8s;">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;
21.8 255 985; 22.6 190 1068; 24.0 174 1082;
25.4 420 1180; 26.4 560 1250; 29.8 585 1268;
30.7 615 1420; 31.7 612 1462 click;
33.8 700 1520; 35.4 940 1660">
<div class="pk-cursor__glyph"></div>
</div>
<!-- Beat 5: receipt stats. -->
<div class="r27-stats">
<div class="r27-stats__line" style="animation-delay: 25.7s;">1 release</div>
<div class="r27-stats__line r27-stats__line--accent" style="animation-delay: 26.0s;">4 providers</div>
<div class="r27-stats__line" style="animation-delay: 26.3s;">0 bans</div>
</div>
<!-- Beat 6: confirmation toast, then lockup. -->
<div class="pk-toast" style="animation-delay: 31.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.27.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: 36.3s;">
<span class="r27-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.27.0"] = {
id: "release-0.27.0",
duration: 40,
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>