@codellyson/framely-cli 0.1.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 (40) hide show
  1. package/commands/compositions.js +135 -0
  2. package/commands/preview.js +889 -0
  3. package/commands/render.js +295 -0
  4. package/commands/still.js +165 -0
  5. package/index.js +93 -0
  6. package/package.json +60 -0
  7. package/studio/App.css +605 -0
  8. package/studio/App.jsx +185 -0
  9. package/studio/CompositionsView.css +399 -0
  10. package/studio/CompositionsView.jsx +327 -0
  11. package/studio/PropsEditor.css +195 -0
  12. package/studio/PropsEditor.tsx +176 -0
  13. package/studio/RenderDialog.tsx +476 -0
  14. package/studio/ShareDialog.tsx +200 -0
  15. package/studio/index.ts +19 -0
  16. package/studio/player/Player.css +199 -0
  17. package/studio/player/Player.jsx +355 -0
  18. package/studio/styles/design-system.css +592 -0
  19. package/studio/styles/dialogs.css +420 -0
  20. package/studio/templates/AnimatedGradient.jsx +99 -0
  21. package/studio/templates/InstagramStory.jsx +172 -0
  22. package/studio/templates/LowerThird.jsx +139 -0
  23. package/studio/templates/ProductShowcase.jsx +162 -0
  24. package/studio/templates/SlideTransition.jsx +211 -0
  25. package/studio/templates/SocialIntro.jsx +122 -0
  26. package/studio/templates/SubscribeAnimation.jsx +186 -0
  27. package/studio/templates/TemplateCard.tsx +58 -0
  28. package/studio/templates/TemplateFilters.tsx +97 -0
  29. package/studio/templates/TemplatePreviewDialog.tsx +196 -0
  30. package/studio/templates/TemplatesMarketplace.css +686 -0
  31. package/studio/templates/TemplatesMarketplace.tsx +172 -0
  32. package/studio/templates/TextReveal.jsx +134 -0
  33. package/studio/templates/UseTemplateDialog.tsx +154 -0
  34. package/studio/templates/index.ts +45 -0
  35. package/utils/browser.js +188 -0
  36. package/utils/codecs.js +200 -0
  37. package/utils/logger.js +35 -0
  38. package/utils/props.js +42 -0
  39. package/utils/render.js +447 -0
  40. package/utils/validate.js +148 -0
