wizz-method 2.15.0 → 2.17.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.
Files changed (85) hide show
  1. package/README.md +12 -1
  2. package/THIRD-PARTY-NOTICES.md +6 -0
  3. package/package.json +5 -2
  4. package/skills-registry.yaml +18 -6
  5. package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
  6. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +34 -12
  7. package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
  8. package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
  9. package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
  10. package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
  11. package/src/bmm-skills/module.yaml +10 -0
  12. package/src/core-skills/_shared/handoff-protocol.md +10 -0
  13. package/src/modules/wizz/_shared/encerramento.md +4 -0
  14. package/src/modules/wizz/_shared/organizacao.md +58 -0
  15. package/src/modules/wizz/_shared/video-pipeline.md +1 -0
  16. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +9 -1
  17. package/src/modules/wizz/agents/wizz-maestro/customize.toml +8 -2
  18. package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +11 -0
  19. package/src/skills-lib/wizz-router/SKILL.md +3 -1
  20. package/src/skills-lib/wizz-router/references/routing-table-flat.md +1 -0
  21. package/src/skills-lib/wizz-video-styles/SKILL.md +66 -0
  22. package/src/skills-lib/wizz-video-styles/assets/audio/CREDITS.md +54 -0
  23. package/src/skills-lib/wizz-video-styles/assets/audio/LIBRARIES.md +31 -0
  24. package/src/skills-lib/wizz-video-styles/assets/audio/kit.json +182 -0
  25. package/src/skills-lib/wizz-video-styles/assets/audio/music/acoustic-lifestyle.mp3 +0 -0
  26. package/src/skills-lib/wizz-video-styles/assets/audio/music/cinematic-tense.mp3 +0 -0
  27. package/src/skills-lib/wizz-video-styles/assets/audio/music/classical-light.mp3 +0 -0
  28. package/src/skills-lib/wizz-video-styles/assets/audio/music/corporate-light.mp3 +0 -0
  29. package/src/skills-lib/wizz-video-styles/assets/audio/music/lofi-chill.mp3 +0 -0
  30. package/src/skills-lib/wizz-video-styles/assets/audio/music/meme-beat.mp3 +0 -0
  31. package/src/skills-lib/wizz-video-styles/assets/audio/music/piano-editorial.mp3 +0 -0
  32. package/src/skills-lib/wizz-video-styles/assets/audio/music/tech-pulse.mp3 +0 -0
  33. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/boom-impact.mp3 +0 -0
  34. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/click-mouse.mp3 +0 -0
  35. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ding-notify.mp3 +0 -0
  36. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/glitch-short.mp3 +0 -0
  37. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ka-ching.mp3 +0 -0
  38. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/pop-bubble.mp3 +0 -0
  39. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/record-scratch.mp3 +0 -0
  40. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/riser-short.mp3 +0 -0
  41. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/swoosh-text.mp3 +0 -0
  42. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/typing-short.mp3 +0 -0
  43. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-fast.mp3 +0 -0
  44. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-soft.mp3 +0 -0
  45. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/EditorialReel.tsx +111 -0
  46. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/README.md +77 -0
  47. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/captions.tsx +56 -0
  48. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/example-words.json +285 -0
  49. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/primitives.tsx +241 -0
  50. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/scenes.tsx +113 -0
  51. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/theme.ts +68 -0
  52. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/timeline.ts +65 -0
  53. package/src/skills-lib/wizz-video-styles/data/recipes.json +341 -0
  54. package/src/skills-lib/wizz-video-styles/data/references.json +3397 -0
  55. package/src/skills-lib/wizz-video-styles/data/styles.json +726 -0
  56. package/src/skills-lib/wizz-video-styles/references/audio.md +54 -0
  57. package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +38 -0
  58. package/src/skills-lib/wizz-video-styles/references/research-method.md +33 -0
  59. package/src/skills-lib/wizz-video-styles/references/style-picker.md +51 -0
  60. package/src/skills-lib/wizz-video-styles/references/styles/camera-no-setup.md +60 -0
  61. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +60 -0
  62. package/src/skills-lib/wizz-video-styles/references/styles/esquete.md +57 -0
  63. package/src/skills-lib/wizz-video-styles/references/styles/experimento.md +56 -0
  64. package/src/skills-lib/wizz-video-styles/references/styles/explicador-animado.md +62 -0
  65. package/src/skills-lib/wizz-video-styles/references/styles/macro-asmr.md +61 -0
  66. package/src/skills-lib/wizz-video-styles/references/styles/meme-rapido.md +65 -0
  67. package/src/skills-lib/wizz-video-styles/references/styles/motion-editorial.md +69 -0
  68. package/src/skills-lib/wizz-video-styles/references/styles/podcast-clip.md +61 -0
  69. package/src/skills-lib/wizz-video-styles/references/styles/pov-vlog.md +57 -0
  70. package/src/skills-lib/wizz-video-styles/references/styles/quadro-branco.md +60 -0
  71. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-cards.md +62 -0
  72. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-legenda.md +62 -0
  73. package/src/skills-lib/wizz-video-styles/references/styles/tour.md +59 -0
  74. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +66 -0
  75. package/src/skills-lib/wizz-video-styles/scripts/build_catalog.py +128 -0
  76. package/tools/hooks/README.md +14 -2
  77. package/tools/hooks/session-rules.js +65 -140
  78. package/tools/hooks/wizz-arrumar.js +418 -0
  79. package/tools/hooks/wizz-artifact-guard.js +160 -0
  80. package/tools/hooks/wizz-feature-lib.js +414 -0
  81. package/tools/hooks/wizz-feature.js +401 -0
  82. package/tools/hooks/wizz-layout-lib.js +152 -0
  83. package/tools/hooks/wizz-router-enforce.js +21 -1
  84. package/tools/installer/core/doctor.js +5 -1
  85. package/tools/installer/core/hooks-settings.js +10 -0
