mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(motion): panel-demo kit + reference composition (#365)
Reusable pk-* building blocks (motion/kit/) that recreate the control panel's look for HyperFrames compositions: frame chrome, task card, status pills with swap crossfade, chips, toast, typing reveal, cursor sprite. Tokens lifted from the panel's dark theme and badge components. Reference composition motion/compositions/panel-demo/ (12s, both cuts): a task title types into intake, the card materializes, the cursor clicks, the status pill flips to completed, a toast slides in. Verified: motion vitest gate green (7/7); both cuts visually checked in-browser at native resolution. Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -35,3 +35,22 @@ This composition is the library's reference point — match its restraint, don't
|
|||||||
- **Motion** — entrances are CSS keyframe animations with a restrained easing (`cubic-bezier(0.22, 1, 0.36, 1)` — smooth settle, no overshoot, approximating Remotion's `spring({damping:17, mass:0.7, stiffness:140})` without the bounce), staggered across elements via `animation-delay` rather than all firing on frame 0. Keep one continuous ambient motion (here: the scanning accent line) so the frame is never fully static once entrances land. Every animation should earn its place — hierarchy, or a beat of pacing, not motion for its own sake.
|
- **Motion** — entrances are CSS keyframe animations with a restrained easing (`cubic-bezier(0.22, 1, 0.36, 1)` — smooth settle, no overshoot, approximating Remotion's `spring({damping:17, mass:0.7, stiffness:140})` without the bounce), staggered across elements via `animation-delay` rather than all firing on frame 0. Keep one continuous ambient motion (here: the scanning accent line) so the frame is never fully static once entrances land. Every animation should earn its place — hierarchy, or a beat of pacing, not motion for its own sake.
|
||||||
- **Layout** — anchor content asymmetrically (this clip sits in the lower two-thirds, left-aligned); avoid a perfectly centered card, which reads as a generic template rather than a designed frame.
|
- **Layout** — anchor content asymmetrically (this clip sits in the lower two-thirds, left-aligned); avoid a perfectly centered card, which reads as a generic template rather than a designed frame.
|
||||||
- **AI tells to avoid** — no default AI-purple gradient wash, no centered-everything, no emoji as design elements, no one-font-one-size, no em dash in on-screen copy (voiceover script, highlight bullets, kicker text) or filler verbs ("Elevate", "Seamless", "Unleash", "Next-Gen").
|
- **AI tells to avoid** — no default AI-purple gradient wash, no centered-everything, no emoji as design elements, no one-font-one-size, no em dash in on-screen copy (voiceover script, highlight bullets, kicker text) or filler verbs ("Elevate", "Seamless", "Unleash", "Next-Gen").
|
||||||
|
|
||||||
|
## Panel-demo kit (`kit/`)
|
||||||
|
|
||||||
|
`kit/` is a second register alongside the release-announcement's text-card
|
||||||
|
style: reusable `pk-`-namespaced CSS/HTML that recreates the control panel's
|
||||||
|
look (dark chrome, task cards, status pills, toasts, a typing reveal, a
|
||||||
|
cursor) so a composition can simulate the product actually being used,
|
||||||
|
instead of announcing it over a headline. Use the **text-card register**
|
||||||
|
(release-announcement's pattern) for version/feature announcements with no
|
||||||
|
product visuals; use the **demo register** (`kit/`) whenever the story is
|
||||||
|
"watch this happen in the app" — a task moving through the panel, a feature
|
||||||
|
being triggered, an agent doing something visible.
|
||||||
|
|
||||||
|
`compositions/panel-demo/` is the reference composition: a task title types
|
||||||
|
into an intake field, a card materializes in a column, a cursor clicks it
|
||||||
|
done, a toast confirms, out on "roboco.tech". Start a new demo-register
|
||||||
|
composition from its structure and `kit.css`'s classes rather than
|
||||||
|
reinventing the panel's chrome per clip. See `kit/README.md` for the full
|
||||||
|
piece-by-piece reference.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
// HTML-structure smoke test for the panel-demo composition — mirrors
|
||||||
|
// release-announcement.test.js. We parse the HTML as text and assert
|
||||||
|
// structural invariants: dimensions, props.js wiring, the kit stylesheet
|
||||||
|
// link, timed clips, the kit's pill/toast/typing/cursor pieces actually
|
||||||
|
// being used, and the offline-render constraint (no CDN, no network URLs).
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const dir = here;
|
||||||
|
const read = (name) => readFileSync(join(dir, name), "utf8");
|
||||||
|
|
||||||
|
const orientations = [
|
||||||
|
{ file: "vertical.html", height: "1920" },
|
||||||
|
{ file: "square.html", height: "1080" },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("panel-demo composition", () => {
|
||||||
|
it("ships props.js (default preview values, sidecar overwrites at render)", () => {
|
||||||
|
expect(existsSync(join(dir, "props.js"))).toBe(true);
|
||||||
|
const src = read("props.js");
|
||||||
|
expect(src).toContain("window.__PROPS__");
|
||||||
|
expect(src).toContain("window.__ORIENTATION__");
|
||||||
|
expect(src).toContain("taskTitle");
|
||||||
|
expect(src).toContain("toastTitle");
|
||||||
|
expect(src).toContain("toastBody");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(orientations)("$file parses with correct dimensions and wiring", ({ file, height }) => {
|
||||||
|
const html = read(file);
|
||||||
|
|
||||||
|
// HyperFrames render params.
|
||||||
|
expect(html).toContain('data-width="1080"');
|
||||||
|
expect(html).toContain(`data-height="${height}"`);
|
||||||
|
expect(html).toMatch(/data-duration="12"/);
|
||||||
|
expect(html).toMatch(/data-fps="30"/);
|
||||||
|
|
||||||
|
// props.js loaded before the kit helper and before any inline script
|
||||||
|
// reads window.__PROPS__ / window.PanelKit.
|
||||||
|
const propsIdx = html.indexOf('<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 (this composition has no theme.css of its own).
|
||||||
|
expect(html).toContain('href="../../kit/kit.css"');
|
||||||
|
|
||||||
|
// At least one timed clip element.
|
||||||
|
expect(html).toMatch(/class="[^"]*clip[^"]*"/);
|
||||||
|
|
||||||
|
// Panel frame chrome.
|
||||||
|
expect(html).toContain("pk-frame");
|
||||||
|
expect(html).toContain("pk-frame__sidebar");
|
||||||
|
expect(html).toContain("pk-frame__topbar");
|
||||||
|
|
||||||
|
// Typing reveal: container + caret present, wired to the kit helper.
|
||||||
|
expect(html).toContain('id="typeTitle"');
|
||||||
|
expect(html).toContain("pk-caret");
|
||||||
|
expect(html).toContain("PanelKit.typeText");
|
||||||
|
|
||||||
|
// Task card + the progress -> completed pill swap.
|
||||||
|
expect(html).toContain("pk-card");
|
||||||
|
expect(html).toContain("pk-pill--progress");
|
||||||
|
expect(html).toContain("pk-pill--completed");
|
||||||
|
|
||||||
|
// Cursor glide + click.
|
||||||
|
expect(html).toContain("pk-cursor");
|
||||||
|
expect(html).toContain("pk-cursor__ring");
|
||||||
|
|
||||||
|
// Toast.
|
||||||
|
expect(html).toContain("pk-toast");
|
||||||
|
expect(html).toContain('id="toastTitle"');
|
||||||
|
expect(html).toContain('id="toastBody"');
|
||||||
|
|
||||||
|
// Outro headline moment.
|
||||||
|
expect(html).toContain("pk-outro");
|
||||||
|
expect(html).toContain("roboco.tech");
|
||||||
|
|
||||||
|
// No CDN / network fetch of any kind — the render is offline.
|
||||||
|
expect(html).not.toMatch(/<script[^>]+src="https?:\/\//);
|
||||||
|
expect(html).not.toMatch(/href="https?:\/\//);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Default sample props for local preview (npx hyperframes preview). The
|
||||||
|
// video-renderer sidecar OVERWRITES this file at render time with the real
|
||||||
|
// per-request values + window.__ORIENTATION__. 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.
|
||||||
|
window.__PROPS__ = {
|
||||||
|
taskTitle: "Ship the panel-demo kit",
|
||||||
|
agentName: "fe-dev-1",
|
||||||
|
teamLabel: "Frontend",
|
||||||
|
priorityLabel: "P1 - High",
|
||||||
|
toastTitle: "Task completed",
|
||||||
|
toastBody: "fe-dev-1 merged the panel-demo kit PR.",
|
||||||
|
};
|
||||||
|
window.__ORIENTATION__ = "vertical";
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-composition-id="panel-demo" data-composition-duration="12" data-fps="30">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Panel Demo — square (1080×1080)</title>
|
||||||
|
<link rel="stylesheet" href="../../kit/kit.css" />
|
||||||
|
<script src="props.js"></script>
|
||||||
|
<script src="../../kit/kit.js"></script>
|
||||||
|
<style>
|
||||||
|
/* Orientation-specific placement only — component look lives in
|
||||||
|
kit.css. Timing is identical between orientations (set inline on
|
||||||
|
the elements below); only these positions change with the stage,
|
||||||
|
since the square cut's content area is far shorter than vertical's. */
|
||||||
|
.pk-intake { --pk-intake-top: 16px; }
|
||||||
|
.pk-column { --pk-column-top: 130px; }
|
||||||
|
.pk-toast { --pk-toast-bottom: 384px; }
|
||||||
|
.pk-outro { --pk-outro-bottom: 40px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="stage" data-composition-id="panel-demo" data-width="1080" data-height="1080" data-start="0" data-duration="12" data-fps="30">
|
||||||
|
<div class="pk-frame clip" data-start="0" data-duration="12" data-track-index="1">
|
||||||
|
<aside class="pk-frame__sidebar">
|
||||||
|
<div class="pk-frame__logo"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem pk-frame__navitem--active"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<header class="pk-frame__topbar">
|
||||||
|
<div class="pk-frame__search">Search tasks, agents...</div>
|
||||||
|
<div class="pk-frame__status clip" data-start="0" data-duration="12" data-track-index="0">
|
||||||
|
<span class="pk-frame__statusdot"></span>
|
||||||
|
<span>Live</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pk-frame__content">
|
||||||
|
<!-- Intake: a task title typing out character-by-character. -->
|
||||||
|
<div class="pk-intake clip" data-start="0.2" data-duration="11.8" data-track-index="2" style="animation-delay: 0.2s;">
|
||||||
|
<div class="pk-intake__label">New task</div>
|
||||||
|
<div class="pk-intake__field"><span id="typeTitle">Ship the panel-demo kit</span><span class="pk-caret"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kanban column hosting the task card. -->
|
||||||
|
<div class="pk-column">
|
||||||
|
<div class="pk-column__header">Tasks</div>
|
||||||
|
<div class="pk-card clip" data-start="3.6" data-duration="8.4" data-track-index="3" style="animation-delay: 3.6s;">
|
||||||
|
<div class="pk-card__title" id="cardTitle">Ship the panel-demo kit</div>
|
||||||
|
<div class="pk-card__meta">
|
||||||
|
<span class="pk-chip pk-chip--purple" id="cardTeam">Frontend</span>
|
||||||
|
<span class="pk-chip pk-chip--blue" id="cardPriority">P1 - High</span>
|
||||||
|
<span class="pk-card__assignee" id="cardAssignee">@fe-dev-1</span>
|
||||||
|
</div>
|
||||||
|
<div class="pk-card__status">
|
||||||
|
<span
|
||||||
|
class="pk-pill pk-pill--progress pk-pill--swap-out clip"
|
||||||
|
data-start="3.6"
|
||||||
|
data-duration="3.8"
|
||||||
|
data-track-index="4"
|
||||||
|
style="--pk-pill-enter-delay: 3.6s; --pk-pill-exit-delay: 7s;"
|
||||||
|
>in progress</span>
|
||||||
|
<span
|
||||||
|
class="pk-pill pk-pill--completed pk-pill--swap-in clip"
|
||||||
|
data-start="7.4"
|
||||||
|
data-duration="4.6"
|
||||||
|
data-track-index="5"
|
||||||
|
style="--pk-pill-enter-delay: 7.4s;"
|
||||||
|
>completed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cursor glide to the card, then a click pulse on the pill. -->
|
||||||
|
<div
|
||||||
|
class="pk-cursor clip"
|
||||||
|
data-start="6"
|
||||||
|
data-duration="6"
|
||||||
|
data-track-index="6"
|
||||||
|
style="--pk-cursor-delay: 6s; --pk-cursor-x0: 620px; --pk-cursor-y0: 140px; --pk-cursor-x1: 152px; --pk-cursor-y1: 310px;"
|
||||||
|
>
|
||||||
|
<div class="pk-cursor__glyph"></div>
|
||||||
|
<div class="pk-cursor__ring" style="--pk-click-delay: 7.2s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast: the completion notification. -->
|
||||||
|
<div class="pk-toast clip" data-start="8.6" data-duration="3.4" data-track-index="7" style="animation-delay: 8.6s;">
|
||||||
|
<div class="pk-toast__icon"></div>
|
||||||
|
<div class="pk-toast__body">
|
||||||
|
<div class="pk-toast__title" id="toastTitle">Task completed</div>
|
||||||
|
<div class="pk-toast__text" id="toastBody">fe-dev-1 merged the panel-demo kit PR.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Outro headline moment. -->
|
||||||
|
<div class="pk-outro clip" data-start="10.2" data-duration="1.8" data-track-index="8" style="animation-delay: 10.2s;">roboco.tech</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["panel-demo"] = {
|
||||||
|
id: "panel-demo",
|
||||||
|
duration: 12,
|
||||||
|
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 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("cardTitle", props.taskTitle);
|
||||||
|
setText("cardTeam", props.teamLabel);
|
||||||
|
setText("cardPriority", props.priorityLabel);
|
||||||
|
setText("toastTitle", props.toastTitle);
|
||||||
|
setText("toastBody", props.toastBody);
|
||||||
|
|
||||||
|
if (typeof props.agentName === "string" && props.agentName) {
|
||||||
|
var assignee = document.getElementById("cardAssignee");
|
||||||
|
if (assignee) assignee.textContent = "@" + props.agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The intake field types out the same task title. Populate it from
|
||||||
|
// the prop (falling back to the placeholder already in the markup)
|
||||||
|
// before splitting it into per-character spans for the reveal.
|
||||||
|
var typeEl = document.getElementById("typeTitle");
|
||||||
|
if (typeEl) {
|
||||||
|
if (typeof props.taskTitle === "string" && props.taskTitle) {
|
||||||
|
typeEl.textContent = props.taskTitle;
|
||||||
|
}
|
||||||
|
window.PanelKit.typeText(typeEl, { delay: 0.6 });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-composition-id="panel-demo" data-composition-duration="12" data-fps="30">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Panel Demo — vertical (1080×1920)</title>
|
||||||
|
<link rel="stylesheet" href="../../kit/kit.css" />
|
||||||
|
<script src="props.js"></script>
|
||||||
|
<script src="../../kit/kit.js"></script>
|
||||||
|
<style>
|
||||||
|
/* Orientation-specific placement only — component look lives in
|
||||||
|
kit.css. Timing is identical between orientations (set inline on
|
||||||
|
the elements below); only these positions change with the stage. */
|
||||||
|
.pk-intake { --pk-intake-top: 24px; }
|
||||||
|
.pk-column { --pk-column-top: 200px; }
|
||||||
|
.pk-toast { --pk-toast-bottom: 160px; }
|
||||||
|
.pk-outro { --pk-outro-bottom: 56px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="stage" data-composition-id="panel-demo" data-width="1080" data-height="1920" data-start="0" data-duration="12" data-fps="30">
|
||||||
|
<div class="pk-frame clip" data-start="0" data-duration="12" data-track-index="1">
|
||||||
|
<aside class="pk-frame__sidebar">
|
||||||
|
<div class="pk-frame__logo"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem pk-frame__navitem--active"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
<div class="pk-frame__navitem"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<header class="pk-frame__topbar">
|
||||||
|
<div class="pk-frame__search">Search tasks, agents...</div>
|
||||||
|
<div class="pk-frame__status clip" data-start="0" data-duration="12" data-track-index="0">
|
||||||
|
<span class="pk-frame__statusdot"></span>
|
||||||
|
<span>Live</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pk-frame__content">
|
||||||
|
<!-- Intake: a task title typing out character-by-character. -->
|
||||||
|
<div class="pk-intake clip" data-start="0.2" data-duration="11.8" data-track-index="2" style="animation-delay: 0.2s;">
|
||||||
|
<div class="pk-intake__label">New task</div>
|
||||||
|
<div class="pk-intake__field"><span id="typeTitle">Ship the panel-demo kit</span><span class="pk-caret"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kanban column hosting the task card. -->
|
||||||
|
<div class="pk-column">
|
||||||
|
<div class="pk-column__header">Tasks</div>
|
||||||
|
<div class="pk-card clip" data-start="3.6" data-duration="8.4" data-track-index="3" style="animation-delay: 3.6s;">
|
||||||
|
<div class="pk-card__title" id="cardTitle">Ship the panel-demo kit</div>
|
||||||
|
<div class="pk-card__meta">
|
||||||
|
<span class="pk-chip pk-chip--purple" id="cardTeam">Frontend</span>
|
||||||
|
<span class="pk-chip pk-chip--blue" id="cardPriority">P1 - High</span>
|
||||||
|
<span class="pk-card__assignee" id="cardAssignee">@fe-dev-1</span>
|
||||||
|
</div>
|
||||||
|
<div class="pk-card__status">
|
||||||
|
<span
|
||||||
|
class="pk-pill pk-pill--progress pk-pill--swap-out clip"
|
||||||
|
data-start="3.6"
|
||||||
|
data-duration="3.8"
|
||||||
|
data-track-index="4"
|
||||||
|
style="--pk-pill-enter-delay: 3.6s; --pk-pill-exit-delay: 7s;"
|
||||||
|
>in progress</span>
|
||||||
|
<span
|
||||||
|
class="pk-pill pk-pill--completed pk-pill--swap-in clip"
|
||||||
|
data-start="7.4"
|
||||||
|
data-duration="4.6"
|
||||||
|
data-track-index="5"
|
||||||
|
style="--pk-pill-enter-delay: 7.4s;"
|
||||||
|
>completed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cursor glide to the card, then a click pulse on the pill. -->
|
||||||
|
<div
|
||||||
|
class="pk-cursor clip"
|
||||||
|
data-start="6"
|
||||||
|
data-duration="6"
|
||||||
|
data-track-index="6"
|
||||||
|
style="--pk-cursor-delay: 6s; --pk-cursor-x0: 620px; --pk-cursor-y0: 160px; --pk-cursor-x1: 152px; --pk-cursor-y1: 380px;"
|
||||||
|
>
|
||||||
|
<div class="pk-cursor__glyph"></div>
|
||||||
|
<div class="pk-cursor__ring" style="--pk-click-delay: 7.2s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast: the completion notification. -->
|
||||||
|
<div class="pk-toast clip" data-start="8.6" data-duration="3.4" data-track-index="7" style="animation-delay: 8.6s;">
|
||||||
|
<div class="pk-toast__icon"></div>
|
||||||
|
<div class="pk-toast__body">
|
||||||
|
<div class="pk-toast__title" id="toastTitle">Task completed</div>
|
||||||
|
<div class="pk-toast__text" id="toastBody">fe-dev-1 merged the panel-demo kit PR.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Outro headline moment. -->
|
||||||
|
<div class="pk-outro clip" data-start="10.2" data-duration="1.8" data-track-index="8" style="animation-delay: 10.2s;">roboco.tech</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["panel-demo"] = {
|
||||||
|
id: "panel-demo",
|
||||||
|
duration: 12,
|
||||||
|
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 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("cardTitle", props.taskTitle);
|
||||||
|
setText("cardTeam", props.teamLabel);
|
||||||
|
setText("cardPriority", props.priorityLabel);
|
||||||
|
setText("toastTitle", props.toastTitle);
|
||||||
|
setText("toastBody", props.toastBody);
|
||||||
|
|
||||||
|
if (typeof props.agentName === "string" && props.agentName) {
|
||||||
|
var assignee = document.getElementById("cardAssignee");
|
||||||
|
if (assignee) assignee.textContent = "@" + props.agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The intake field types out the same task title. Populate it from
|
||||||
|
// the prop (falling back to the placeholder already in the markup)
|
||||||
|
// before splitting it into per-character spans for the reveal.
|
||||||
|
var typeEl = document.getElementById("typeTitle");
|
||||||
|
if (typeEl) {
|
||||||
|
if (typeof props.taskTitle === "string" && props.taskTitle) {
|
||||||
|
typeEl.textContent = props.taskTitle;
|
||||||
|
}
|
||||||
|
window.PanelKit.typeText(typeEl, { delay: 0.6 });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# kit
|
||||||
|
|
||||||
|
Reusable plain-CSS/HTML building blocks that recreate the RoboCo control
|
||||||
|
panel's look, for compositions that want to simulate the product rather
|
||||||
|
than run a text card. Hand-rolled (no React, no Tailwind, no bundler) — this
|
||||||
|
is a recreation of the panel's design language, not a port of its
|
||||||
|
components. Load `kit.css` (and `kit.js` if you use the typing helper); no
|
||||||
|
composition-level `theme.css` is needed, kit.css owns the reset + fonts.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="../../kit/kit.css" />
|
||||||
|
<script src="props.js"></script>
|
||||||
|
<script src="../../kit/kit.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Every class is namespaced `pk-`. See `compositions/panel-demo/` for a full
|
||||||
|
worked example (task types in, a card appears, a cursor clicks it done, a
|
||||||
|
toast confirms it).
|
||||||
|
|
||||||
|
## Pieces
|
||||||
|
|
||||||
|
- **`pk-frame`** — the app chrome: a slim sidebar (`pk-frame__sidebar`,
|
||||||
|
`pk-frame__navitem[--active]`) and a top bar (`pk-frame__topbar`,
|
||||||
|
`pk-frame__search`, `pk-frame__status` for the pulsing "Live" dot) around
|
||||||
|
a `pk-frame__content` area. Fixed sidebar/topbar sizing works at both
|
||||||
|
1080x1920 and 1080x1080 — only the content area's height changes.
|
||||||
|
- **`pk-column`** / **`pk-card`** — a kanban-ish list container
|
||||||
|
(`pk-column__header` + children) hosting task cards. A card is
|
||||||
|
`pk-card__title`, `pk-card__meta` (chips + `pk-card__assignee`), and
|
||||||
|
`pk-card__status` (a pill).
|
||||||
|
- **`pk-chip`** — a small team/priority tag: `<span class="pk-chip
|
||||||
|
pk-chip--purple">Frontend</span>`. Variants: `--purple`, `--blue`,
|
||||||
|
`--green`, `--orange`, `--pink`, `--gray`.
|
||||||
|
- **`pk-pill`** — a status pill: `<span class="pk-pill
|
||||||
|
pk-pill--progress">in progress</span>`. Variants: `--progress`,
|
||||||
|
`--review`, `--approved`, `--completed`. To swap one pill for another
|
||||||
|
(e.g. progress -> completed), stack two pills at the same spot inside a
|
||||||
|
`position: relative` `pk-card__status` and give the outgoing one
|
||||||
|
`pk-pill--swap-out` (fades in then out) and the incoming one
|
||||||
|
`pk-pill--swap-in` (fades in and stays), each with its own
|
||||||
|
`--pk-pill-enter-delay` / `--pk-pill-exit-delay` (composition-absolute
|
||||||
|
seconds — the same convention `data-start` uses).
|
||||||
|
- **`pk-toast`** — a notification card that slides in from a corner:
|
||||||
|
`pk-toast__icon` + `pk-toast__body` (`pk-toast__title`,
|
||||||
|
`pk-toast__text`). Position via `--pk-toast-bottom` (right-anchored).
|
||||||
|
- **`pk-type`** / **`pk-caret`** — character-by-character typing reveal.
|
||||||
|
Write the target element's final text directly in the HTML, then call
|
||||||
|
`window.PanelKit.typeText(el, { delay, stagger })` (from `kit.js`) once,
|
||||||
|
synchronously, before rendering starts — it splits the text into
|
||||||
|
`pk-type__char` spans with staggered `animation-delay`s (works with any
|
||||||
|
font, no per-character width math). `delay` is the composition-absolute
|
||||||
|
second the first character appears; `stagger` defaults to 0.045s/char.
|
||||||
|
Add a `<span class="pk-caret"></span>` next to it for the blinking caret.
|
||||||
|
- **`pk-cursor`** — a small CSS-shape cursor (`pk-cursor__glyph`) that
|
||||||
|
glides along a straight path and pulses (`pk-cursor__ring`) on arrival.
|
||||||
|
Position via `--pk-cursor-x0/y0/x1/y1` (content-box-relative pixels) and
|
||||||
|
timing via `--pk-cursor-delay` (glide start) / `--pk-click-delay` (pulse
|
||||||
|
start), all composition-absolute seconds.
|
||||||
|
|
||||||
|
## Offline constraints (same as every composition)
|
||||||
|
|
||||||
|
No CDN, no npm runtime deps, no bundler — plain files only, loaded via
|
||||||
|
relative paths (`../../kit/...` from a composition two levels down). Fonts
|
||||||
|
are the vendored `motion/public/fonts/*.woff2`, loaded via `@font-face` in
|
||||||
|
`kit.css`. Motion is CSS keyframes timed with `animation-delay` (seconds
|
||||||
|
from frame 0 — no `requestAnimationFrame`, no `Date.now()`), so a render is
|
||||||
|
frame-deterministic.
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
/* Panel kit — reusable building blocks that recreate the RoboCo control
|
||||||
|
* panel's look for HyperFrames compositions. This is a hand-rolled
|
||||||
|
* recreation (plain CSS/HTML), not a screenshot or a port of the panel's
|
||||||
|
* React/Tailwind components. Every class is namespaced `pk-` (panel kit).
|
||||||
|
*
|
||||||
|
* Tokens are lifted from panel/src/app/globals.css's dark theme (the panel
|
||||||
|
* always renders dark in product screenshots) and from the status-pill /
|
||||||
|
* type-chip color maps in panel/src/components/tasks/*-badge.tsx. See
|
||||||
|
* motion/kit/README.md for usage and motion/README.md for the design bar
|
||||||
|
* this kit still has to answer to (variance ~6, motion ~6, density ~4).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Share Tech Mono";
|
||||||
|
src: url("../public/fonts/ShareTechMono-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
src: url("../public/fonts/Inter-Variable.woff2") format("woff2-variations");
|
||||||
|
font-weight: 400 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Surfaces — panel dark theme is achromatic (oklch chroma 0), not the
|
||||||
|
* warm-tinted ink used by release-announcement's theme.css. */
|
||||||
|
--pk-bg: #0a0a0a; /* --background (dark) oklch(0.145 0 0) */
|
||||||
|
--pk-surface: #1a1a1a; /* --card (dark) oklch(0.205 0 0) */
|
||||||
|
--pk-surface-raised: #262626; /* --secondary/--muted (dark) oklch(0.269 0 0) */
|
||||||
|
--pk-border: rgba(255, 255, 255, 0.10); /* --border (dark), exact */
|
||||||
|
--pk-border-strong: rgba(255, 255, 255, 0.16);
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--pk-text: #fafafa; /* --foreground (dark) oklch(0.985 0 0) */
|
||||||
|
--pk-text-muted: #a3a3a3; /* --muted-foreground (dark) oklch(0.708 0 0) */
|
||||||
|
--pk-text-faint: #737373;
|
||||||
|
|
||||||
|
/* The one active-state inversion the panel uses (selected sidebar item):
|
||||||
|
* near-white bg, near-black text. */
|
||||||
|
--pk-active-bg: #e5e5e5; /* --primary (dark) oklch(0.922 0 0) */
|
||||||
|
--pk-active-fg: #1a1a1a; /* --primary-foreground (dark) oklch(0.205 0 0) */
|
||||||
|
|
||||||
|
/* One accent, used sparingly (cursor, caret, live dot) — shared with
|
||||||
|
* release-announcement's theme so multiple compositions still read as one
|
||||||
|
* library rather than each clip inventing its own brand color. */
|
||||||
|
--pk-accent: #ff5a1f;
|
||||||
|
|
||||||
|
/* Status pill palette — solid bg + white text, lifted from
|
||||||
|
* task-status-badge.tsx's Tailwind classes. */
|
||||||
|
--pk-pill-progress: #2563eb; /* bg-blue-600 (IN_PROGRESS) */
|
||||||
|
--pk-pill-review: #7c3aed; /* violet-600, echoes the violet PR chip */
|
||||||
|
--pk-pill-approved: #d97706; /* bg-amber-600 (AWAITING_CEO_APPROVAL) */
|
||||||
|
--pk-pill-completed: #16a34a; /* green-600 (COMPLETED family) */
|
||||||
|
|
||||||
|
--pk-radius: 16px;
|
||||||
|
--pk-radius-sm: 10px;
|
||||||
|
--pk-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
|
||||||
|
--pk-font-display: "Share Tech Mono";
|
||||||
|
--pk-font-body: "Inter";
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--pk-bg);
|
||||||
|
color: var(--pk-text);
|
||||||
|
font-family: var(--pk-font-body);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stage {
|
||||||
|
position: relative;
|
||||||
|
width: 1080px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--pk-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-frame — panel chrome: a slim sidebar rail + top header bar around a
|
||||||
|
* content area. A stylized crop of the app (not a full screenshot), so the
|
||||||
|
* same fixed sidebar/topbar sizing works at both 1080x1920 and 1080x1080 —
|
||||||
|
* only the content area's available height changes with the stage.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-frame {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--pk-bg);
|
||||||
|
opacity: 0;
|
||||||
|
animation: pk-enter-fade 0.6s var(--pk-ease) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__sidebar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 168px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding-top: 32px;
|
||||||
|
background: var(--pk-surface);
|
||||||
|
border-right: 1px solid var(--pk-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--pk-radius-sm);
|
||||||
|
background: var(--pk-accent);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__navitem {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--pk-radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.pk-frame__navitem::before {
|
||||||
|
content: "";
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--pk-text-muted);
|
||||||
|
}
|
||||||
|
.pk-frame__navitem--active {
|
||||||
|
background: var(--pk-active-bg);
|
||||||
|
}
|
||||||
|
.pk-frame__navitem--active::before {
|
||||||
|
background: var(--pk-active-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__topbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 168px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 96px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 40px;
|
||||||
|
border-bottom: 1px solid var(--pk-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 360px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: var(--pk-radius-sm);
|
||||||
|
border: 1px solid var(--pk-border);
|
||||||
|
background: var(--pk-surface);
|
||||||
|
color: var(--pk-text-faint);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--pk-text-muted);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.pk-frame__statusdot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--pk-accent);
|
||||||
|
box-shadow: 0 0 12px var(--pk-accent);
|
||||||
|
animation: pk-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-frame__content {
|
||||||
|
position: absolute;
|
||||||
|
left: 168px;
|
||||||
|
right: 0;
|
||||||
|
top: 96px;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-intake — a text-input style surface for the typing reveal.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-intake {
|
||||||
|
position: absolute;
|
||||||
|
left: 56px;
|
||||||
|
right: 56px;
|
||||||
|
top: var(--pk-intake-top, 40px);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
animation: pk-enter-rise 0.6s var(--pk-ease) forwards;
|
||||||
|
}
|
||||||
|
.pk-intake__label {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--pk-text-faint);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.pk-intake__field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: var(--pk-radius-sm);
|
||||||
|
border: 1px solid var(--pk-border-strong);
|
||||||
|
background: var(--pk-surface);
|
||||||
|
font-family: var(--pk-font-body);
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--pk-text);
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-type — character-by-character typing reveal. kit.js splits the target
|
||||||
|
* element's text into one <span class="pk-type__char"> per character with a
|
||||||
|
* staggered animation-delay; this stylesheet only owns the reveal keyframe
|
||||||
|
* and the blinking caret, so it works with any font (no width bookkeeping).
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-type__char {
|
||||||
|
display: inline;
|
||||||
|
opacity: 0;
|
||||||
|
animation: pk-type-reveal 0.12s steps(1, end) forwards;
|
||||||
|
}
|
||||||
|
@keyframes pk-type-reveal {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-caret {
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 0.9em;
|
||||||
|
margin-left: 2px;
|
||||||
|
vertical-align: -0.1em;
|
||||||
|
background: var(--pk-accent);
|
||||||
|
animation: pk-caret-blink 1s steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes pk-caret-blink {
|
||||||
|
0%, 49% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-column — a kanban-ish list container to host cards.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-column {
|
||||||
|
position: absolute;
|
||||||
|
left: 56px;
|
||||||
|
width: 560px;
|
||||||
|
top: var(--pk-column-top, 220px);
|
||||||
|
}
|
||||||
|
.pk-column__header {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--pk-text-muted);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-card — task card: title, meta row (team/priority chips), status row.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-card {
|
||||||
|
border-radius: var(--pk-radius);
|
||||||
|
border: 1px solid var(--pk-border);
|
||||||
|
background: var(--pk-surface);
|
||||||
|
padding: 32px 36px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.97);
|
||||||
|
animation: pk-enter-rise-scale 0.5s var(--pk-ease) forwards;
|
||||||
|
}
|
||||||
|
.pk-card__title {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--pk-text);
|
||||||
|
}
|
||||||
|
.pk-card__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.pk-card__assignee {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--pk-text-muted);
|
||||||
|
}
|
||||||
|
.pk-card__status {
|
||||||
|
position: relative;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.pk-card__status .pk-pill {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-chip — small team/priority tag: soft bg + colored text, lifted from
|
||||||
|
* task-type-badge.tsx's dark-mode pairs.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.pk-chip--purple { background: #581c87; color: #d8b4fe; }
|
||||||
|
.pk-chip--blue { background: #1e3a8a; color: #93c5fd; }
|
||||||
|
.pk-chip--green { background: #14532d; color: #86efac; }
|
||||||
|
.pk-chip--orange { background: #7c2d12; color: #fdba74; }
|
||||||
|
.pk-chip--pink { background: #831843; color: #f9a8d4; }
|
||||||
|
.pk-chip--gray { background: #27272a; color: #d4d4d8; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-pill — status pill: solid bg + white text, fully rounded (matches the
|
||||||
|
* panel's shared Badge component, which is rounded-full for every variant).
|
||||||
|
* Text mirrors the panel's own rendering of a status enum: lowercase, spaces
|
||||||
|
* instead of underscores (see TaskStatusBadge: status.replace(/_/g," ")).
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pk-pill--progress { background: var(--pk-pill-progress); }
|
||||||
|
.pk-pill--review { background: var(--pk-pill-review); }
|
||||||
|
.pk-pill--approved { background: var(--pk-pill-approved); }
|
||||||
|
.pk-pill--completed { background: var(--pk-pill-completed); }
|
||||||
|
|
||||||
|
/* An enter+exit pair for a pill being swapped out (e.g. progress -> another
|
||||||
|
* status). Apply alongside a variant class; delays are set per-instance via
|
||||||
|
* --pk-pill-enter-delay / --pk-pill-exit-delay (composition-absolute
|
||||||
|
* seconds, same convention as every other animation-delay in this house). */
|
||||||
|
.pk-pill--swap-out {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
animation:
|
||||||
|
pk-enter-rise 0.4s var(--pk-ease) var(--pk-pill-enter-delay, 0s) forwards,
|
||||||
|
pk-fade-out 0.4s var(--pk-ease) var(--pk-pill-exit-delay, 1s) forwards;
|
||||||
|
}
|
||||||
|
.pk-pill--swap-in {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
animation: pk-enter-rise 0.4s var(--pk-ease) var(--pk-pill-enter-delay, 0s) forwards;
|
||||||
|
}
|
||||||
|
@keyframes pk-fade-out {
|
||||||
|
from { opacity: 1; transform: translateY(0); }
|
||||||
|
to { opacity: 0; transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-cursor — a small cursor sprite moved along a keyframed path (a pure CSS
|
||||||
|
* shape, no image asset), with a click-pulse ring. Start/end coordinates are
|
||||||
|
* content-box-relative pixels supplied per composition/orientation via
|
||||||
|
* --pk-cursor-x0/y0/x1/y1; the click delay via --pk-click-delay.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-cursor {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
transform: translate(var(--pk-cursor-x0, 0px), var(--pk-cursor-y0, 0px));
|
||||||
|
animation: pk-cursor-glide 1.2s var(--pk-ease) var(--pk-cursor-delay, 0s) forwards;
|
||||||
|
}
|
||||||
|
@keyframes pk-cursor-glide {
|
||||||
|
from { transform: translate(var(--pk-cursor-x0, 0px), var(--pk-cursor-y0, 0px)); }
|
||||||
|
to { transform: translate(var(--pk-cursor-x1, 0px), var(--pk-cursor-y1, 0px)); }
|
||||||
|
}
|
||||||
|
.pk-cursor__glyph {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
background: var(--pk-text);
|
||||||
|
clip-path: polygon(0 0, 0 70%, 22% 55%, 38% 88%, 52% 82%, 36% 50%, 62% 50%);
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
|
||||||
|
}
|
||||||
|
.pk-cursor__ring {
|
||||||
|
position: absolute;
|
||||||
|
left: -14px;
|
||||||
|
top: -14px;
|
||||||
|
width: 62px;
|
||||||
|
height: 62px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid var(--pk-accent);
|
||||||
|
opacity: 0;
|
||||||
|
animation: pk-cursor-click 0.6s var(--pk-ease) var(--pk-click-delay, 0s) forwards;
|
||||||
|
}
|
||||||
|
@keyframes pk-cursor-click {
|
||||||
|
0% { opacity: 0.9; transform: scale(0.4); }
|
||||||
|
60% { opacity: 0.55; }
|
||||||
|
100% { opacity: 0; transform: scale(1.6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-toast — notification toast, slides in from a corner (right-anchored;
|
||||||
|
* --pk-toast-bottom sets the vertical offset per orientation).
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-toast {
|
||||||
|
position: absolute;
|
||||||
|
right: 56px;
|
||||||
|
bottom: var(--pk-toast-bottom, 120px);
|
||||||
|
width: 560px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
border-radius: var(--pk-radius);
|
||||||
|
border: 1px solid var(--pk-border);
|
||||||
|
background: var(--pk-surface-raised);
|
||||||
|
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(40px);
|
||||||
|
animation: pk-enter-slide 0.6s var(--pk-ease) forwards;
|
||||||
|
}
|
||||||
|
.pk-toast__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--pk-pill-completed);
|
||||||
|
}
|
||||||
|
.pk-toast__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--pk-text);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.pk-toast__text {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--pk-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* pk-outro — a headline moment set in the display face, matching the
|
||||||
|
* release-announcement composition's outro treatment.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
.pk-outro {
|
||||||
|
position: absolute;
|
||||||
|
left: 56px;
|
||||||
|
bottom: var(--pk-outro-bottom, 56px);
|
||||||
|
font-family: var(--pk-font-display);
|
||||||
|
font-size: 56px;
|
||||||
|
color: var(--pk-text);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
animation: pk-enter-rise 0.8s var(--pk-ease) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- *
|
||||||
|
* Shared entrance keyframes.
|
||||||
|
* ---------------------------------------------------------------------- */
|
||||||
|
@keyframes pk-enter-fade {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes pk-enter-rise {
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes pk-enter-rise-scale {
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes pk-enter-slide {
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes pk-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.35; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Panel kit helpers — tiny, framework-free, run synchronously at load
|
||||||
|
// (no requestAnimationFrame, no timers). Loaded after props.js and before
|
||||||
|
// any inline script that reads window.PanelKit, same ordering convention as
|
||||||
|
// props.js itself (see motion/README.md).
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var DEFAULT_STAGGER = 0.045; // seconds between characters
|
||||||
|
|
||||||
|
// Splits `el`'s current text into one <span class="pk-type__char"> per
|
||||||
|
// character (whitespace preserved via CSS `white-space: pre` on the
|
||||||
|
// container) and gives each an absolute animation-delay so the reveal
|
||||||
|
// cascades left-to-right. `delay` is the composition-absolute second the
|
||||||
|
// first character should appear — the same convention every other
|
||||||
|
// animation-delay in this house uses (seconds from frame 0, not relative
|
||||||
|
// to the element). Deterministic: runs once, before any animation fires.
|
||||||
|
function typeText(el, options) {
|
||||||
|
if (!el) return;
|
||||||
|
var opts = options || {};
|
||||||
|
var delay = typeof opts.delay === "number" ? opts.delay : 0;
|
||||||
|
var stagger = typeof opts.stagger === "number" ? opts.stagger : DEFAULT_STAGGER;
|
||||||
|
var text = el.textContent || "";
|
||||||
|
|
||||||
|
el.textContent = "";
|
||||||
|
el.classList.add("pk-type");
|
||||||
|
for (var i = 0; i < text.length; i++) {
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.className = "pk-type__char";
|
||||||
|
span.textContent = text[i];
|
||||||
|
span.style.animationDelay = (delay + i * stagger).toFixed(3) + "s";
|
||||||
|
el.appendChild(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.PanelKit = { typeText: typeText };
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user