@waits/cadence 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MOTION.md +70 -0
- package/README.md +109 -0
- package/bin/cli.mjs +21 -0
- package/package.json +59 -0
- package/prompts/art/compose.ts +18 -0
- package/prompts/art/fantasy.ts +15 -0
- package/prompts/art/landmarks.ts +49 -0
- package/prompts/art/style.ts +20 -0
- package/public/backgrounds/barton-springs.png +0 -0
- package/public/backgrounds/capitol.png +0 -0
- package/public/backgrounds/congress.png +0 -0
- package/public/backgrounds/enchanted-rock.png +0 -0
- package/public/backgrounds/hamilton-pool.png +0 -0
- package/public/backgrounds/mount-bonnell.png +0 -0
- package/public/backgrounds/pennybacker.png +0 -0
- package/public/backgrounds/ut-tower.png +0 -0
- package/scripts/_pkg.ts +28 -0
- package/scripts/audit.ts +69 -0
- package/scripts/changes.ts +70 -0
- package/scripts/check-motion.ts +37 -0
- package/scripts/cli.ts +79 -0
- package/scripts/generate-art.ts +72 -0
- package/scripts/guide.ts +114 -0
- package/scripts/make.ts +76 -0
- package/scripts/redesign.ts +83 -0
- package/scripts/render.ts +62 -0
- package/scripts/smoke.ts +43 -0
- package/scripts/sync-skill.ts +43 -0
- package/scripts/theme.ts +97 -0
- package/src/Root.tsx +51 -0
- package/src/adapters/index.ts +72 -0
- package/src/adapters/parse.ts +65 -0
- package/src/adapters/types.ts +24 -0
- package/src/brand/fonts.ts +74 -0
- package/src/brand/tokens.ts +31 -0
- package/src/brand/tone.ts +25 -0
- package/src/code/highlight.ts +90 -0
- package/src/components/Background.tsx +75 -0
- package/src/components/Changelog.tsx +17 -0
- package/src/components/ChangelogScene.tsx +117 -0
- package/src/components/CodeWindow.tsx +106 -0
- package/src/components/ContactSheet.tsx +44 -0
- package/src/components/Headline.tsx +73 -0
- package/src/components/MotionReel.tsx +108 -0
- package/src/components/panels/DataTable.tsx +34 -0
- package/src/components/panels/Diagram.tsx +67 -0
- package/src/components/panels/Feed.tsx +46 -0
- package/src/components/panels/Fork.tsx +86 -0
- package/src/components/panels/PanelCard.tsx +44 -0
- package/src/components/panels/Proof.tsx +64 -0
- package/src/components/panels/Stat.tsx +28 -0
- package/src/components/panels/Status.tsx +41 -0
- package/src/components/panels/StreamResume.tsx +48 -0
- package/src/components/panels/UploadProgress.tsx +42 -0
- package/src/components/panels/index.tsx +34 -0
- package/src/content/clarinet-3.18.beats.ts +58 -0
- package/src/content/gradient-demo.beats.ts +29 -0
- package/src/content/index-decoded.beats.ts +93 -0
- package/src/content/mainnet-launch.beats.ts +70 -0
- package/src/content/panels.beats.ts +40 -0
- package/src/content/sdk-6.5-mempool.beats.ts +67 -0
- package/src/content/streams-launch.beats.ts +143 -0
- package/src/content/streams.beats.ts +46 -0
- package/src/index.ts +4 -0
- package/src/motion/names.ts +29 -0
- package/src/motion/presets.ts +67 -0
- package/src/motion/useMotion.ts +63 -0
- package/src/prepare.ts +33 -0
- package/src/schema/beats.ts +161 -0
- package/src/templates/changelog-reel.ts +28 -0
- package/src/templates/feature-launch.ts +26 -0
- package/src/templates/index.ts +13 -0
- package/src/templates/milestone.ts +25 -0
- package/src/templates/parts.ts +32 -0
- package/src/templates/types.ts +31 -0
- package/src/theme/default.ts +39 -0
- package/src/theme/derive.ts +119 -0
- package/src/theme/index.ts +33 -0
- package/src/theme/library.ts +60 -0
- package/src/theme/slate.ts +39 -0
- package/src/theme/types.ts +67 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { AbsoluteFill, interpolate, Sequence, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE } from "../brand/tokens";
|
|
3
|
+
import { FONTS } from "../brand/fonts";
|
|
4
|
+
import { isLightBackdrop } from "../brand/tone";
|
|
5
|
+
import type { Beat, Format } from "../schema/beats";
|
|
6
|
+
import { Background } from "./Background";
|
|
7
|
+
import { Headline } from "./Headline";
|
|
8
|
+
import { CodeWindow, codeTypingDoneFrame } from "./CodeWindow";
|
|
9
|
+
import { Panel } from "./panels";
|
|
10
|
+
|
|
11
|
+
/** Beat after the code finishes typing before the output panel "runs". */
|
|
12
|
+
const OUTPUT_GAP = 10;
|
|
13
|
+
|
|
14
|
+
const LAYOUT = {
|
|
15
|
+
"16x9": { top: "30%", dir: "row" as const, gap: 56, pad: "0 110px", codeFont: 24, codeMax: 820, panelW: 620, itemMax: 800 },
|
|
16
|
+
"1x1": { top: "29%", dir: "column" as const, gap: 22, pad: "0 6%", codeFont: 17, codeMax: 940, panelW: "100%", itemMax: 940 },
|
|
17
|
+
"9x16": { top: "23%", dir: "column" as const, gap: 30, pad: "0 6%", codeFont: 21, codeMax: 940, panelW: "100%", itemMax: 940 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One beat: painting backdrop + Field Notebook UI layer. Reflows by format —
|
|
22
|
+
* 16:9 lays code + panel side-by-side; square/vertical stack them in a column.
|
|
23
|
+
*/
|
|
24
|
+
export const ChangelogScene: React.FC<{ beat: Beat; format: Format }> = ({ beat, format }) => {
|
|
25
|
+
const frame = useCurrentFrame();
|
|
26
|
+
const isWide = format === "16x9";
|
|
27
|
+
const stack = !isWide || beat.layout === "center";
|
|
28
|
+
const L = LAYOUT[format];
|
|
29
|
+
const light = isLightBackdrop(beat.background);
|
|
30
|
+
const captionIn = interpolate(frame, [40, 58], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth });
|
|
31
|
+
|
|
32
|
+
// Sequential: the output panel waits for the code to finish "running".
|
|
33
|
+
const panelStart = beat.code ? codeTypingDoneFrame(beat.code.tokens ?? [], beat.code.motion) + OUTPUT_GAP : 0;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AbsoluteFill style={{ backgroundColor: COLORS.ink }}>
|
|
37
|
+
<Background bg={beat.background} />
|
|
38
|
+
<Headline eyebrow={beat.eyebrow} headline={beat.headline} motion={beat.headlineMotion} format={format} light={light} />
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
position: "absolute",
|
|
43
|
+
top: L.top,
|
|
44
|
+
left: 0,
|
|
45
|
+
right: 0,
|
|
46
|
+
bottom: "6%",
|
|
47
|
+
display: "flex",
|
|
48
|
+
flexDirection: stack ? "column" : L.dir,
|
|
49
|
+
alignItems: stack ? "center" : "flex-start",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
gap: L.gap,
|
|
52
|
+
padding: L.pad,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{beat.code && (
|
|
56
|
+
<div style={{ flex: stack ? "0 0 auto" : "1 1 0", width: stack ? "100%" : undefined, maxWidth: stack ? L.itemMax : L.codeMax }}>
|
|
57
|
+
<CodeWindow filename={beat.code.filename} tokens={beat.code.tokens ?? []} motion={beat.code.motion} fontSize={L.codeFont} />
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
{beat.panel && (
|
|
61
|
+
<div style={{ flex: "0 0 auto", width: stack ? "100%" : L.panelW, maxWidth: stack ? L.itemMax : 620 }}>
|
|
62
|
+
<Sequence from={panelStart} layout="none">
|
|
63
|
+
<Panel spec={beat.panel} />
|
|
64
|
+
</Sequence>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{(beat.caption || beat.badge) && (
|
|
70
|
+
<div
|
|
71
|
+
style={{
|
|
72
|
+
position: "absolute",
|
|
73
|
+
bottom: "7%",
|
|
74
|
+
left: 0,
|
|
75
|
+
right: 0,
|
|
76
|
+
display: "flex",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
justifyContent: "center",
|
|
79
|
+
gap: 14,
|
|
80
|
+
opacity: captionIn,
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{beat.badge && (
|
|
84
|
+
<span
|
|
85
|
+
style={{
|
|
86
|
+
fontFamily: FONTS.mono,
|
|
87
|
+
fontSize: isWide ? 14 : 12,
|
|
88
|
+
fontWeight: 600,
|
|
89
|
+
letterSpacing: "0.08em",
|
|
90
|
+
textTransform: "uppercase",
|
|
91
|
+
color: COLORS.gold,
|
|
92
|
+
background: COLORS.goldSoft,
|
|
93
|
+
padding: "4px 10px",
|
|
94
|
+
borderRadius: 999,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{beat.badge}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{beat.caption && (
|
|
101
|
+
<span
|
|
102
|
+
style={{
|
|
103
|
+
fontFamily: FONTS.body,
|
|
104
|
+
fontSize: isWide ? 21 : 18,
|
|
105
|
+
color: light ? COLORS.textMuted : COLORS.titleWhite,
|
|
106
|
+
opacity: light ? 1 : 0.86,
|
|
107
|
+
textShadow: light ? "none" : "0 1px 14px rgba(30,41,59,0.5)",
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{beat.caption}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</AbsoluteFill>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useCurrentFrame } from "remotion";
|
|
2
|
+
import { CARET_BG, COLORS, FLOAT_SHADOW } from "../brand/tokens";
|
|
3
|
+
import { FONTS } from "../brand/fonts";
|
|
4
|
+
import { useMotion, type MotionSpec } from "../motion/useMotion";
|
|
5
|
+
import { CODE_BG, type CodeLine } from "../code/highlight";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
filename: string;
|
|
9
|
+
tokens: CodeLine[];
|
|
10
|
+
motion?: MotionSpec;
|
|
11
|
+
fontSize?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const CHARS_PER_FRAME = 2.6;
|
|
15
|
+
const typeStartFor = (motion?: MotionSpec) => (motion?.delay ?? 12) + 6;
|
|
16
|
+
const totalChars = (tokens: CodeLine[]) =>
|
|
17
|
+
tokens.reduce((n, line) => n + line.reduce((m, t) => m + t.content.length, 0) + 1, 0);
|
|
18
|
+
|
|
19
|
+
/** Frame at which the typewriter finishes — the output panel waits for this. */
|
|
20
|
+
export const codeTypingDoneFrame = (tokens: CodeLine[], motion?: MotionSpec) =>
|
|
21
|
+
typeStartFor(motion) + Math.ceil(totalChars(tokens) / CHARS_PER_FRAME);
|
|
22
|
+
|
|
23
|
+
/** Floating code window with a single-caret typewriter over pre-tokenized code. */
|
|
24
|
+
export const CodeWindow: React.FC<Props> = ({ filename, tokens, motion = { enter: "settle", delay: 12 }, fontSize = 22 }) => {
|
|
25
|
+
const frame = useCurrentFrame();
|
|
26
|
+
const style = useMotion(motion);
|
|
27
|
+
|
|
28
|
+
const typeStart = typeStartFor(motion);
|
|
29
|
+
const total = totalChars(tokens);
|
|
30
|
+
const revealed = Math.min(total, Math.max(0, Math.round((frame - typeStart) * CHARS_PER_FRAME)));
|
|
31
|
+
const typing = revealed < total; // caret only while typing; gone once "run"
|
|
32
|
+
const caretOn = Math.floor(frame / 8) % 2 === 0;
|
|
33
|
+
|
|
34
|
+
let budget = revealed;
|
|
35
|
+
let caretPlaced = false; // exactly one caret, at the typing head
|
|
36
|
+
const lineHeight = Math.round(fontSize * 1.7);
|
|
37
|
+
|
|
38
|
+
const placeCaret = () => {
|
|
39
|
+
if (typing && !caretPlaced) {
|
|
40
|
+
caretPlaced = true;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const lines = tokens.map((line, li) => {
|
|
47
|
+
const out: React.ReactNode[] = [];
|
|
48
|
+
let caretHere = false;
|
|
49
|
+
for (let ti = 0; ti < line.length; ti++) {
|
|
50
|
+
const tok = line[ti];
|
|
51
|
+
if (budget <= 0) { caretHere = placeCaret(); break; }
|
|
52
|
+
const take = Math.min(tok.content.length, budget);
|
|
53
|
+
out.push(<span key={ti} style={{ color: tok.color }}>{tok.content.slice(0, take)}</span>);
|
|
54
|
+
budget -= take;
|
|
55
|
+
if (take < tok.content.length) { caretHere = placeCaret(); break; }
|
|
56
|
+
}
|
|
57
|
+
if (budget > 0) budget -= 1; // newline cost
|
|
58
|
+
else if (line.length === 0) caretHere = caretHere || placeCaret();
|
|
59
|
+
return (
|
|
60
|
+
<div key={li} style={{ minHeight: lineHeight, whiteSpace: "pre" }}>
|
|
61
|
+
{out}
|
|
62
|
+
{caretHere && (
|
|
63
|
+
<span
|
|
64
|
+
style={{
|
|
65
|
+
display: "inline-block",
|
|
66
|
+
width: fontSize * 0.5,
|
|
67
|
+
height: fontSize,
|
|
68
|
+
transform: "translateY(3px)",
|
|
69
|
+
background: CARET_BG,
|
|
70
|
+
boxShadow: "0 0 0 1.5px rgba(110,92,60,0.45)",
|
|
71
|
+
borderRadius: 2,
|
|
72
|
+
opacity: caretOn ? 1 : 0,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
width: "100%",
|
|
84
|
+
borderRadius: 20,
|
|
85
|
+
background: `${CODE_BG}f7`,
|
|
86
|
+
boxShadow: FLOAT_SHADOW,
|
|
87
|
+
backdropFilter: "blur(6px)",
|
|
88
|
+
overflow: "hidden",
|
|
89
|
+
border: "1px solid rgba(255,255,255,0.55)",
|
|
90
|
+
...style,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<div style={{ height: 54, display: "flex", alignItems: "center", padding: "0 22px", gap: 9 }}>
|
|
94
|
+
<Dot color="#f6645f" /><Dot color="#f7bd45" /><Dot color="#2fc94e" />
|
|
95
|
+
<span style={{ flex: 1, textAlign: "center", color: "rgba(0,0,0,0.34)", fontSize: 15, fontFamily: FONTS.mono, marginRight: 60 }}>{filename}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div style={{ padding: "26px 40px 42px", fontFamily: FONTS.mono, fontSize, lineHeight: `${lineHeight}px`, color: COLORS.ink }}>
|
|
98
|
+
{lines}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const Dot: React.FC<{ color: string }> = ({ color }) => (
|
|
105
|
+
<span style={{ width: 13, height: 13, borderRadius: "50%", background: color, display: "inline-block" }} />
|
|
106
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { AbsoluteFill, Img, continueRender, delayRender, staticFile } from "remotion";
|
|
3
|
+
import { COLORS, RADIUS } from "../brand/tokens";
|
|
4
|
+
import { FONTS } from "../brand/fonts";
|
|
5
|
+
|
|
6
|
+
type Entry = { file: string; landmark: string; level: string; format: string; name: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Review board for generated paintings. Reads the candidates manifest written by
|
|
10
|
+
* `bun run art` and lays them out in a labeled grid. Render with
|
|
11
|
+
* `remotion still ContactSheet out/contact.png` to pick winners, then move the
|
|
12
|
+
* chosen files from _candidates/ to committed public/backgrounds/.
|
|
13
|
+
*/
|
|
14
|
+
export const ContactSheet: React.FC = () => {
|
|
15
|
+
const [handle] = useState(() => delayRender("manifest"));
|
|
16
|
+
const [items, setItems] = useState<Entry[]>([]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
fetch(staticFile("backgrounds/_candidates/manifest.json"))
|
|
20
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
21
|
+
.then((d: Entry[]) => { setItems(d); continueRender(handle); })
|
|
22
|
+
.catch(() => continueRender(handle));
|
|
23
|
+
}, [handle]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<AbsoluteFill style={{ background: COLORS.paper, padding: 64, fontFamily: FONTS.body }}>
|
|
27
|
+
<div style={{ fontFamily: FONTS.display, fontSize: 44, fontWeight: 600, letterSpacing: "-0.02em", color: COLORS.ink }}>Hill Country Sublime</div>
|
|
28
|
+
<div style={{ fontFamily: FONTS.note, fontSize: 26, color: COLORS.signalBlue, marginBottom: 32 }}>
|
|
29
|
+
{items.length ? `${items.length} candidates — pick winners → public/backgrounds/` : "no candidates yet — run `bun run art --all`"}
|
|
30
|
+
</div>
|
|
31
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 24 }}>
|
|
32
|
+
{items.map((it) => (
|
|
33
|
+
<div key={it.name} style={{ borderRadius: RADIUS.lg, overflow: "hidden", border: `1px solid ${COLORS.hairline}`, background: COLORS.paperElevated }}>
|
|
34
|
+
<Img src={staticFile(it.file)} style={{ width: "100%", aspectRatio: "16 / 9", objectFit: "cover", display: "block" }} />
|
|
35
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "12px 16px" }}>
|
|
36
|
+
<span style={{ fontSize: 18, fontWeight: 600, color: COLORS.ink }}>{it.landmark}</span>
|
|
37
|
+
<span style={{ fontFamily: FONTS.mono, fontSize: 12, textTransform: "uppercase", letterSpacing: "0.06em", color: COLORS.textMuted }}>{it.level} · {it.format}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</AbsoluteFill>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE } from "../brand/tokens";
|
|
3
|
+
import { FONTS } from "../brand/fonts";
|
|
4
|
+
import { useMotion, type MotionSpec } from "../motion/useMotion";
|
|
5
|
+
import type { Format } from "../schema/beats";
|
|
6
|
+
|
|
7
|
+
// Per-format type scale. Eyebrow is a small tracked uppercase gold label (our
|
|
8
|
+
// signature mono label = the reference's "NEW IN 1.7"); headline is a heavy,
|
|
9
|
+
// tight Sora grotesk in warm near-white with a crisp-plus-soft shadow.
|
|
10
|
+
const H = {
|
|
11
|
+
"16x9": { top: "13%", size: 72, eyebrow: 15, track: 0.18, max: "70%" },
|
|
12
|
+
"1x1": { top: "6%", size: 44, eyebrow: 13, track: 0.16, max: "86%" },
|
|
13
|
+
"9x16": { top: "8%", size: 52, eyebrow: 14, track: 0.16, max: "86%" },
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const HEADLINE_SHADOW = "0 1px 2px rgba(30,41,59,0.24), 0 6px 34px rgba(30,41,59,0.42)";
|
|
17
|
+
// On a light backdrop, a soft white halo lifts the dark headline off the field.
|
|
18
|
+
const HEADLINE_SHADOW_LIGHT = "0 1px 2px rgba(255,255,255,0.6)";
|
|
19
|
+
|
|
20
|
+
export const Headline: React.FC<{ eyebrow?: string; headline: string; motion?: MotionSpec; format?: Format; light?: boolean }> = ({
|
|
21
|
+
eyebrow,
|
|
22
|
+
headline,
|
|
23
|
+
motion = { enter: "rise", delay: 8 },
|
|
24
|
+
format = "16x9",
|
|
25
|
+
light = false,
|
|
26
|
+
}) => {
|
|
27
|
+
const frame = useCurrentFrame();
|
|
28
|
+
const style = useMotion(motion);
|
|
29
|
+
const m = H[format];
|
|
30
|
+
|
|
31
|
+
const eyebrowIn = interpolate(frame, [14, 28], [0, 1], {
|
|
32
|
+
extrapolateLeft: "clamp",
|
|
33
|
+
extrapolateRight: "clamp",
|
|
34
|
+
easing: EASE.smooth,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ position: "absolute", top: m.top, left: 0, right: 0, display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", ...style }}>
|
|
39
|
+
{eyebrow && (
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
fontFamily: FONTS.mono,
|
|
43
|
+
fontSize: m.eyebrow,
|
|
44
|
+
fontWeight: 600,
|
|
45
|
+
letterSpacing: `${m.track}em`,
|
|
46
|
+
textTransform: "uppercase",
|
|
47
|
+
color: COLORS.gold,
|
|
48
|
+
opacity: eyebrowIn,
|
|
49
|
+
marginBottom: 16,
|
|
50
|
+
textShadow: light ? "none" : "0 1px 10px rgba(20,24,33,0.35)",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{eyebrow}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
fontFamily: FONTS.display,
|
|
59
|
+
fontSize: m.size,
|
|
60
|
+
fontWeight: 700,
|
|
61
|
+
letterSpacing: "-0.025em",
|
|
62
|
+
lineHeight: 1.0,
|
|
63
|
+
color: light ? COLORS.ink : COLORS.titleWhite,
|
|
64
|
+
textShadow: light ? HEADLINE_SHADOW_LIGHT : HEADLINE_SHADOW,
|
|
65
|
+
maxWidth: m.max,
|
|
66
|
+
textWrap: "balance",
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{headline}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE, FLOAT_SHADOW, RADIUS } from "../brand/tokens";
|
|
3
|
+
import { FONTS } from "../brand/fonts";
|
|
4
|
+
import { ENTER_PRESETS, type EnterPreset } from "../motion/names";
|
|
5
|
+
import { countValue, drawDashoffset, enterStyle, staggerDelay, typewriterChars } from "../motion/presets";
|
|
6
|
+
|
|
7
|
+
const LOOP = 90; // each preset replays every 3s
|
|
8
|
+
const DUR = 22; // entrance length in frames
|
|
9
|
+
|
|
10
|
+
const NOTES: Record<EnterPreset, string> = {
|
|
11
|
+
rise: "headlines",
|
|
12
|
+
settle: "windows + panels",
|
|
13
|
+
bloom: "backgrounds",
|
|
14
|
+
type: "code + terminal",
|
|
15
|
+
stagger: "list rows",
|
|
16
|
+
draw: "diagrams + badge",
|
|
17
|
+
count: "metrics",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Pure eased 0→1 progress within the current loop (no hooks). */
|
|
21
|
+
const reveal = (frame: number, delay = 0, dur = DUR) =>
|
|
22
|
+
interpolate(frame % LOOP, [delay, delay + dur], [0, 1], {
|
|
23
|
+
extrapolateLeft: "clamp",
|
|
24
|
+
extrapolateRight: "clamp",
|
|
25
|
+
easing: EASE.smooth,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const Tile: React.FC<{ name: EnterPreset; children: React.ReactNode }> = ({ name, children }) => (
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
gap: 16,
|
|
34
|
+
padding: 28,
|
|
35
|
+
borderRadius: RADIUS.lg,
|
|
36
|
+
background: COLORS.paperElevated,
|
|
37
|
+
border: `1px solid ${COLORS.hairline}`,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<div style={{ height: 120, display: "flex", alignItems: "center", justifyContent: "center" }}>{children}</div>
|
|
41
|
+
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between" }}>
|
|
42
|
+
<span style={{ fontFamily: FONTS.mono, fontSize: 18, fontWeight: 600, letterSpacing: "0.05em", textTransform: "uppercase", color: COLORS.ink }}>{name}</span>
|
|
43
|
+
<span style={{ fontFamily: FONTS.note, fontSize: 22, color: COLORS.textMuted }}>{NOTES[name]}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** A small mock "window" used by the transform presets. */
|
|
49
|
+
const MockCard: React.FC<{ style: React.CSSProperties }> = ({ style }) => (
|
|
50
|
+
<div style={{ width: 180, height: 96, borderRadius: RADIUS.md, background: COLORS.paper, border: `1px solid ${COLORS.hairline}`, boxShadow: FLOAT_SHADOW, ...style }}>
|
|
51
|
+
<div style={{ height: 22, borderBottom: `1px solid ${COLORS.hairline}`, display: "flex", gap: 5, alignItems: "center", paddingLeft: 9 }}>
|
|
52
|
+
{["#f87171", "#fbbf24", "#34d399"].map((c) => (<span key={c} style={{ width: 7, height: 7, borderRadius: "50%", background: c }} />))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const Demo: React.FC<{ name: EnterPreset }> = ({ name }) => {
|
|
58
|
+
const frame = useCurrentFrame();
|
|
59
|
+
const p = reveal(frame);
|
|
60
|
+
switch (name) {
|
|
61
|
+
case "rise":
|
|
62
|
+
case "settle":
|
|
63
|
+
case "bloom":
|
|
64
|
+
return <MockCard style={enterStyle(name, p)} />;
|
|
65
|
+
case "stagger":
|
|
66
|
+
return (
|
|
67
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, width: 200 }}>
|
|
68
|
+
{[0, 1, 2].map((i) => (
|
|
69
|
+
<div key={i} style={{ height: 24, borderRadius: RADIUS.sm, background: COLORS.signalBlueSoft, border: `1px solid ${COLORS.signalBlueBorder}`, ...enterStyle("stagger", reveal(frame, staggerDelay(i, 6))) }} />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
case "type": {
|
|
74
|
+
const src = "sl.index.events()";
|
|
75
|
+
return <span style={{ fontFamily: FONTS.mono, fontSize: 22, color: COLORS.ink, whiteSpace: "pre" }}>{src.slice(0, typewriterChars(p, src.length))}<Caret /></span>;
|
|
76
|
+
}
|
|
77
|
+
case "draw": {
|
|
78
|
+
const C = 2 * Math.PI * 34;
|
|
79
|
+
return (
|
|
80
|
+
<svg width={120} height={120} viewBox="0 0 120 120">
|
|
81
|
+
<circle cx={60} cy={60} r={34} fill="none" stroke={COLORS.markerPink} strokeWidth={4} strokeLinecap="round" strokeDasharray={C} strokeDashoffset={drawDashoffset(p, C)} transform="rotate(-90 60 60)" />
|
|
82
|
+
<text x={60} y={68} textAnchor="middle" fontFamily={FONTS.note} fontSize={28} fill={COLORS.markerPink} opacity={p}>NEW</text>
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
case "count":
|
|
87
|
+
return <span style={{ fontFamily: FONTS.mono, fontSize: 46, fontWeight: 600, color: COLORS.ink, fontVariantNumeric: "tabular-nums" }}>{Math.round(countValue(p, 9000)).toLocaleString()}</span>;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const Caret: React.FC = () => {
|
|
92
|
+
const frame = useCurrentFrame();
|
|
93
|
+
return <span style={{ display: "inline-block", width: 10, height: 22, transform: "translateY(4px)", background: COLORS.ink, opacity: Math.floor(frame / 8) % 2 === 0 ? 1 : 0 }} />;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const MotionReel: React.FC = () => {
|
|
97
|
+
return (
|
|
98
|
+
<AbsoluteFill style={{ background: COLORS.paper, backgroundImage: `radial-gradient(${COLORS.hairline} 1px, transparent 1px)`, backgroundSize: "30px 30px", padding: 72 }}>
|
|
99
|
+
<div style={{ fontFamily: FONTS.display, fontSize: 46, fontWeight: 600, letterSpacing: "-0.02em", color: COLORS.ink, marginBottom: 8 }}>Motion vocabulary</div>
|
|
100
|
+
<div style={{ fontFamily: FONTS.note, fontSize: 26, color: COLORS.signalBlue, marginBottom: 36 }}>ease-out only — smooth enters, snappy pops</div>
|
|
101
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 24 }}>
|
|
102
|
+
{ENTER_PRESETS.map((name) => (
|
|
103
|
+
<Tile key={name} name={name}><Demo name={name} /></Tile>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
</AbsoluteFill>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE } from "../../brand/tokens";
|
|
3
|
+
import { FONTS } from "../../brand/fonts";
|
|
4
|
+
import { enterStyle, staggerDelay } from "../../motion/presets";
|
|
5
|
+
import type { PanelSpec } from "../../schema/beats";
|
|
6
|
+
import { PanelCard } from "./PanelCard";
|
|
7
|
+
|
|
8
|
+
type Spec = Extract<PanelSpec, { kind: "data-table" }>;
|
|
9
|
+
|
|
10
|
+
const ROW_START = 24;
|
|
11
|
+
|
|
12
|
+
export const DataTablePanel: React.FC<{ spec: Spec }> = ({ spec }) => {
|
|
13
|
+
const frame = useCurrentFrame();
|
|
14
|
+
return (
|
|
15
|
+
<PanelCard motion={spec.motion}>
|
|
16
|
+
<div style={{ padding: "22px 26px" }}>
|
|
17
|
+
{spec.title && <div style={{ fontSize: 18, fontWeight: 600, color: COLORS.ink, marginBottom: 16 }}>{spec.title}</div>}
|
|
18
|
+
<div style={{ display: "grid", gridTemplateColumns: `repeat(${spec.columns.length}, 1fr)`, columnGap: 18 }}>
|
|
19
|
+
{spec.columns.map((c) => (
|
|
20
|
+
<div key={c} style={{ fontFamily: FONTS.mono, fontSize: 13, textTransform: "uppercase", letterSpacing: "0.06em", color: COLORS.textMuted, paddingBottom: 12, borderBottom: `1px solid ${COLORS.hairline}` }}>{c}</div>
|
|
21
|
+
))}
|
|
22
|
+
{spec.rows.map((row, ri) => {
|
|
23
|
+
const start = ROW_START + staggerDelay(ri, 8);
|
|
24
|
+
const p = interpolate(frame, [start, start + 14], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth });
|
|
25
|
+
const st = enterStyle("stagger", p);
|
|
26
|
+
return row.map((cell, ci) => (
|
|
27
|
+
<div key={`${ri}-${ci}`} style={{ fontFamily: FONTS.mono, fontSize: 18, color: ci === 0 ? COLORS.ink : "rgba(0,0,0,0.7)", padding: "14px 0", borderBottom: `1px solid ${COLORS.hairline}`, fontVariantNumeric: "tabular-nums", ...st }}>{cell}</div>
|
|
28
|
+
));
|
|
29
|
+
})}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</PanelCard>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE } from "../../brand/tokens";
|
|
3
|
+
import { FONTS } from "../../brand/fonts";
|
|
4
|
+
import { drawDashoffset } from "../../motion/presets";
|
|
5
|
+
import type { PanelSpec } from "../../schema/beats";
|
|
6
|
+
import { PanelCard } from "./PanelCard";
|
|
7
|
+
|
|
8
|
+
type Spec = Extract<PanelSpec, { kind: "diagram" }>;
|
|
9
|
+
|
|
10
|
+
const W = 620;
|
|
11
|
+
const H = 240;
|
|
12
|
+
const NW = 132;
|
|
13
|
+
const NH = 62;
|
|
14
|
+
|
|
15
|
+
const NODE_FILL = { default: COLORS.chrome, data: COLORS.signalBlueSoft, api: COLORS.signalBlue } as const;
|
|
16
|
+
const NODE_STROKE = { default: COLORS.hairline, data: COLORS.signalBlueBorder, api: COLORS.signalBlue } as const;
|
|
17
|
+
const NODE_TEXT = { default: COLORS.ink, data: COLORS.signalBlue, api: COLORS.paper } as const;
|
|
18
|
+
|
|
19
|
+
export const DiagramPanel: React.FC<{ spec: Spec }> = ({ spec }) => {
|
|
20
|
+
const frame = useCurrentFrame();
|
|
21
|
+
const n = spec.nodes.length;
|
|
22
|
+
const gap = (W - NW * n) / (n + 1);
|
|
23
|
+
const cy = H / 2;
|
|
24
|
+
const pos = new Map(spec.nodes.map((node, i) => [node.id, { x: gap + i * (NW + gap), y: cy - NH / 2 }]));
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<PanelCard motion={spec.motion} style={{ background: "rgba(252,251,247,0.95)" }}>
|
|
28
|
+
<svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ display: "block", padding: "8px" }}>
|
|
29
|
+
<defs>
|
|
30
|
+
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
|
|
31
|
+
<path d="M0,0 L7,3 L0,6 Z" fill={COLORS.textMuted} />
|
|
32
|
+
</marker>
|
|
33
|
+
</defs>
|
|
34
|
+
{spec.edges.map((e, i) => {
|
|
35
|
+
const a = pos.get(e.from)!;
|
|
36
|
+
const b = pos.get(e.to)!;
|
|
37
|
+
const x1 = a.x + NW, y1 = a.y + NH / 2, x2 = b.x - 4, y2 = b.y + NH / 2;
|
|
38
|
+
const len = Math.hypot(x2 - x1, y2 - y1);
|
|
39
|
+
const start = 16 + i * 8;
|
|
40
|
+
const p = interpolate(frame, [start, start + 18], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth });
|
|
41
|
+
return (
|
|
42
|
+
<g key={i}>
|
|
43
|
+
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke={COLORS.textMuted} strokeWidth={1.5} markerEnd="url(#arrow)" strokeDasharray={len} strokeDashoffset={drawDashoffset(p, len)} />
|
|
44
|
+
{e.label && p > 0.6 && (
|
|
45
|
+
<text x={(x1 + x2) / 2} y={(y1 + y2) / 2 - 8} textAnchor="middle" fontFamily={FONTS.mono} fontSize={11} fill={COLORS.textMuted} opacity={(p - 0.6) / 0.4}>{e.label}</text>
|
|
46
|
+
)}
|
|
47
|
+
</g>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
{spec.nodes.map((node, i) => {
|
|
51
|
+
const pp = pos.get(node.id)!;
|
|
52
|
+
const start = 10 + i * 7;
|
|
53
|
+
const p = interpolate(frame, [start, start + 14], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth });
|
|
54
|
+
return (
|
|
55
|
+
<g key={node.id} opacity={p} transform={`translate(0 ${(1 - p) * 8})`}>
|
|
56
|
+
<rect x={pp.x} y={pp.y} width={NW} height={NH} rx={8} fill={NODE_FILL[node.type]} stroke={NODE_STROKE[node.type]} strokeWidth={1.5} />
|
|
57
|
+
<text x={pp.x + NW / 2} y={pp.y + NH / 2 + 5} textAnchor="middle" fontFamily={FONTS.display} fontSize={15} fontWeight={600} fill={NODE_TEXT[node.type]}>{node.label}</text>
|
|
58
|
+
</g>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</svg>
|
|
62
|
+
{spec.note && (
|
|
63
|
+
<div style={{ textAlign: "right", padding: "0 22px 16px", fontFamily: FONTS.note, fontSize: 22, color: COLORS.signalBlue }}>{spec.note}</div>
|
|
64
|
+
)}
|
|
65
|
+
</PanelCard>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
2
|
+
import { COLORS, EASE, RADIUS } from "../../brand/tokens";
|
|
3
|
+
import { FONTS } from "../../brand/fonts";
|
|
4
|
+
import { enterStyle, staggerDelay } from "../../motion/presets";
|
|
5
|
+
import type { PanelSpec } from "../../schema/beats";
|
|
6
|
+
import { PanelCard, PanelHeader } from "./PanelCard";
|
|
7
|
+
|
|
8
|
+
type Spec = Extract<PanelSpec, { kind: "feed" }>;
|
|
9
|
+
|
|
10
|
+
const ROW_START = 26;
|
|
11
|
+
const ROW_STAGGER = 11;
|
|
12
|
+
const ROW_DUR = 16;
|
|
13
|
+
|
|
14
|
+
export const FeedPanel: React.FC<{ spec: Spec }> = ({ spec }) => {
|
|
15
|
+
const frame = useCurrentFrame();
|
|
16
|
+
const pulse = 0.5 + 0.5 * Math.sin(frame / 6);
|
|
17
|
+
return (
|
|
18
|
+
<PanelCard motion={spec.motion}>
|
|
19
|
+
<PanelHeader>
|
|
20
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
21
|
+
<span style={{ color: COLORS.ink, fontSize: 19, fontWeight: 600 }}>{spec.title}</span>
|
|
22
|
+
{spec.subtitle && <span style={{ color: COLORS.textMuted, fontSize: 17 }}>{spec.subtitle}</span>}
|
|
23
|
+
</div>
|
|
24
|
+
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
|
|
25
|
+
<span style={{ width: 10, height: 10, borderRadius: "50%", background: COLORS.successGreen, opacity: 0.4 + 0.6 * pulse }} />
|
|
26
|
+
<span style={{ color: "#16a34a", fontSize: 18, fontFamily: FONTS.note }}>{spec.status}</span>
|
|
27
|
+
</div>
|
|
28
|
+
</PanelHeader>
|
|
29
|
+
<div style={{ padding: "10px 12px 16px" }}>
|
|
30
|
+
{spec.rows.map((row, i) => {
|
|
31
|
+
const start = ROW_START + staggerDelay(i, ROW_STAGGER);
|
|
32
|
+
const p = interpolate(frame, [start, start + ROW_DUR], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth });
|
|
33
|
+
return (
|
|
34
|
+
<div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "15px 18px", margin: "4px 0", borderRadius: RADIUS.md, background: i % 2 === 0 ? "rgba(0,0,0,0.035)" : "transparent", ...enterStyle("stagger", p) }}>
|
|
35
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
|
36
|
+
<span style={{ fontSize: 13, fontWeight: 700, color: COLORS.signalBlue, background: COLORS.signalBlueSoft, padding: "4px 9px", borderRadius: RADIUS.sm + 4, letterSpacing: 0.3 }}>{row.badge}</span>
|
|
37
|
+
<span style={{ color: "rgba(0,0,0,0.7)", fontSize: 19 }}>→ {row.label}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<span style={{ color: COLORS.ink, fontSize: 19, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{row.value}</span>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</div>
|
|
44
|
+
</PanelCard>
|
|
45
|
+
);
|
|
46
|
+
};
|