@devinilabs/reelstack 1.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 +128 -0
- package/README.md +125 -0
- package/cli/beats.js +124 -0
- package/cli/bootstrap.js +124 -0
- package/cli/capture.js +34 -0
- package/cli/direction.js +114 -0
- package/cli/icons.js +49 -0
- package/cli/index.js +101 -0
- package/cli/init.js +253 -0
- package/cli/license.js +168 -0
- package/cli/lint.js +865 -0
- package/cli/preview.js +59 -0
- package/cli/render.js +404 -0
- package/cli/scaffold.js +239 -0
- package/cli/smoke.js +76 -0
- package/cli/update.js +26 -0
- package/cli/utils.js +184 -0
- package/docs/buyers-guide.md +220 -0
- package/docs/design-discipline.md +130 -0
- package/docs/family-galleries/dark.md +95 -0
- package/docs/family-galleries/forbidden.md +78 -0
- package/docs/family-galleries/glass.md +98 -0
- package/docs/family-galleries/paper.md +82 -0
- package/docs/family-galleries/warm.md +86 -0
- package/docs/superpowers/plans/2026-05-09-reelstack-init-readiness-gate.md +1166 -0
- package/docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md +233 -0
- package/families/dark/components/DriftingSpotlights.tsx +59 -0
- package/families/dark/components/FilmGrain.tsx +44 -0
- package/families/dark/components/ForestCard.tsx +43 -0
- package/families/dark/components/GridBackground.tsx +29 -0
- package/families/dark/components/RadialVignette.tsx +21 -0
- package/families/dark/components/Scanlines.tsx +35 -0
- package/families/dark/components/SegmentOpacity.ts +37 -0
- package/families/dark/components/index.ts +13 -0
- package/families/dark/index.ts +31 -0
- package/families/dark/palette.ts +98 -0
- package/families/dark/presets/claudedispatch.ts +46 -0
- package/families/dark/presets/codedrop.ts +37 -0
- package/families/dark/presets/gpt55.ts +54 -0
- package/families/dark/presets/notebooklm.ts +50 -0
- package/families/dark/presets/resourcescta.ts +35 -0
- package/families/dark/presets/skills.ts +40 -0
- package/families/dark/presets/stitch.ts +46 -0
- package/families/dark/presets/stitch2.ts +43 -0
- package/families/dark/typography.ts +16 -0
- package/families/forbidden/components/ForbiddenCausticBlobs.tsx +52 -0
- package/families/forbidden/components/NewsprintTexture.tsx +28 -0
- package/families/forbidden/components/TintedShadow.tsx +36 -0
- package/families/forbidden/components/index.ts +38 -0
- package/families/forbidden/index.ts +17 -0
- package/families/forbidden/palette.ts +88 -0
- package/families/forbidden/presets/heretic.ts +44 -0
- package/families/forbidden/typography.ts +18 -0
- package/families/glass/components/BreakdownCard.tsx +158 -0
- package/families/glass/components/CausticBlobs.tsx +49 -0
- package/families/glass/components/Counter.tsx +72 -0
- package/families/glass/components/EyebrowPill.tsx +59 -0
- package/families/glass/components/FilmStrip.tsx +202 -0
- package/families/glass/components/FloatingGlyphs.tsx +78 -0
- package/families/glass/components/GlassCard.tsx +58 -0
- package/families/glass/components/GlassCardBezel.tsx +45 -0
- package/families/glass/components/HairlineGrid.tsx +30 -0
- package/families/glass/components/IridescentRing.tsx +114 -0
- package/families/glass/components/IridescentText.tsx +98 -0
- package/families/glass/components/LightBeam.tsx +46 -0
- package/families/glass/components/ParticleBurst.tsx +62 -0
- package/families/glass/components/SonarRings.tsx +81 -0
- package/families/glass/components/StaggeredWords.tsx +74 -0
- package/families/glass/components/index.ts +20 -0
- package/families/glass/index.ts +31 -0
- package/families/glass/palette.ts +93 -0
- package/families/glass/presets/claudewatch.ts +64 -0
- package/families/glass/presets/claudewatchcta.ts +43 -0
- package/families/glass/presets/graphify.ts +45 -0
- package/families/glass/presets/gstack.ts +48 -0
- package/families/glass/presets/jcode.ts +50 -0
- package/families/glass/presets/lilagents.ts +52 -0
- package/families/glass/presets/paperclip.ts +43 -0
- package/families/glass/typography.ts +15 -0
- package/families/index.ts +49 -0
- package/families/paper/components/CardSpring.tsx +42 -0
- package/families/paper/components/CreamGrid.tsx +26 -0
- package/families/paper/components/EditorialSerifText.tsx +51 -0
- package/families/paper/components/GreenAccentCard.tsx +10 -0
- package/families/paper/components/PaperShadow.tsx +30 -0
- package/families/paper/components/ScaleBlurText.tsx +40 -0
- package/families/paper/components/index.ts +11 -0
- package/families/paper/index.ts +23 -0
- package/families/paper/palette.ts +102 -0
- package/families/paper/presets/designreel.ts +32 -0
- package/families/paper/presets/devini3d.ts +45 -0
- package/families/paper/presets/justdrop.ts +39 -0
- package/families/paper/presets/opus.ts +48 -0
- package/families/paper/typography.ts +17 -0
- package/families/warm/components/AccentGlow.tsx +60 -0
- package/families/warm/components/BentoCell.tsx +56 -0
- package/families/warm/components/BentoGrid.tsx +30 -0
- package/families/warm/components/FilmGrain.tsx +36 -0
- package/families/warm/components/ScaleBlurCounter.tsx +71 -0
- package/families/warm/components/WarmSurface.tsx +35 -0
- package/families/warm/components/index.ts +11 -0
- package/families/warm/index.ts +19 -0
- package/families/warm/palette.ts +81 -0
- package/families/warm/presets/huashu.ts +49 -0
- package/families/warm/presets/mempalace.ts +51 -0
- package/families/warm/typography.ts +17 -0
- package/package.json +85 -0
- package/reference/dark/claudedispatch.tsx +2441 -0
- package/reference/dark/notebooklm.tsx +2316 -0
- package/reference/dark/stitch.tsx +3040 -0
- package/reference/forbidden/heretic.tsx +2636 -0
- package/reference/glass/claudewatch.tsx +3827 -0
- package/reference/glass/graphify.tsx +2418 -0
- package/reference/glass/paperclip.tsx +2218 -0
- package/reference/paper/designreel.tsx +883 -0
- package/reference/paper/justdrop.tsx +1898 -0
- package/reference/paper/opus.tsx +1770 -0
- package/reference/warm/huashu.tsx +3413 -0
- package/reference/warm/mempalace.tsx +2909 -0
- package/skill/SKILL.md +229 -0
- package/skill/commands/reelstack-beats.md +20 -0
- package/skill/commands/reelstack-capture.md +24 -0
- package/skill/commands/reelstack-critique.md +15 -0
- package/skill/commands/reelstack-dark.md +40 -0
- package/skill/commands/reelstack-direction.md +17 -0
- package/skill/commands/reelstack-forbidden.md +25 -0
- package/skill/commands/reelstack-glass.md +39 -0
- package/skill/commands/reelstack-icons.md +22 -0
- package/skill/commands/reelstack-init.md +17 -0
- package/skill/commands/reelstack-lint.md +22 -0
- package/skill/commands/reelstack-paper.md +36 -0
- package/skill/commands/reelstack-render.md +20 -0
- package/skill/commands/reelstack-warm.md +36 -0
- package/templates/dark/template.tsx +115 -0
- package/templates/forbidden/template.tsx +111 -0
- package/templates/glass/template.tsx +201 -0
- package/templates/paper/template.tsx +133 -0
- package/templates/warm/template.tsx +210 -0
- package/utils/ai-purple-blocklist.ts +13 -0
- package/utils/banned-fonts.ts +11 -0
- package/utils/cubic-bezier.ts +36 -0
- package/utils/easing.ts +84 -0
- package/utils/grid.ts +13 -0
- package/utils/render-presets.json +56 -0
- package/utils/safe-zones.tsx +57 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Forbidden typography scale. Geist Sans + Geist Mono only.
|
|
2
|
+
* Slightly larger body / caption rhythm than Glass — matches HereticReel's
|
|
3
|
+
* declassified-document feel. Pulled from existing template usage
|
|
4
|
+
* (h1 84, h2 48, body 36); locks the implicit scale into named tokens.
|
|
5
|
+
* Text colors come from palette (ink / inkSoft / inkMuted) — never embed
|
|
6
|
+
* colors in typography tokens.
|
|
7
|
+
*/
|
|
8
|
+
export const typography = {
|
|
9
|
+
hero: { fontSize: 96, fontWeight: 700, letterSpacing: -2, lineHeight: 1.05, fontFamily: "Geist, system-ui" },
|
|
10
|
+
h1: { fontSize: 84, fontWeight: 700, letterSpacing: -1.5, lineHeight: 1.1, fontFamily: "Geist, system-ui" },
|
|
11
|
+
h2: { fontSize: 48, fontWeight: 700, letterSpacing: -1.2, lineHeight: 1.15, fontFamily: "Geist, system-ui" },
|
|
12
|
+
h3: { fontSize: 36, fontWeight: 600, letterSpacing: -1, lineHeight: 1.2, fontFamily: "Geist, system-ui" },
|
|
13
|
+
body: { fontSize: 36, fontWeight: 500, letterSpacing: -0.2, lineHeight: 1.45, fontFamily: "Geist, system-ui" },
|
|
14
|
+
caption: { fontSize: 22, fontWeight: 500, letterSpacing: 0.5, lineHeight: 1.4, fontFamily: "Geist, system-ui", textTransform: "uppercase" as const },
|
|
15
|
+
mono: { fontSize: 24, fontWeight: 500, letterSpacing: 0.3, lineHeight: 1.4, fontFamily: "Geist Mono, monospace" },
|
|
16
|
+
counter: { fontSize: 144, fontWeight: 700, letterSpacing: -2, lineHeight: 1, fontFamily: "Geist Mono, monospace" },
|
|
17
|
+
} as const;
|
|
18
|
+
export type Typography = typeof typography;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BreakdownCard — glass card with a filename header, accent dot, and arrow rows.
|
|
7
|
+
*
|
|
8
|
+
* Source: ClaudeWatchReel "Type. Paste. Watch." scene — the BREAKDOWN.MD lower
|
|
9
|
+
* callout (around line 1307) showing what the skill extracted from a video.
|
|
10
|
+
* The same shape doubles as a TRIGGERS.MD / FEATURES.MD / NOTES.MD anywhere a
|
|
11
|
+
* reel needs a "structured output" beat.
|
|
12
|
+
*
|
|
13
|
+
* Use when: a scene needs to reveal 1–4 mono rows of "what was extracted"
|
|
14
|
+
* style content, anchored under a hero illustration. The blinking-cursor
|
|
15
|
+
* variant evokes terminal output finishing in real time.
|
|
16
|
+
*
|
|
17
|
+
* Behavior: glass-card surface (mirrors `GlassCard` variant=`strong` shadows
|
|
18
|
+
* + blur). Title row shows accent dot + uppercase Geist-Mono filename. Each
|
|
19
|
+
* row springs in with a 4-frame stagger (gentle damping). Optional 6px-wide
|
|
20
|
+
* blinking cursor toggles every 15 frames after the last row finishes.
|
|
21
|
+
*/
|
|
22
|
+
export const BreakdownCard: React.FC<{
|
|
23
|
+
title?: string;
|
|
24
|
+
accentColor?: string;
|
|
25
|
+
rows: { arrow?: string; left: string; right?: string; rightAccent?: string }[];
|
|
26
|
+
startFrame?: number;
|
|
27
|
+
cursor?: boolean;
|
|
28
|
+
style?: CSSProperties;
|
|
29
|
+
}> = ({
|
|
30
|
+
title = "BREAKDOWN.MD",
|
|
31
|
+
accentColor = "#FF7A4D",
|
|
32
|
+
rows,
|
|
33
|
+
startFrame = 0,
|
|
34
|
+
cursor = true,
|
|
35
|
+
style,
|
|
36
|
+
}) => {
|
|
37
|
+
const frame = useCurrentFrame();
|
|
38
|
+
const { fps } = useVideoConfig();
|
|
39
|
+
const local = Math.max(0, frame - startFrame);
|
|
40
|
+
|
|
41
|
+
// Title row entrance
|
|
42
|
+
const titleT = spring({
|
|
43
|
+
frame: local,
|
|
44
|
+
fps,
|
|
45
|
+
config: { damping: 15, stiffness: 80 },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Cursor begins blinking after the last row has had time to settle.
|
|
49
|
+
const lastRowStart = rows.length * 4 + 14;
|
|
50
|
+
const cursorVisible =
|
|
51
|
+
cursor && local >= lastRowStart
|
|
52
|
+
? Math.floor((local - lastRowStart) / 15) % 2 === 0
|
|
53
|
+
: false;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
maxWidth: 720,
|
|
59
|
+
background: palette.glassFillStrong,
|
|
60
|
+
backdropFilter: "blur(20px) saturate(180%)",
|
|
61
|
+
WebkitBackdropFilter: "blur(20px) saturate(180%)",
|
|
62
|
+
border: `1.5px solid ${palette.glassBorder}`,
|
|
63
|
+
borderRadius: 24,
|
|
64
|
+
padding: 32,
|
|
65
|
+
boxShadow: [
|
|
66
|
+
"0 1px 0 rgba(255,255,255,0.7) inset",
|
|
67
|
+
"0 -1px 0 rgba(255,255,255,0.3) inset",
|
|
68
|
+
"0 8px 24px rgba(20,20,32,0.10)",
|
|
69
|
+
"0 32px 64px rgba(20,20,32,0.14)",
|
|
70
|
+
].join(", "),
|
|
71
|
+
fontFamily: "Geist Mono, monospace",
|
|
72
|
+
opacity: titleT,
|
|
73
|
+
transform: `translateY(${(1 - titleT) * 14}px)`,
|
|
74
|
+
...style,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{/* Title row */}
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
gap: 12,
|
|
83
|
+
marginBottom: 18,
|
|
84
|
+
fontSize: 18,
|
|
85
|
+
color: palette.inkMuted,
|
|
86
|
+
letterSpacing: "0.14em",
|
|
87
|
+
fontWeight: 700,
|
|
88
|
+
textTransform: "uppercase",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
width: 10,
|
|
94
|
+
height: 10,
|
|
95
|
+
borderRadius: "50%",
|
|
96
|
+
background: accentColor,
|
|
97
|
+
boxShadow: `0 0 14px ${accentColor}`,
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
<span>{title}</span>
|
|
101
|
+
{cursor ? (
|
|
102
|
+
<span
|
|
103
|
+
style={{
|
|
104
|
+
marginLeft: "auto",
|
|
105
|
+
display: "inline-block",
|
|
106
|
+
width: 6,
|
|
107
|
+
height: 22,
|
|
108
|
+
background: cursorVisible ? palette.ink : "transparent",
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Rows */}
|
|
115
|
+
{rows.map((row, i) => {
|
|
116
|
+
const rowLocal = local - (i + 1) * 4;
|
|
117
|
+
const t = spring({
|
|
118
|
+
frame: Math.max(0, rowLocal),
|
|
119
|
+
fps,
|
|
120
|
+
config: { damping: 15, stiffness: 80 },
|
|
121
|
+
});
|
|
122
|
+
const arrow = row.arrow ?? "→";
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
key={i}
|
|
126
|
+
style={{
|
|
127
|
+
display: "flex",
|
|
128
|
+
alignItems: "baseline",
|
|
129
|
+
gap: 12,
|
|
130
|
+
fontSize: 26,
|
|
131
|
+
fontWeight: 600,
|
|
132
|
+
color: palette.ink,
|
|
133
|
+
letterSpacing: "0.02em",
|
|
134
|
+
lineHeight: 1.55,
|
|
135
|
+
opacity: t,
|
|
136
|
+
transform: `translateY(${(1 - t) * 12}px)`,
|
|
137
|
+
willChange: "transform, opacity",
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<span style={{ color: accentColor, flexShrink: 0 }}>{arrow}</span>
|
|
141
|
+
<span style={{ flex: 1 }}>{row.left}</span>
|
|
142
|
+
{row.right ? (
|
|
143
|
+
<span
|
|
144
|
+
style={{
|
|
145
|
+
color: row.rightAccent ?? accentColor,
|
|
146
|
+
fontWeight: 700,
|
|
147
|
+
flexShrink: 0,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{row.right}
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette, gradients } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CausticBlobs — animated radial gradients drifting on sine paths.
|
|
7
|
+
*
|
|
8
|
+
* Simulates underwater light refraction. The signature background motion of
|
|
9
|
+
* the Glass Iridescent family. Stack 4 blobs (one per iridescent stop) and
|
|
10
|
+
* the result reads as living, premium glass.
|
|
11
|
+
*
|
|
12
|
+
* Frame-budget: cheap. 4 absolutely-positioned divs with CSS gradients only.
|
|
13
|
+
*/
|
|
14
|
+
export const CausticBlobs: React.FC<{
|
|
15
|
+
intensity?: number; // 0..1 — overall opacity multiplier
|
|
16
|
+
reduceMotion?: boolean;
|
|
17
|
+
}> = ({ intensity = 1, reduceMotion = false }) => {
|
|
18
|
+
const frame = useCurrentFrame();
|
|
19
|
+
const blobs = [
|
|
20
|
+
{ gradient: gradients.causticCyan, speedX: 0.0040, speedY: 0.0030, size: 1200 },
|
|
21
|
+
{ gradient: gradients.causticViolet, speedX: 0.0033, speedY: 0.0042, size: 1100 },
|
|
22
|
+
{ gradient: gradients.causticRose, speedX: 0.0027, speedY: 0.0035, size: 1300 },
|
|
23
|
+
{ gradient: gradients.causticGold, speedX: 0.0046, speedY: 0.0024, size: 1000 },
|
|
24
|
+
];
|
|
25
|
+
return (
|
|
26
|
+
<AbsoluteFill style={{ background: palette.bg, opacity: intensity }}>
|
|
27
|
+
{blobs.map((b, i) => {
|
|
28
|
+
const cx = reduceMotion ? 0.5 : 0.5 + 0.35 * Math.sin(frame * b.speedX + i);
|
|
29
|
+
const cy = reduceMotion ? 0.5 : 0.5 + 0.30 * Math.cos(frame * b.speedY + i * 1.3);
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
key={i}
|
|
33
|
+
style={{
|
|
34
|
+
position: "absolute",
|
|
35
|
+
left: `${cx * 100 - 50}%`,
|
|
36
|
+
top: `${cy * 100 - 50}%`,
|
|
37
|
+
width: b.size,
|
|
38
|
+
height: b.size,
|
|
39
|
+
background: b.gradient,
|
|
40
|
+
filter: "blur(60px)",
|
|
41
|
+
mixBlendMode: "screen",
|
|
42
|
+
pointerEvents: "none",
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</AbsoluteFill>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import { useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Counter — numeric ticker. Animates from 0 (or fromValue) to target with a
|
|
7
|
+
* power4Out easing curve. Snaps to integers per frame. Scale + blur entrance.
|
|
8
|
+
*
|
|
9
|
+
* 5-digit MONO counters cap at 256px width per house rules — be careful with
|
|
10
|
+
* fontSize × digit-count.
|
|
11
|
+
*/
|
|
12
|
+
export const Counter: React.FC<{
|
|
13
|
+
startFrame: number;
|
|
14
|
+
durationFrames?: number;
|
|
15
|
+
fromValue?: number;
|
|
16
|
+
toValue: number;
|
|
17
|
+
prefix?: string;
|
|
18
|
+
suffix?: string;
|
|
19
|
+
fontSize?: number;
|
|
20
|
+
color?: string;
|
|
21
|
+
fontFamily?: string;
|
|
22
|
+
maxWidth?: number;
|
|
23
|
+
reduceMotion?: boolean;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
}> = ({
|
|
26
|
+
startFrame,
|
|
27
|
+
durationFrames = 36,
|
|
28
|
+
fromValue = 0,
|
|
29
|
+
toValue,
|
|
30
|
+
prefix = "",
|
|
31
|
+
suffix = "",
|
|
32
|
+
fontSize = 144,
|
|
33
|
+
color = palette.ink,
|
|
34
|
+
fontFamily = "Geist Mono, monospace",
|
|
35
|
+
maxWidth = 256,
|
|
36
|
+
reduceMotion = false,
|
|
37
|
+
style,
|
|
38
|
+
}) => {
|
|
39
|
+
const frame = useCurrentFrame();
|
|
40
|
+
const local = Math.max(0, frame - startFrame);
|
|
41
|
+
const tRaw = Math.min(1, local / durationFrames);
|
|
42
|
+
// power4Out
|
|
43
|
+
const t = reduceMotion ? 1 : 1 - Math.pow(1 - tRaw, 4);
|
|
44
|
+
const value = reduceMotion ? toValue : Math.round(fromValue + (toValue - fromValue) * t);
|
|
45
|
+
const blur = reduceMotion ? 0 : (1 - t) * 12; // px
|
|
46
|
+
const scale = reduceMotion ? 1 : 0.92 + t * 0.08;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
style={{
|
|
51
|
+
display: "inline-block",
|
|
52
|
+
fontFamily,
|
|
53
|
+
fontSize,
|
|
54
|
+
fontWeight: 700,
|
|
55
|
+
letterSpacing: -2,
|
|
56
|
+
color,
|
|
57
|
+
filter: `blur(${blur}px)`,
|
|
58
|
+
transform: `scale(${scale})`,
|
|
59
|
+
transformOrigin: "center",
|
|
60
|
+
maxWidth,
|
|
61
|
+
overflow: "hidden",
|
|
62
|
+
textOverflow: "ellipsis",
|
|
63
|
+
whiteSpace: "nowrap",
|
|
64
|
+
...style,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{prefix}
|
|
68
|
+
{value.toLocaleString()}
|
|
69
|
+
{suffix}
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EyebrowPill — small contextual label that springs in at a beat.
|
|
7
|
+
*
|
|
8
|
+
* Iridescent background, 1.5px border, gentle spring entrance (damping 15,
|
|
9
|
+
* stiffness 80). Used as a "chapter mark" callout above hero text.
|
|
10
|
+
*/
|
|
11
|
+
export const EyebrowPill: React.FC<{
|
|
12
|
+
startFrame: number;
|
|
13
|
+
tint?: "violet" | "teal";
|
|
14
|
+
reduceMotion?: boolean;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}> = ({ startFrame, tint = "violet", reduceMotion = false, style, children }) => {
|
|
18
|
+
const frame = useCurrentFrame();
|
|
19
|
+
const { fps } = useVideoConfig();
|
|
20
|
+
const local = Math.max(0, frame - startFrame);
|
|
21
|
+
const t = reduceMotion
|
|
22
|
+
? 1
|
|
23
|
+
: spring({ frame: local, fps, config: { damping: 15, stiffness: 80 } });
|
|
24
|
+
|
|
25
|
+
const bg = tint === "violet" ? palette.violetPill : palette.tealPill;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
display: "inline-flex",
|
|
31
|
+
// Hug content even when parent is a flex/grid that would otherwise
|
|
32
|
+
// stretch us. inline-flex alone is not enough — flex parents apply
|
|
33
|
+
// align-items: stretch by default on the cross axis.
|
|
34
|
+
alignSelf: "flex-start",
|
|
35
|
+
width: "max-content",
|
|
36
|
+
maxWidth: "100%",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: 8,
|
|
39
|
+
padding: "10px 18px",
|
|
40
|
+
borderRadius: 999,
|
|
41
|
+
background: `${bg}38`, // ~22% alpha
|
|
42
|
+
border: `1.5px solid ${bg}AA`,
|
|
43
|
+
color: palette.ink,
|
|
44
|
+
fontFamily: "Geist, system-ui",
|
|
45
|
+
fontSize: 22,
|
|
46
|
+
fontWeight: 600,
|
|
47
|
+
letterSpacing: 0.5,
|
|
48
|
+
textTransform: "uppercase",
|
|
49
|
+
opacity: t,
|
|
50
|
+
transform: reduceMotion
|
|
51
|
+
? "none"
|
|
52
|
+
: `translateY(${(1 - t) * 12}px) scale(${0.95 + t * 0.05})`,
|
|
53
|
+
...style,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FilmStrip — perforated 35mm film roll with colored play-button thumbnails.
|
|
7
|
+
*
|
|
8
|
+
* Source: ClaudeWatchReel "Type. Paste. Watch." scene (frames 491–717). The
|
|
9
|
+
* centerpiece illustration of a video being scrubbed frame-by-frame.
|
|
10
|
+
*
|
|
11
|
+
* Use when: a reel needs to communicate "video / frame / playback" without
|
|
12
|
+
* dropping in actual footage. Pair with a vertical scan beam (LightBeam) and
|
|
13
|
+
* a BreakdownCard underneath for the full ClaudeWatch deluxe look.
|
|
14
|
+
*
|
|
15
|
+
* Behavior: each tile reveals with translateY(20px) + scale(0.94→1) + opacity
|
|
16
|
+
* 0→1 over 14 frames, staggered by `staggerFrames`. Each tile shows a centered
|
|
17
|
+
* play-triangle. If `playheadSpeed > 0`, a vertical scrubber line sweeps
|
|
18
|
+
* left→right across the strip, looping.
|
|
19
|
+
*/
|
|
20
|
+
export const FilmStrip: React.FC<{
|
|
21
|
+
tiles: { color: string; gradient?: string; label?: string }[];
|
|
22
|
+
startFrame?: number;
|
|
23
|
+
staggerFrames?: number;
|
|
24
|
+
playheadSpeed?: number; // 0 disables the playhead; >0 = canvas-px per frame
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}> = ({
|
|
27
|
+
tiles,
|
|
28
|
+
startFrame = 0,
|
|
29
|
+
staggerFrames = 4,
|
|
30
|
+
playheadSpeed = 0,
|
|
31
|
+
style,
|
|
32
|
+
}) => {
|
|
33
|
+
const frame = useCurrentFrame();
|
|
34
|
+
const local = Math.max(0, frame - startFrame);
|
|
35
|
+
|
|
36
|
+
const tileWidth = 140;
|
|
37
|
+
const tileGap = 12;
|
|
38
|
+
const stripPadX = 16;
|
|
39
|
+
const sprocketBand = 16;
|
|
40
|
+
const stripHeight = 200;
|
|
41
|
+
|
|
42
|
+
const stripWidth =
|
|
43
|
+
tiles.length * tileWidth + Math.max(0, tiles.length - 1) * tileGap + stripPadX * 2;
|
|
44
|
+
|
|
45
|
+
// Playhead sweeps left→right and loops
|
|
46
|
+
const innerWidth = stripWidth - stripPadX * 2;
|
|
47
|
+
const playheadX =
|
|
48
|
+
playheadSpeed > 0
|
|
49
|
+
? stripPadX + ((local * playheadSpeed) % innerWidth)
|
|
50
|
+
: 0;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
position: "relative",
|
|
56
|
+
display: "inline-block",
|
|
57
|
+
width: stripWidth,
|
|
58
|
+
height: stripHeight,
|
|
59
|
+
background: palette.ink,
|
|
60
|
+
borderRadius: 6,
|
|
61
|
+
overflow: "hidden",
|
|
62
|
+
boxShadow:
|
|
63
|
+
"0 6px 14px -6px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,255,255,0.08)",
|
|
64
|
+
...style,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{/* Sprocket holes — top */}
|
|
68
|
+
<div
|
|
69
|
+
style={{
|
|
70
|
+
position: "absolute",
|
|
71
|
+
top: 4,
|
|
72
|
+
left: 0,
|
|
73
|
+
right: 0,
|
|
74
|
+
height: 12,
|
|
75
|
+
backgroundImage: `repeating-linear-gradient(90deg, ${palette.bg} 0 14px, transparent 14px 30px)`,
|
|
76
|
+
opacity: 0.85,
|
|
77
|
+
pointerEvents: "none",
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
{/* Sprocket holes — bottom */}
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
position: "absolute",
|
|
84
|
+
bottom: 4,
|
|
85
|
+
left: 0,
|
|
86
|
+
right: 0,
|
|
87
|
+
height: 12,
|
|
88
|
+
backgroundImage: `repeating-linear-gradient(90deg, ${palette.bg} 0 14px, transparent 14px 30px)`,
|
|
89
|
+
opacity: 0.85,
|
|
90
|
+
pointerEvents: "none",
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
{/* Tiles */}
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
position: "absolute",
|
|
98
|
+
top: sprocketBand + 6,
|
|
99
|
+
bottom: sprocketBand + 6,
|
|
100
|
+
left: stripPadX,
|
|
101
|
+
right: stripPadX,
|
|
102
|
+
display: "flex",
|
|
103
|
+
gap: tileGap,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{tiles.map((tile, i) => {
|
|
107
|
+
const tileLocal = local - i * staggerFrames;
|
|
108
|
+
const t = interpolate(tileLocal, [0, 14], [0, 1], {
|
|
109
|
+
extrapolateLeft: "clamp",
|
|
110
|
+
extrapolateRight: "clamp",
|
|
111
|
+
});
|
|
112
|
+
const scale = 0.94 + t * 0.06;
|
|
113
|
+
const y = (1 - t) * 20;
|
|
114
|
+
const bg =
|
|
115
|
+
tile.gradient ??
|
|
116
|
+
`linear-gradient(135deg, ${palette.inkSoft}, ${tile.color}99)`;
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
key={i}
|
|
120
|
+
style={{
|
|
121
|
+
width: tileWidth,
|
|
122
|
+
height: "100%",
|
|
123
|
+
borderRadius: 4,
|
|
124
|
+
background: bg,
|
|
125
|
+
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.18)",
|
|
126
|
+
position: "relative",
|
|
127
|
+
opacity: t,
|
|
128
|
+
transform: `translateY(${y}px) scale(${scale})`,
|
|
129
|
+
willChange: "transform, opacity",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{/* Play triangle */}
|
|
133
|
+
<div
|
|
134
|
+
style={{
|
|
135
|
+
position: "absolute",
|
|
136
|
+
top: "50%",
|
|
137
|
+
left: "50%",
|
|
138
|
+
transform: "translate(-50%,-50%)",
|
|
139
|
+
width: 0,
|
|
140
|
+
height: 0,
|
|
141
|
+
borderTop: "10px solid transparent",
|
|
142
|
+
borderBottom: "10px solid transparent",
|
|
143
|
+
borderLeft: "16px solid rgba(255,255,255,0.55)",
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
{tile.label ? (
|
|
147
|
+
<div
|
|
148
|
+
style={{
|
|
149
|
+
position: "absolute",
|
|
150
|
+
bottom: 6,
|
|
151
|
+
left: 8,
|
|
152
|
+
right: 8,
|
|
153
|
+
fontFamily: "Geist Mono, monospace",
|
|
154
|
+
fontSize: 10,
|
|
155
|
+
fontWeight: 600,
|
|
156
|
+
color: "rgba(255,255,255,0.78)",
|
|
157
|
+
letterSpacing: "0.08em",
|
|
158
|
+
textTransform: "uppercase",
|
|
159
|
+
pointerEvents: "none",
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{tile.label}
|
|
163
|
+
</div>
|
|
164
|
+
) : null}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Vertical scrubber playhead */}
|
|
171
|
+
{playheadSpeed > 0 ? (
|
|
172
|
+
<>
|
|
173
|
+
<div
|
|
174
|
+
style={{
|
|
175
|
+
position: "absolute",
|
|
176
|
+
top: -4,
|
|
177
|
+
bottom: -4,
|
|
178
|
+
left: playheadX,
|
|
179
|
+
width: 4,
|
|
180
|
+
background: `linear-gradient(180deg, transparent, ${palette.iriCyan}, transparent)`,
|
|
181
|
+
boxShadow: `0 0 24px ${palette.iriCyan}, 0 0 48px ${palette.iriCyan}88`,
|
|
182
|
+
transform: "translateX(-50%)",
|
|
183
|
+
pointerEvents: "none",
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
<div
|
|
187
|
+
style={{
|
|
188
|
+
position: "absolute",
|
|
189
|
+
top: 0,
|
|
190
|
+
bottom: 0,
|
|
191
|
+
left: playheadX,
|
|
192
|
+
width: 120,
|
|
193
|
+
background: `radial-gradient(ellipse at center, ${palette.iriCyan}33, transparent 60%)`,
|
|
194
|
+
transform: "translateX(-50%)",
|
|
195
|
+
pointerEvents: "none",
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
</>
|
|
199
|
+
) : null}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FloatingGlyphs — unicode math/code symbols drifting on sine paths.
|
|
7
|
+
*
|
|
8
|
+
* House rule: NEVER emojis. Always unicode math/code symbols.
|
|
9
|
+
* Default set: ± ∞ ∑ ⟨ ⟩ ∫ ≠ ∂ ∇ ⊕ ⊗.
|
|
10
|
+
*
|
|
11
|
+
* Glyphs fade as they approach canvas edges so they don't distract from
|
|
12
|
+
* the hero subject.
|
|
13
|
+
*/
|
|
14
|
+
export const FloatingGlyphs: React.FC<{
|
|
15
|
+
count?: number;
|
|
16
|
+
glyphs?: string[];
|
|
17
|
+
fontSize?: number;
|
|
18
|
+
speed?: number;
|
|
19
|
+
color?: string;
|
|
20
|
+
reduceMotion?: boolean;
|
|
21
|
+
}> = ({
|
|
22
|
+
count = 14,
|
|
23
|
+
glyphs = ["±", "∞", "∑", "⟨", "⟩", "∫", "≠", "∂", "∇", "⊕", "⊗"],
|
|
24
|
+
fontSize = 36,
|
|
25
|
+
speed = 0.0009,
|
|
26
|
+
color = palette.inkMuted,
|
|
27
|
+
reduceMotion = false,
|
|
28
|
+
}) => {
|
|
29
|
+
const frame = useCurrentFrame();
|
|
30
|
+
const items = useMemo(() => {
|
|
31
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
32
|
+
const seed = i * 137;
|
|
33
|
+
return {
|
|
34
|
+
glyph: glyphs[i % glyphs.length],
|
|
35
|
+
baseX: ((seed * 53) % 1000) / 1000, // 0..1
|
|
36
|
+
baseY: ((seed * 71) % 1000) / 1000,
|
|
37
|
+
amplitudeX: 0.06 + ((seed * 17) % 100) / 1500,
|
|
38
|
+
amplitudeY: 0.04 + ((seed * 23) % 100) / 1800,
|
|
39
|
+
phase: (i * 0.27) % (Math.PI * 2),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}, [count, glyphs]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}
|
|
47
|
+
>
|
|
48
|
+
{items.map((g, i) => {
|
|
49
|
+
const t = frame * speed + g.phase;
|
|
50
|
+
const x = reduceMotion ? g.baseX * 100 : (g.baseX + Math.sin(t * 7 + i) * g.amplitudeX) * 100;
|
|
51
|
+
const y = reduceMotion ? g.baseY * 100 : (g.baseY + Math.cos(t * 5 + i) * g.amplitudeY) * 100;
|
|
52
|
+
// edge fade — fade out within 8% of any edge
|
|
53
|
+
const fade = Math.max(
|
|
54
|
+
0,
|
|
55
|
+
Math.min(1, (Math.min(x, 100 - x, y, 100 - y) - 4) / 6),
|
|
56
|
+
);
|
|
57
|
+
return (
|
|
58
|
+
<span
|
|
59
|
+
key={i}
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
left: `${x}%`,
|
|
63
|
+
top: `${y}%`,
|
|
64
|
+
transform: "translate(-50%, -50%)",
|
|
65
|
+
fontFamily: "Geist Mono, monospace",
|
|
66
|
+
fontSize,
|
|
67
|
+
fontWeight: 500,
|
|
68
|
+
color,
|
|
69
|
+
opacity: 0.42 * fade,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{g.glyph}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { palette } from "../palette";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GlassCard — frosted-glass surface. The Glass family's most-used primitive.
|
|
6
|
+
*
|
|
7
|
+
* Variants:
|
|
8
|
+
* - "default" — backdropFilter blur(16px), rgba 0.42 fill
|
|
9
|
+
* - "strong" — rgba 0.62 fill, blur(20px)
|
|
10
|
+
* - "heavy" — rgba 0.62 fill, blur(32px) saturate(180%) — gstack feel
|
|
11
|
+
*
|
|
12
|
+
* Multi-layer shadow stack reads as printed glass, not flat overlay.
|
|
13
|
+
*/
|
|
14
|
+
export const GlassCard: React.FC<{
|
|
15
|
+
variant?: "default" | "strong" | "heavy";
|
|
16
|
+
radius?: number;
|
|
17
|
+
padding?: number | string;
|
|
18
|
+
bezel?: boolean;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}> = ({ variant = "default", radius = 24, padding = 32, bezel = false, style, children }) => {
|
|
22
|
+
const fills = {
|
|
23
|
+
default: palette.glassFill,
|
|
24
|
+
strong: palette.glassFillStrong,
|
|
25
|
+
heavy: palette.glassFillStrong,
|
|
26
|
+
};
|
|
27
|
+
const blurs = {
|
|
28
|
+
default: "blur(16px)",
|
|
29
|
+
strong: "blur(20px)",
|
|
30
|
+
heavy: "blur(32px) saturate(180%)",
|
|
31
|
+
};
|
|
32
|
+
const baseShadows = [
|
|
33
|
+
"0 1px 0 rgba(255,255,255,0.7) inset",
|
|
34
|
+
"0 -1px 0 rgba(255,255,255,0.3) inset",
|
|
35
|
+
"0 8px 24px rgba(20,20,32,0.10)",
|
|
36
|
+
"0 32px 64px rgba(20,20,32,0.14)",
|
|
37
|
+
];
|
|
38
|
+
// Soft Double-Bezel: inner 1px white border at radius - 1
|
|
39
|
+
const shadows = bezel
|
|
40
|
+
? ["inset 0 0 0 1px rgba(255,255,255,0.6)", ...baseShadows]
|
|
41
|
+
: baseShadows;
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
background: fills[variant],
|
|
46
|
+
backdropFilter: blurs[variant],
|
|
47
|
+
WebkitBackdropFilter: blurs[variant],
|
|
48
|
+
border: `1.5px solid ${palette.glassBorder}`,
|
|
49
|
+
borderRadius: radius,
|
|
50
|
+
padding,
|
|
51
|
+
boxShadow: shadows.join(", "),
|
|
52
|
+
...style,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|