@@ -0,0 +1,285 @@
1
+ {
2
+ "words": [
3
+ {
4
+ "text": "I",
5
+ "start": 0.04,
6
+ "end": 0.23
7
+ },
8
+ {
9
+ "text": "shipped",
10
+ "start": 0.26,
11
+ "end": 0.63
12
+ },
13
+ {
14
+ "text": "a",
15
+ "start": 0.66,
16
+ "end": 0.85
17
+ },
18
+ {
19
+ "text": "whole",
20
+ "start": 0.88,
21
+ "end": 1.19
22
+ },
23
+ {
24
+ "text": "app",
25
+ "start": 1.22,
26
+ "end": 1.47
27
+ },
28
+ {
29
+ "text": "in",
30
+ "start": 1.5,
31
+ "end": 1.72
32
+ },
33
+ {
34
+ "text": "one",
35
+ "start": 1.75,
36
+ "end": 2.0
37
+ },
38
+ {
39
+ "text": "weekend.",
40
+ "start": 2.03,
41
+ "end": 2.4
42
+ },
43
+ {
44
+ "text": "Here",
45
+ "start": 2.78,
46
+ "end": 3.06
47
+ },
48
+ {
49
+ "text": "is",
50
+ "start": 3.09,
51
+ "end": 3.31
52
+ },
53
+ {
54
+ "text": "how.",
55
+ "start": 3.34,
56
+ "end": 3.59
57
+ },
58
+ {
59
+ "text": "I",
60
+ "start": 3.97,
61
+ "end": 4.16
62
+ },
63
+ {
64
+ "text": "only",
65
+ "start": 4.19,
66
+ "end": 4.47
67
+ },
68
+ {
69
+ "text": "tell",
70
+ "start": 4.5,
71
+ "end": 4.78
72
+ },
73
+ {
74
+ "text": "the",
75
+ "start": 4.81,
76
+ "end": 5.06
77
+ },
78
+ {
79
+ "text": "agent",
80
+ "start": 5.09,
81
+ "end": 5.4
82
+ },
83
+ {
84
+ "text": "what",
85
+ "start": 5.43,
86
+ "end": 5.71
87
+ },
88
+ {
89
+ "text": "I",
90
+ "start": 5.74,
91
+ "end": 5.93
92
+ },
93
+ {
94
+ "text": "want.",
95
+ "start": 5.96,
96
+ "end": 6.24
97
+ },
98
+ {
99
+ "text": "Look",
100
+ "start": 6.62,
101
+ "end": 6.9
102
+ },
103
+ {
104
+ "text": "at",
105
+ "start": 6.93,
106
+ "end": 7.15
107
+ },
108
+ {
109
+ "text": "the",
110
+ "start": 7.18,
111
+ "end": 7.43
112
+ },
113
+ {
114
+ "text": "result.",
115
+ "start": 7.46,
116
+ "end": 7.8
117
+ },
118
+ {
119
+ "text": "A",
120
+ "start": 8.18,
121
+ "end": 8.37
122
+ },
123
+ {
124
+ "text": "real",
125
+ "start": 8.4,
126
+ "end": 8.68
127
+ },
128
+ {
129
+ "text": "screen.",
130
+ "start": 8.71,
131
+ "end": 9.05
132
+ },
133
+ {
134
+ "text": "In",
135
+ "start": 9.43,
136
+ "end": 9.65
137
+ },
138
+ {
139
+ "text": "forty",
140
+ "start": 9.68,
141
+ "end": 9.99
142
+ },
143
+ {
144
+ "text": "eight",
145
+ "start": 10.02,
146
+ "end": 10.33
147
+ },
148
+ {
149
+ "text": "hours,",
150
+ "start": 10.36,
151
+ "end": 10.67
152
+ },
153
+ {
154
+ "text": "31",
155
+ "start": 10.85,
156
+ "end": 11.07
157
+ },
158
+ {
159
+ "text": "commits",
160
+ "start": 11.1,
161
+ "end": 11.47
162
+ },
163
+ {
164
+ "text": "and",
165
+ "start": 11.5,
166
+ "end": 11.75
167
+ },
168
+ {
169
+ "text": "12",
170
+ "start": 11.78,
171
+ "end": 12.0
172
+ },
173
+ {
174
+ "text": "tests.",
175
+ "start": 12.03,
176
+ "end": 12.34
177
+ },
178
+ {
179
+ "text": "Three",
180
+ "start": 12.72,
181
+ "end": 13.03
182
+ },
183
+ {
184
+ "text": "agents",
185
+ "start": 13.06,
186
+ "end": 13.4
187
+ },
188
+ {
189
+ "text": "write,",
190
+ "start": 13.43,
191
+ "end": 13.74
192
+ },
193
+ {
194
+ "text": "test",
195
+ "start": 13.92,
196
+ "end": 14.2
197
+ },
198
+ {
199
+ "text": "and",
200
+ "start": 14.23,
201
+ "end": 14.48
202
+ },
203
+ {
204
+ "text": "review.",
205
+ "start": 14.51,
206
+ "end": 14.85
207
+ },
208
+ {
209
+ "text": "First",
210
+ "start": 15.23,
211
+ "end": 15.54
212
+ },
213
+ {
214
+ "text": "a",
215
+ "start": 15.57,
216
+ "end": 15.76
217
+ },
218
+ {
219
+ "text": "spec,",
220
+ "start": 15.79,
221
+ "end": 16.07
222
+ },
223
+ {
224
+ "text": "then",
225
+ "start": 16.25,
226
+ "end": 16.53
227
+ },
228
+ {
229
+ "text": "the",
230
+ "start": 16.56,
231
+ "end": 16.81
232
+ },
233
+ {
234
+ "text": "code.",
235
+ "start": 16.84,
236
+ "end": 17.12
237
+ },
238
+ {
239
+ "text": "Comment",
240
+ "start": 17.5,
241
+ "end": 17.87
242
+ },
243
+ {
244
+ "text": "SHIP",
245
+ "start": 17.9,
246
+ "end": 18.18
247
+ },
248
+ {
249
+ "text": "and",
250
+ "start": 18.21,
251
+ "end": 18.46
252
+ },
253
+ {
254
+ "text": "I",
255
+ "start": 18.49,
256
+ "end": 18.68
257
+ },
258
+ {
259
+ "text": "will",
260
+ "start": 18.71,
261
+ "end": 18.99
262
+ },
263
+ {
264
+ "text": "send",
265
+ "start": 19.02,
266
+ "end": 19.3
267
+ },
268
+ {
269
+ "text": "you",
270
+ "start": 19.33,
271
+ "end": 19.58
272
+ },
273
+ {
274
+ "text": "the",
275
+ "start": 19.61,
276
+ "end": 19.86
277
+ },
278
+ {
279
+ "text": "prompt.",
280
+ "start": 19.89,
281
+ "end": 20.23
282
+ }
283
+ ],
284
+ "total": 20.33
285
+ }
@@ -0,0 +1,241 @@
1
+ import React, { useId } from "react";
2
+ import { AbsoluteFill, Easing, Img, interpolate, spring, staticFile, useCurrentFrame } from "remotion";
3
+ import { FPS, H, W, useTheme } from "./theme";
4
+
5
+ // Vocabulário do estilo: fundo claro com grade, cards escuros com sombra funda,
6
+ // serifa itálica bicolor, pílula digitada, conector tracejado, raios, contador.
7
+
8
+ export const clamp = { extrapolateLeft: "clamp", extrapolateRight: "clamp" } as const;
9
+ export const ease = Easing.bezier(0.22, 1, 0.36, 1);
10
+ export const useSeconds = (): number => useCurrentFrame() / FPS;
11
+ export const pop = (frame: number, delay = 0): number =>
12
+ spring({ frame: frame - Math.round(delay * FPS), fps: FPS, config: { damping: 15, mass: 0.7 } });
13
+
14
+ const CX = W / 2;
15
+
16
+ // Envelope de cena: entra subindo com mola, sai subindo e sumindo (sem corte seco).
17
+ export const Stage: React.FC<{ dur: number; children: React.ReactNode }> = ({ dur, children }) => {
18
+ const frame = useCurrentFrame();
19
+ const t = frame / FPS;
20
+ const s = pop(frame);
21
+ const out = interpolate(t, [dur - 0.3, dur], [0, 1], { ...clamp, easing: Easing.in(Easing.cubic) });
22
+ const cam = interpolate(t, [0, dur], [1, 1.035], clamp);
23
+ return (
24
+ <AbsoluteFill
25
+ style={{
26
+ opacity: Math.min(1, s * 1.4) * (1 - out),
27
+ transform: `translateY(${(1 - s) * 70 - out * 60}px) scale(${(0.94 + 0.06 * s) * cam})`,
28
+ }}
29
+ >
30
+ {children}
31
+ </AbsoluteFill>
32
+ );
33
+ };
34
+
35
+ export const Backdrop: React.FC = () => {
36
+ const c = useTheme();
37
+ return (
38
+ <AbsoluteFill
39
+ style={{
40
+ background: c.bg,
41
+ backgroundImage: `linear-gradient(${c.grid} 2px, transparent 2px), linear-gradient(90deg, ${c.grid} 2px, transparent 2px)`,
42
+ backgroundSize: "72px 72px",
43
+ }}
44
+ >
45
+ <AbsoluteFill style={{ background: `radial-gradient(ellipse at 50% 45%, ${c.glow}, rgba(0,0,0,0) 60%, ${c.vignette})` }} />
46
+ </AbsoluteFill>
47
+ );
48
+ };
49
+
50
+ // Título serifado em duas linhas: a primeira na cor do texto, a segunda no destaque.
51
+ export const Headline: React.FC<{ a: string; b: string; top?: number; delay?: number; size?: number }> = ({
52
+ a, b, top = 300, delay = 0, size = 104,
53
+ }) => {
54
+ const c = useTheme();
55
+ const frame = useCurrentFrame();
56
+ const s1 = pop(frame, delay);
57
+ const s2 = pop(frame, delay + 0.18);
58
+ const base = { fontFamily: c.serif, fontWeight: 700, fontSize: size, lineHeight: 1.0, letterSpacing: -2 } as const;
59
+ return (
60
+ <div style={{ position: "absolute", top, left: 0, right: 0, textAlign: "center" }}>
61
+ <div style={{ ...base, color: c.ink, opacity: s1, transform: `translateY(${(1 - s1) * 30}px)` }}>{a}</div>
62
+ <div style={{ ...base, color: c.accent, opacity: s2, transform: `translateY(${(1 - s2) * 30}px)` }}>{b}</div>
63
+ </div>
64
+ );
65
+ };
66
+
67
+ // Raios que "acendem" abaixo de uma pílula (o agente trabalhando). grow: 0..1.
68
+ export const Rays: React.FC<{ grow: number; y: number; size?: number }> = ({ grow, y, size = 300 }) => {
69
+ const c = useTheme();
70
+ const frame = useCurrentFrame();
71
+ const n = 11;
72
+ return (
73
+ <div style={{ position: "absolute", left: CX - size, top: y, width: size * 2, height: size, overflow: "hidden" }}>
74
+ {Array.from({ length: n }, (_, i) => {
75
+ const angle = -80 + (160 / (n - 1)) * i;
76
+ const wobble = Math.sin(frame / 9 + i) * 0.06;
77
+ const len = size * (0.62 + (0.3 * ((i * 7) % 5)) / 4) * grow * (1 + wobble);
78
+ return (
79
+ <div
80
+ key={i}
81
+ style={{
82
+ position: "absolute", left: size - 17, top: 0, width: 34, height: len, borderRadius: 17,
83
+ background: `linear-gradient(${c.accentWarm}, ${c.accentLight})`,
84
+ transformOrigin: "50% 0%", transform: `rotate(${angle}deg)`,
85
+ boxShadow: `0 10px 18px ${c.accentShadow}`,
86
+ }}
87
+ />
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ // Anel girando: objeto-símbolo de abertura.
95
+ export const Ring: React.FC<{ y?: number; size?: number }> = ({ y = 760, size = 420 }) => {
96
+ const c = useTheme();
97
+ const frame = useCurrentFrame();
98
+ const s = pop(frame, 0.1);
99
+ return (
100
+ <div style={{ position: "absolute", left: CX - size / 2, top: y, width: size, height: size, transform: `scale(${s}) rotate(${frame * 0.8}deg)` }}>
101
+ <div style={{ position: "absolute", inset: 0, borderRadius: "50%", border: `${size * 0.186}px solid ${c.accent}`, borderRightColor: "transparent", boxShadow: `0 50px 80px ${c.accentShadow}` }} />
102
+ </div>
103
+ );
104
+ };
105
+
106
+ // Pílula escura com texto digitado (input de prompt). Tempos em segundos da cena.
107
+ export const Pill: React.FC<{
108
+ text: string; typeFrom: number; typeDur: number; y: number; w?: number; icon?: string; serif?: boolean;
109
+ }> = ({ text, typeFrom, typeDur, y, w = 820, icon = "✦", serif = true }) => {
110
+ const c = useTheme();
111
+ const t = useSeconds();
112
+ const n = Math.round(interpolate(t, [typeFrom, typeFrom + typeDur], [0, text.length], clamp));
113
+ const caret = n < text.length && Math.floor(t * 3) % 2 === 0;
114
+ return (
115
+ <div style={{ position: "absolute", left: CX - w / 2, top: y, width: w, height: 150, borderRadius: 40, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, display: "flex", alignItems: "center", padding: "0 44px", gap: 28 }}>
116
+ <div style={{ width: 62, height: 62, borderRadius: 18, background: c.accent, color: c.onAccent, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 34 }}>{icon}</div>
117
+ <div style={{ fontFamily: serif ? c.serif : c.sans, fontWeight: 600, fontSize: 56, color: c.line, whiteSpace: "nowrap" }}>
118
+ {text.slice(0, n)}
119
+ <span style={{ color: c.accentLight, opacity: caret ? 1 : 0 }}>|</span>
120
+ </div>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ // Chip pequeno, centrado em (x, y). Entra com mola em `delay` (s).
126
+ export const Chip: React.FC<{
127
+ label: string; x: number; y: number; delay: number; accent?: boolean; big?: boolean; icon?: string;
128
+ }> = ({ label, x, y, delay, accent, big, icon = "▤" }) => {
129
+ const c = useTheme();
130
+ const s = pop(useCurrentFrame(), delay);
131
+ return (
132
+ <div style={{ position: "absolute", left: x, top: y, transform: `translate(-50%,-50%) scale(${s})`, opacity: s > 0.02 ? 1 : 0, background: accent ? c.accent : `linear-gradient(${c.cardHi}, ${c.card})`, color: accent ? c.onAccent : c.line, borderRadius: 26, padding: big ? "26px 40px" : "20px 32px", boxShadow: c.shadow, display: "flex", alignItems: "center", gap: 18, fontFamily: c.serif, fontWeight: 600, fontSize: big ? 52 : 44, whiteSpace: "nowrap" }}>
133
+ {icon ? <span style={{ fontFamily: c.sans, fontSize: big ? 34 : 28, opacity: 0.8 }}>{icon}</span> : null}
134
+ {label}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ // Conector tracejado que se desenha de (x1,y1) a (x2,y2) a partir de `from` (s).
140
+ export const Dash: React.FC<{ x1: number; y1: number; x2: number; y2: number; from: number; dur?: number }> = ({
141
+ x1, y1, x2, y2, from, dur = 0.45,
142
+ }) => {
143
+ const c = useTheme();
144
+ const t = useSeconds();
145
+ const id = `dash${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
146
+ const p = interpolate(t, [from, from + dur], [0, 1], { ...clamp, easing: ease });
147
+ const len = Math.hypot(x2 - x1, y2 - y1);
148
+ const d = `M ${x1} ${y1} C ${x1} ${(y1 + y2) / 2}, ${(x1 + x2) / 2} ${y2}, ${x2} ${y2}`;
149
+ return (
150
+ <svg style={{ position: "absolute", inset: 0 }} width={W} height={H}>
151
+ <defs>
152
+ <mask id={id}>
153
+ <path d={d} stroke="#fff" strokeWidth={10} fill="none" strokeDasharray={len * 1.5} strokeDashoffset={len * 1.5 * (1 - p)} />
154
+ </mask>
155
+ </defs>
156
+ <path d={d} stroke={c.dash} strokeWidth={4} fill="none" strokeDasharray="10 12" mask={`url(#${id})`} />
157
+ </svg>
158
+ );
159
+ };
160
+
161
+ const resolveSrc = (src: string): string => (/^(https?:|data:)/.test(src) ? src : staticFile(src));
162
+
163
+ // Celular-card com screenshot em proporção real. `ratio` = altura/largura da imagem
164
+ // (iPhone 1206x2622 = 2.174). Sem `src`, desenha uma tela de exemplo (o molde roda sem assets).
165
+ export const PhoneCard: React.FC<{
166
+ src?: string; y?: number; scale?: number; focusY?: number; maxH?: number; ratio?: number;
167
+ }> = ({ src, y = 250, scale = 1, focusY = 0, maxH = 1180, ratio = 2622 / 1206 }) => {
168
+ const c = useTheme();
169
+ const w = 600 * scale;
170
+ const h = w * ratio;
171
+ const viewH = Math.min(h, maxH);
172
+ return (
173
+ <div style={{ position: "absolute", left: CX - w / 2 - 16, top: y, width: w + 32, height: viewH + 32, borderRadius: 74 * scale, background: c.card, padding: 16, boxShadow: c.shadow }}>
174
+ <div style={{ width: w, height: viewH, borderRadius: 60 * scale, overflow: "hidden", background: c.bg }}>
175
+ {src ? (
176
+ <Img src={resolveSrc(src)} style={{ width: w, height: h, transform: `translateY(${-focusY * scale}px)` }} />
177
+ ) : (
178
+ <div style={{ padding: `${60 * scale}px ${40 * scale}px`, display: "flex", flexDirection: "column", gap: 28 * scale }}>
179
+ <div style={{ height: 70 * scale, borderRadius: 20 * scale, background: c.accent }} />
180
+ {Array.from({ length: 6 }, (_, i) => (
181
+ <div key={i} style={{ height: 120 * scale, borderRadius: 24 * scale, background: c.line, opacity: 1 - i * 0.1 }} />
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ );
188
+ };
189
+
190
+ // Card com número que sobe até `to` a partir de `delay` (s).
191
+ export const Counter: React.FC<{ to: number; label: string; y: number; delay: number }> = ({ to, label, y, delay }) => {
192
+ const c = useTheme();
193
+ const frame = useCurrentFrame();
194
+ const s = pop(frame, delay);
195
+ const v = Math.round(interpolate(frame / FPS, [delay, delay + 1.0], [0, to], { ...clamp, easing: Easing.out(Easing.cubic) }));
196
+ return (
197
+ <div style={{ position: "absolute", left: 140, right: 140, top: y, height: 330, borderRadius: 44, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, transform: `scale(${s})`, opacity: s > 0.02 ? 1 : 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
198
+ <div style={{ fontFamily: c.serif, fontWeight: 700, fontSize: 180, color: c.accentLight, lineHeight: 1 }}>{v}</div>
199
+ <div style={{ fontFamily: c.serif, fontWeight: 600, fontSize: 52, color: c.line, marginTop: 6 }}>{label}</div>
200
+ </div>
201
+ );
202
+ };
203
+
204
+ export type DocPage = { lines: string[]; at: number; mono?: boolean }; // `at` em s da cena
205
+ const REVIEW_SPOTS = [
206
+ { x: 175, y: 560 }, { x: 905, y: 700 }, { x: 175, y: 1020 }, { x: 905, y: 1160 },
207
+ ] as const;
208
+
209
+ // Card alto escuro (documento). Cada página troca o conteúdo e digita as linhas;
210
+ // `reviewers` pendura até 4 chips conectados por tracejado ao redor do card.
211
+ export const DocCard: React.FC<{
212
+ pages: DocPage[]; title: string; altTitle?: string; reviewers?: { labels: string[]; at: number };
213
+ }> = ({ pages, title, altTitle, reviewers }) => {
214
+ const c = useTheme();
215
+ const t = useSeconds();
216
+ const page = [...pages].reverse().find((p) => t >= p.at) ?? pages[0];
217
+ const isAlt = page !== pages[0];
218
+ const shown = page.lines.slice(0, Math.round(interpolate(t, [page.at + 0.2, page.at + 1.2], [0, page.lines.length], clamp)));
219
+ return (
220
+ <>
221
+ <div style={{ position: "absolute", left: 220, top: 380, width: 640, height: 980, borderRadius: 48, background: `linear-gradient(${c.cardHi}, ${c.card})`, boxShadow: c.shadow, padding: "130px 48px 0", overflow: "hidden" }}>
222
+ {shown.map((l, i) => (
223
+ <div key={`${page.at}-${i}`} style={{ fontFamily: page.mono ? c.mono : c.sans, fontSize: 25, lineHeight: page.mono ? "46px" : "42px", color: i === 0 && !page.mono ? c.accentLight : c.line, whiteSpace: "nowrap", overflow: "hidden", opacity: 0.92 }}>
224
+ {l}
225
+ </div>
226
+ ))}
227
+ </div>
228
+ <Chip label={isAlt && altTitle ? altTitle : title} x={CX} y={380} delay={0.05} accent={isAlt} />
229
+ {(reviewers?.labels ?? []).slice(0, 4).map((label, i) => {
230
+ const spot = REVIEW_SPOTS[i];
231
+ const from = (reviewers?.at ?? 0) + i * 0.22;
232
+ return (
233
+ <React.Fragment key={label}>
234
+ <Dash x1={spot.x < CX ? 230 : 850} y1={spot.y} x2={spot.x} y2={spot.y} from={from} dur={0.2} />
235
+ <Chip label={label} x={spot.x} y={spot.y} delay={from} accent />
236
+ </React.Fragment>
237
+ );
238
+ })}
239
+ </>
240
+ );
241
+ };
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { interpolate } from "remotion";
3
+ import {
4
+ Chip, Counter, Dash, DocCard, Headline, PhoneCard, Pill, Rays, Ring, clamp, ease, useSeconds,
5
+ type DocPage,
6
+ } from "./primitives";
7
+ import { useTheme } from "./theme";
8
+ import type { Anchored } from "./timeline";
9
+
10
+ // Cenas como DADOS: cada objeto do array vira um componente. `at` é a palavra-âncora
11
+ // onde a cena começa; `marks` nomeia palavras internas (ex.: quando o contador sobe).
12
+
13
+ export type SceneDef = Anchored &
14
+ (
15
+ | { kind: "hook"; a: string; b: string }
16
+ | { kind: "prompt"; a: string; b: string; prompt: string; typedMark?: string }
17
+ | { kind: "phone"; src?: string; ratio?: number; scale?: number; y?: number; focusY?: number }
18
+ | { kind: "numbers"; chip: string; counters: { to: number; label: string; mark: string }[] }
19
+ | { kind: "steps"; prompt: string; items: [string, string, string]; typedMark?: string }
20
+ | { kind: "doc"; title: string; altTitle?: string; pages: (Omit<DocPage, "at"> & { mark?: string })[]; reviewers?: { labels: string[]; mark: string } }
21
+ | { kind: "cta"; a: string; b: string; keyword: string; footer: string; hideCaptions?: boolean }
22
+ );
23
+
24
+ export type SceneCtx = { dur: number; mark: (name: string | undefined, fallback?: number) => number };
25
+
26
+ const Hook: React.FC<{ a: string; b: string }> = ({ a, b }) => (
27
+ <>
28
+ <Headline a={a} b={b} />
29
+ <Ring />
30
+ </>
31
+ );
32
+
33
+ const Prompt: React.FC<{ a: string; b: string; prompt: string; typed: number; dur: number }> = ({ a, b, prompt, typed, dur }) => {
34
+ const t = useSeconds();
35
+ return (
36
+ <>
37
+ <Headline a={a} b={b} />
38
+ <Pill text={prompt} typeFrom={0.2} typeDur={Math.min(typed, dur * 0.6)} y={820} />
39
+ <Rays grow={interpolate(t, [0.4, 1.0], [0, 1], { ...clamp, easing: ease })} y={970} />
40
+ </>
41
+ );
42
+ };
43
+
44
+ const Numbers: React.FC<{ chip: string; counters: { to: number; label: string; delay: number }[] }> = ({ chip, counters }) => (
45
+ <>
46
+ <Chip label={chip} x={540} y={330} delay={0.05} accent />
47
+ {counters.slice(0, 2).map((c, i) => (
48
+ <Counter key={c.label} to={c.to} label={c.label} y={470 + i * 390} delay={c.delay} />
49
+ ))}
50
+ </>
51
+ );
52
+
53
+ const Steps: React.FC<{ prompt: string; items: [string, string, string]; typed: number }> = ({ prompt, items, typed }) => {
54
+ const t = useSeconds();
55
+ const spots = [
56
+ { x: 230, y: 1000, from: [300, 760] as const, delay: 0.25 },
57
+ { x: 540, y: 1100, from: [540, 760] as const, delay: 0.55 },
58
+ { x: 850, y: 1000, from: [780, 760] as const, delay: 0.85 },
59
+ ];
60
+ return (
61
+ <>
62
+ <Pill text={prompt} typeFrom={0.1} typeDur={Math.max(0.6, typed - 0.1)} y={330} w={700} />
63
+ <Rays grow={interpolate(t, [0.1, 0.6], [0, 1], { ...clamp, easing: ease })} y={480} size={260} />
64
+ {spots.map((s, i) => (
65
+ <React.Fragment key={items[i]}>
66
+ <Dash x1={s.from[0]} y1={s.from[1]} x2={s.x} y2={s.y - 40} from={s.delay - 0.05} />
67
+ <Chip label={items[i]} x={s.x} y={s.y} delay={s.delay} />
68
+ </React.Fragment>
69
+ ))}
70
+ </>
71
+ );
72
+ };
73
+
74
+ const Cta: React.FC<{ a: string; b: string; keyword: string; footer: string }> = ({ a, b, keyword, footer }) => {
75
+ const c = useTheme();
76
+ const t = useSeconds();
77
+ return (
78
+ <>
79
+ <Headline a={a} b={b} top={360} />
80
+ <Pill text={keyword} typeFrom={0.35} typeDur={0.5} y={760} w={520} />
81
+ <Rays grow={interpolate(t, [0.6, 1.1], [0, 1], { ...clamp, easing: ease })} y={910} />
82
+ <div style={{ position: "absolute", top: 1290, left: 120, right: 120, textAlign: "center", fontFamily: c.serif, fontWeight: 600, fontSize: 54, color: c.ink, lineHeight: 1.2 }}>
83
+ {footer}
84
+ </div>
85
+ </>
86
+ );
87
+ };
88
+
89
+ export const SceneView: React.FC<{ scene: SceneDef; ctx: SceneCtx }> = ({ scene, ctx }) => {
90
+ switch (scene.kind) {
91
+ case "hook":
92
+ return <Hook a={scene.a} b={scene.b} />;
93
+ case "prompt":
94
+ return <Prompt a={scene.a} b={scene.b} prompt={scene.prompt} typed={ctx.mark(scene.typedMark, 1.1)} dur={ctx.dur} />;
95
+ case "phone":
96
+ return <PhoneCard src={scene.src} ratio={scene.ratio} scale={scene.scale ?? 1} y={scene.y ?? 260} focusY={scene.focusY} maxH={1200} />;
97
+ case "numbers":
98
+ return <Numbers chip={scene.chip} counters={scene.counters.map((c) => ({ ...c, delay: ctx.mark(c.mark, 0.3) }))} />;
99
+ case "steps":
100
+ return <Steps prompt={scene.prompt} items={scene.items} typed={ctx.mark(scene.typedMark, 0.8)} />;
101
+ case "doc":
102
+ return (
103
+ <DocCard
104
+ title={scene.title}
105
+ altTitle={scene.altTitle}
106
+ pages={scene.pages.map((p, i) => ({ lines: p.lines, mono: p.mono, at: i === 0 ? 0.2 : ctx.mark(p.mark, 1) }))}
107
+ reviewers={scene.reviewers ? { labels: scene.reviewers.labels, at: ctx.mark(scene.reviewers.mark, 2) } : undefined}
108
+ />
109
+ );
110
+ case "cta":
111
+ return <Cta a={scene.a} b={scene.b} keyword={scene.keyword} footer={scene.footer} />;
112
+ }
113
+ };