@@ -0,0 +1,420 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ Dialog Styles — Shared by RenderDialog & ExportDialog
3
+ Uses design system tokens from design-system.css
4
+ ═══════════════════════════════════════════════════════════════════════════ */
5
+
6
+ /* ─── Overlay ─── */
7
+ .dialog-overlay {
8
+ position: fixed;
9
+ inset: 0;
10
+ background-color: rgba(0, 0, 0, 0.7);
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ z-index: 1000;
15
+ font-family: var(--font-sans);
16
+ animation: dialog-fade-in var(--transition-fast) ease-out;
17
+ }
18
+
19
+ @keyframes dialog-fade-in {
20
+ from { opacity: 0; }
21
+ to { opacity: 1; }
22
+ }
23
+
24
+ @keyframes dialog-slide-up {
25
+ from { opacity: 0; transform: translateY(10px) scale(0.98); }
26
+ to { opacity: 1; transform: translateY(0) scale(1); }
27
+ }
28
+
29
+ /* ─── Panel ─── */
30
+ .dialog-panel {
31
+ background: var(--zinc-900);
32
+ border: 1px solid rgba(255, 255, 255, 0.06);
33
+ border-radius: var(--radius-xl);
34
+ width: 100%;
35
+ max-width: 520px;
36
+ max-height: 90vh;
37
+ overflow: auto;
38
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
39
+ animation: dialog-slide-up var(--transition-base) ease-out;
40
+ }
41
+
42
+ /* ─── Header ─── */
43
+ .dialog-header {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: var(--space-4) var(--space-5);
48
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
49
+ }
50
+
51
+ .dialog-title {
52
+ margin: 0;
53
+ color: var(--zinc-100);
54
+ font-size: var(--text-lg);
55
+ font-weight: 600;
56
+ letter-spacing: -0.02em;
57
+ }
58
+
59
+ .dialog-close-btn {
60
+ background: none;
61
+ border: none;
62
+ color: var(--zinc-500);
63
+ font-size: 24px;
64
+ cursor: pointer;
65
+ padding: 0;
66
+ line-height: 1;
67
+ transition: color var(--transition-fast);
68
+ }
69
+
70
+ .dialog-close-btn:hover {
71
+ color: var(--zinc-300);
72
+ }
73
+
74
+ .dialog-close-btn:disabled {
75
+ cursor: not-allowed;
76
+ opacity: 0.4;
77
+ }
78
+
79
+ /* ─── Body ─── */
80
+ .dialog-body {
81
+ padding: var(--space-5);
82
+ }
83
+
84
+ /* ─── Footer ─── */
85
+ .dialog-footer {
86
+ padding: var(--space-3) var(--space-5);
87
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
88
+ display: flex;
89
+ gap: var(--space-3);
90
+ justify-content: flex-end;
91
+ }
92
+
93
+ /* ─── Sections ─── */
94
+ .dialog-section {
95
+ margin-bottom: var(--space-4);
96
+ }
97
+
98
+ .dialog-section:last-child {
99
+ margin-bottom: 0;
100
+ }
101
+
102
+ .dialog-label {
103
+ display: block;
104
+ color: var(--zinc-400);
105
+ font-size: var(--text-xs);
106
+ font-weight: 500;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.05em;
109
+ margin-bottom: var(--space-2);
110
+ }
111
+
112
+ /* ─── Info Card ─── */
113
+ .dialog-info-card {
114
+ padding: var(--space-3);
115
+ background: var(--zinc-800);
116
+ border: 1px solid rgba(255, 255, 255, 0.06);
117
+ border-radius: var(--radius-lg);
118
+ margin-bottom: var(--space-5);
119
+ }
120
+
121
+ .dialog-info-card-title {
122
+ color: var(--zinc-100);
123
+ font-weight: 500;
124
+ margin-bottom: var(--space-1);
125
+ font-size: var(--text-sm);
126
+ }
127
+
128
+ .dialog-info-card-subtitle {
129
+ color: var(--zinc-500);
130
+ font-size: var(--text-xs);
131
+ }
132
+
133
+ /* ─── Code Area (textarea / input) ─── */
134
+ .dialog-code {
135
+ width: 100%;
136
+ padding: var(--space-3);
137
+ background: var(--zinc-800);
138
+ border: 1px solid rgba(255, 255, 255, 0.06);
139
+ border-radius: var(--radius-lg);
140
+ color: var(--zinc-200);
141
+ font-family: var(--font-mono);
142
+ font-size: var(--text-xs);
143
+ line-height: 1.6;
144
+ resize: none;
145
+ outline: none;
146
+ transition: border-color var(--transition-fast);
147
+ }
148
+
149
+ .dialog-code:focus {
150
+ border-color: var(--indigo-500);
151
+ }
152
+
153
+ /* ─── Option Grid (codec / quality selectors) ─── */
154
+ .dialog-option-grid {
155
+ display: grid;
156
+ gap: var(--space-2);
157
+ }
158
+
159
+ .dialog-option-grid--3col {
160
+ grid-template-columns: repeat(3, 1fr);
161
+ }
162
+
163
+ .dialog-option-grid--4col {
164
+ grid-template-columns: repeat(4, 1fr);
165
+ }
166
+
167
+ .dialog-option-btn {
168
+ padding: var(--space-2);
169
+ background: rgba(255, 255, 255, 0.06);
170
+ border: 1px solid rgba(255, 255, 255, 0.06);
171
+ border-radius: var(--radius-md);
172
+ color: var(--zinc-300);
173
+ font-family: var(--font-sans);
174
+ font-size: var(--text-xs);
175
+ cursor: pointer;
176
+ text-align: center;
177
+ transition: all var(--transition-fast);
178
+ }
179
+
180
+ .dialog-option-btn:hover:not(:disabled) {
181
+ background: rgba(255, 255, 255, 0.1);
182
+ border-color: rgba(255, 255, 255, 0.1);
183
+ }
184
+
185
+ .dialog-option-btn.active {
186
+ background: linear-gradient(135deg, var(--indigo-500), var(--purple-600));
187
+ border-color: transparent;
188
+ color: white;
189
+ font-weight: 500;
190
+ }
191
+
192
+ .dialog-option-btn:disabled {
193
+ cursor: not-allowed;
194
+ opacity: 0.5;
195
+ }
196
+
197
+ .dialog-option-hint {
198
+ color: var(--zinc-600);
199
+ font-size: var(--text-xs);
200
+ margin-top: var(--space-1);
201
+ }
202
+
203
+ /* ─── Inputs ─── */
204
+ .dialog-input {
205
+ padding: var(--space-2);
206
+ background: rgba(255, 255, 255, 0.06);
207
+ border: 1px solid rgba(255, 255, 255, 0.06);
208
+ border-radius: var(--radius-md);
209
+ color: var(--zinc-100);
210
+ font-family: var(--font-sans);
211
+ font-size: var(--text-sm);
212
+ outline: none;
213
+ transition: border-color var(--transition-fast);
214
+ }
215
+
216
+ .dialog-input:focus {
217
+ border-color: var(--indigo-500);
218
+ }
219
+
220
+ .dialog-input:disabled {
221
+ opacity: 0.5;
222
+ cursor: not-allowed;
223
+ }
224
+
225
+ /* Range input */
226
+ .dialog-range {
227
+ width: 100%;
228
+ accent-color: var(--indigo-500);
229
+ }
230
+
231
+ .dialog-range-labels {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ color: var(--zinc-600);
235
+ font-size: var(--text-xs);
236
+ }
237
+
238
+ /* Frame range row */
239
+ .dialog-frame-range {
240
+ display: flex;
241
+ gap: var(--space-2);
242
+ align-items: center;
243
+ }
244
+
245
+ .dialog-frame-range .dialog-input {
246
+ flex: 1;
247
+ }
248
+
249
+ .dialog-frame-range-sep {
250
+ color: var(--zinc-600);
251
+ font-size: var(--text-sm);
252
+ }
253
+
254
+ /* Checkbox options */
255
+ .dialog-checkbox-group {
256
+ display: flex;
257
+ gap: var(--space-4);
258
+ }
259
+
260
+ .dialog-checkbox-label {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: var(--space-2);
264
+ color: var(--zinc-400);
265
+ font-size: var(--text-xs);
266
+ cursor: pointer;
267
+ }
268
+
269
+ .dialog-checkbox-label input[type="checkbox"] {
270
+ accent-color: var(--indigo-500);
271
+ }
272
+
273
+ /* ─── Alerts ─── */
274
+ .dialog-alert {
275
+ padding: var(--space-3);
276
+ border-radius: var(--radius-lg);
277
+ font-size: var(--text-xs);
278
+ margin-bottom: var(--space-4);
279
+ border: 1px solid;
280
+ }
281
+
282
+ .dialog-alert--error {
283
+ background: rgba(239, 68, 68, 0.1);
284
+ border-color: rgba(239, 68, 68, 0.3);
285
+ color: var(--error);
286
+ }
287
+
288
+ .dialog-alert--success {
289
+ background: rgba(16, 185, 129, 0.1);
290
+ border-color: rgba(16, 185, 129, 0.3);
291
+ }
292
+
293
+ .dialog-alert-title {
294
+ color: var(--success);
295
+ font-weight: 500;
296
+ margin-bottom: var(--space-1);
297
+ }
298
+
299
+ .dialog-alert-subtitle {
300
+ color: var(--zinc-500);
301
+ font-size: var(--text-xs);
302
+ }
303
+
304
+ .dialog-alert-link {
305
+ display: inline-block;
306
+ margin-top: var(--space-2);
307
+ padding: var(--space-2) var(--space-3);
308
+ background: var(--success);
309
+ border-radius: var(--radius-sm);
310
+ color: white;
311
+ font-size: var(--text-xs);
312
+ font-weight: 500;
313
+ text-decoration: none;
314
+ transition: filter var(--transition-fast);
315
+ }
316
+
317
+ .dialog-alert-link:hover {
318
+ filter: brightness(1.1);
319
+ color: white;
320
+ }
321
+
322
+ /* ─── Progress Bar ─── */
323
+ .dialog-progress {
324
+ margin-bottom: var(--space-4);
325
+ }
326
+
327
+ .dialog-progress-track {
328
+ height: 4px;
329
+ background: var(--zinc-800);
330
+ border-radius: var(--radius-full);
331
+ overflow: hidden;
332
+ }
333
+
334
+ .dialog-progress-fill {
335
+ height: 100%;
336
+ background: linear-gradient(90deg, var(--indigo-500), var(--purple-500));
337
+ transition: width var(--transition-base);
338
+ border-radius: var(--radius-full);
339
+ }
340
+
341
+ .dialog-progress-text {
342
+ color: var(--zinc-500);
343
+ font-size: var(--text-xs);
344
+ text-align: center;
345
+ margin-top: var(--space-1);
346
+ }
347
+
348
+ /* ─── Buttons ─── */
349
+ .dialog-btn {
350
+ display: inline-flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ gap: var(--space-2);
354
+ padding: var(--space-2) var(--space-4);
355
+ font-family: var(--font-sans);
356
+ font-size: var(--text-xs);
357
+ font-weight: 500;
358
+ line-height: 1;
359
+ border-radius: var(--radius-md);
360
+ border: none;
361
+ cursor: pointer;
362
+ transition: all var(--transition-fast);
363
+ }
364
+
365
+ .dialog-btn--primary {
366
+ background: linear-gradient(135deg, var(--indigo-500), var(--purple-600));
367
+ color: white;
368
+ padding: var(--space-3) var(--space-5);
369
+ font-size: var(--text-sm);
370
+ border-radius: var(--radius-lg);
371
+ }
372
+
373
+ .dialog-btn--primary:hover:not(:disabled) {
374
+ filter: brightness(1.1);
375
+ box-shadow: 0 0 20px -5px var(--indigo-500);
376
+ }
377
+
378
+ .dialog-btn--primary:disabled {
379
+ opacity: 0.6;
380
+ cursor: not-allowed;
381
+ }
382
+
383
+ .dialog-btn--secondary {
384
+ background: transparent;
385
+ border: 1px solid rgba(255, 255, 255, 0.1);
386
+ color: var(--zinc-100);
387
+ padding: var(--space-3) var(--space-5);
388
+ font-size: var(--text-sm);
389
+ border-radius: var(--radius-lg);
390
+ }
391
+
392
+ .dialog-btn--secondary:hover:not(:disabled) {
393
+ background: rgba(255, 255, 255, 0.06);
394
+ border-color: rgba(255, 255, 255, 0.2);
395
+ }
396
+
397
+ .dialog-btn--secondary:disabled {
398
+ opacity: 0.4;
399
+ cursor: not-allowed;
400
+ }
401
+
402
+ .dialog-btn--subtle {
403
+ background: rgba(255, 255, 255, 0.06);
404
+ color: var(--zinc-300);
405
+ }
406
+
407
+ .dialog-btn--subtle:hover {
408
+ background: rgba(255, 255, 255, 0.1);
409
+ }
410
+
411
+ .dialog-btn--success {
412
+ background: var(--success);
413
+ color: white;
414
+ }
415
+
416
+ .dialog-btn-row {
417
+ display: flex;
418
+ gap: var(--space-2);
419
+ margin-top: var(--space-2);
420
+ }
@@ -0,0 +1,99 @@
1
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from '@codellyson/framely';
2
+
3
+ /**
4
+ * Animated Gradient Background Template
5
+ */
6
+ export function AnimatedGradient({
7
+ colors = ['#6366f1', '#8b5cf6', '#d946ef'],
8
+ speed = 1,
9
+ angle = 45,
10
+ }) {
11
+ const frame = useCurrentFrame();
12
+ const { fps } = useVideoConfig();
13
+
14
+ // Continuous animation that loops
15
+ const progress = (frame * speed) / fps;
16
+
17
+ // Create flowing gradient effect
18
+ const gradientAngle = angle + Math.sin(progress * 0.5) * 30;
19
+
20
+ // Color positions shift over time
21
+ const shift1 = (Math.sin(progress * 0.8) + 1) * 25;
22
+ const shift2 = 50 + Math.sin(progress * 0.6) * 20;
23
+ const shift3 = 100 - (Math.cos(progress * 0.7) + 1) * 15;
24
+
25
+ // Build gradient string
26
+ const gradient = `linear-gradient(${gradientAngle}deg,
27
+ ${colors[0]} ${shift1}%,
28
+ ${colors[1] || colors[0]} ${shift2}%,
29
+ ${colors[2] || colors[1] || colors[0]} ${shift3}%
30
+ )`;
31
+
32
+ // Overlay orbs
33
+ const orbs = colors.map((color, i) => {
34
+ const orbProgress = progress + i * 2;
35
+ const x = 50 + Math.sin(orbProgress * 0.3 + i) * 40;
36
+ const y = 50 + Math.cos(orbProgress * 0.4 + i * 0.5) * 40;
37
+ const scale = 0.8 + Math.sin(orbProgress * 0.5) * 0.3;
38
+
39
+ return {
40
+ color,
41
+ x,
42
+ y,
43
+ scale,
44
+ };
45
+ });
46
+
47
+ return (
48
+ <AbsoluteFill style={{ background: '#000', overflow: 'hidden' }}>
49
+ {/* Main gradient */}
50
+ <div
51
+ style={{
52
+ position: 'absolute',
53
+ inset: -100,
54
+ background: gradient,
55
+ filter: 'blur(0px)',
56
+ }}
57
+ />
58
+
59
+ {/* Animated orbs */}
60
+ {orbs.map((orb, i) => (
61
+ <div
62
+ key={i}
63
+ style={{
64
+ position: 'absolute',
65
+ left: `${orb.x}%`,
66
+ top: `${orb.y}%`,
67
+ width: '60%',
68
+ height: '60%',
69
+ background: `radial-gradient(circle, ${orb.color}66 0%, transparent 70%)`,
70
+ transform: `translate(-50%, -50%) scale(${orb.scale})`,
71
+ filter: 'blur(60px)',
72
+ }}
73
+ />
74
+ ))}
75
+
76
+ {/* Noise overlay for texture */}
77
+ <div
78
+ style={{
79
+ position: 'absolute',
80
+ inset: 0,
81
+ background: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
82
+ opacity: 0.05,
83
+ mixBlendMode: 'overlay',
84
+ }}
85
+ />
86
+
87
+ {/* Vignette */}
88
+ <div
89
+ style={{
90
+ position: 'absolute',
91
+ inset: 0,
92
+ background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.4) 100%)',
93
+ }}
94
+ />
95
+ </AbsoluteFill>
96
+ );
97
+ }
98
+
99
+ export default AnimatedGradient;
@@ -0,0 +1,172 @@
1
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
2
+
3
+ /**
4
+ * Instagram Story Template
5
+ */
6
+ export function InstagramStory({
7
+ headline = 'New Post!',
8
+ description = 'Check out our latest update',
9
+ backgroundColor = '#f472b6',
10
+ textColor = '#ffffff',
11
+ }) {
12
+ const frame = useCurrentFrame();
13
+ const { fps, width } = useVideoConfig();
14
+
15
+ // Background pulse
16
+ const bgPulse = 1 + Math.sin(frame * 0.1) * 0.02;
17
+
18
+ // Sticker animations
19
+ const stickers = [
20
+ { emoji: '✨', x: '15%', y: '20%', delay: 0, scale: 1.2 },
21
+ { emoji: '🔥', x: '85%', y: '25%', delay: 5, scale: 1.0 },
22
+ { emoji: '💫', x: '20%', y: '75%', delay: 10, scale: 0.9 },
23
+ { emoji: '⭐', x: '80%', y: '70%', delay: 15, scale: 1.1 },
24
+ { emoji: '💖', x: '50%', y: '15%', delay: 8, scale: 1.0 },
25
+ ];
26
+
27
+ // Headline animation
28
+ const headlineSpring = spring({ frame, fps, config: { damping: 12, stiffness: 150 } });
29
+ const headlineScale = interpolate(headlineSpring, [0, 1], [0, 1]);
30
+ const headlineRotate = interpolate(headlineSpring, [0, 1], [-10, 0]);
31
+
32
+ // Description animation
33
+ const descY = interpolate(frame, [20, 40], [30, 0], { extrapolateRight: 'clamp' });
34
+ const descOpacity = interpolate(frame, [20, 35], [0, 1], { extrapolateRight: 'clamp' });
35
+
36
+ // Swipe up indicator
37
+ const swipeY = interpolate(
38
+ frame % 60,
39
+ [0, 30, 60],
40
+ [0, -15, 0]
41
+ );
42
+ const swipeOpacity = interpolate(frame, [60, 80], [0, 1], { extrapolateRight: 'clamp' });
43
+
44
+ // Background gradient shift
45
+ const gradientAngle = 135 + Math.sin(frame * 0.05) * 20;
46
+
47
+ return (
48
+ <AbsoluteFill
49
+ style={{
50
+ background: `linear-gradient(${gradientAngle}deg, ${backgroundColor}, ${backgroundColor}cc, ${backgroundColor}99)`,
51
+ transform: `scale(${bgPulse})`,
52
+ display: 'flex',
53
+ flexDirection: 'column',
54
+ alignItems: 'center',
55
+ justifyContent: 'center',
56
+ overflow: 'hidden',
57
+ }}
58
+ >
59
+ {/* Animated circles background */}
60
+ {[...Array(6)].map((_, i) => {
61
+ const circleProgress = ((frame + i * 20) % 120) / 120;
62
+ const circleScale = interpolate(circleProgress, [0, 0.5, 1], [0, 1.5, 0]);
63
+ const circleOpacity = interpolate(circleProgress, [0, 0.3, 0.7, 1], [0, 0.3, 0.3, 0]);
64
+ return (
65
+ <div
66
+ key={i}
67
+ style={{
68
+ position: 'absolute',
69
+ left: '50%',
70
+ top: '50%',
71
+ width: width * 0.8,
72
+ height: width * 0.8,
73
+ borderRadius: '50%',
74
+ border: `3px solid ${textColor}`,
75
+ transform: `translate(-50%, -50%) scale(${circleScale})`,
76
+ opacity: circleOpacity,
77
+ }}
78
+ />
79
+ );
80
+ })}
81
+
82
+ {/* Stickers */}
83
+ {stickers.map((sticker, i) => {
84
+ const stickerSpring = spring({
85
+ frame: frame - sticker.delay,
86
+ fps,
87
+ config: { damping: 10, stiffness: 200 },
88
+ });
89
+ const stickerScale = interpolate(stickerSpring, [0, 1], [0, sticker.scale]);
90
+ const stickerRotate = Math.sin((frame + i * 10) * 0.1) * 15;
91
+ const stickerY = Math.sin((frame + i * 5) * 0.08) * 10;
92
+
93
+ return (
94
+ <div
95
+ key={i}
96
+ style={{
97
+ position: 'absolute',
98
+ left: sticker.x,
99
+ top: sticker.y,
100
+ fontSize: width * 0.08,
101
+ transform: `translate(-50%, -50%) scale(${stickerScale}) rotate(${stickerRotate}deg) translateY(${stickerY}px)`,
102
+ }}
103
+ >
104
+ {sticker.emoji}
105
+ </div>
106
+ );
107
+ })}
108
+
109
+ {/* Content card */}
110
+ <div
111
+ style={{
112
+ background: 'rgba(255, 255, 255, 0.15)',
113
+ backdropFilter: 'blur(20px)',
114
+ borderRadius: 30,
115
+ padding: '40px 50px',
116
+ textAlign: 'center',
117
+ border: '2px solid rgba(255, 255, 255, 0.2)',
118
+ }}
119
+ >
120
+ {/* Headline */}
121
+ <h1
122
+ style={{
123
+ fontSize: width * 0.08,
124
+ fontWeight: 800,
125
+ color: textColor,
126
+ margin: 0,
127
+ transform: `scale(${headlineScale}) rotate(${headlineRotate}deg)`,
128
+ textShadow: '0 4px 20px rgba(0,0,0,0.2)',
129
+ }}
130
+ >
131
+ {headline}
132
+ </h1>
133
+
134
+ {/* Description */}
135
+ <p
136
+ style={{
137
+ fontSize: width * 0.04,
138
+ color: textColor,
139
+ margin: '15px 0 0',
140
+ opacity: descOpacity,
141
+ transform: `translateY(${descY}px)`,
142
+ fontWeight: 500,
143
+ }}
144
+ >
145
+ {description}
146
+ </p>
147
+ </div>
148
+
149
+ {/* Swipe up indicator */}
150
+ <div
151
+ style={{
152
+ position: 'absolute',
153
+ bottom: 60,
154
+ display: 'flex',
155
+ flexDirection: 'column',
156
+ alignItems: 'center',
157
+ opacity: swipeOpacity,
158
+ transform: `translateY(${swipeY}px)`,
159
+ }}
160
+ >
161
+ <svg width="30" height="30" viewBox="0 0 24 24" fill={textColor}>
162
+ <path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" />
163
+ </svg>
164
+ <span style={{ color: textColor, fontSize: 14, fontWeight: 600, marginTop: 5 }}>
165
+ Swipe Up
166
+ </span>
167
+ </div>
168
+ </AbsoluteFill>
169
+ );
170
+ }
171
+
172
+ export default InstagramStory;