@lovett/ui 0.0.1

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 (102) hide show
  1. package/README.md +28 -0
  2. package/package.json +53 -0
  3. package/src/accordion.tsx +153 -0
  4. package/src/atoms.tsx +197 -0
  5. package/src/badge.tsx +91 -0
  6. package/src/brand-logo-tile.tsx +200 -0
  7. package/src/bulk-action-bar.tsx +104 -0
  8. package/src/button-group.tsx +46 -0
  9. package/src/button.tsx +93 -0
  10. package/src/calculator-shell-v2.tsx +380 -0
  11. package/src/calculator-shell.tsx +310 -0
  12. package/src/card.tsx +369 -0
  13. package/src/checkbox.tsx +114 -0
  14. package/src/chip-nav.tsx +347 -0
  15. package/src/choropleth.tsx +62 -0
  16. package/src/code-block.tsx +235 -0
  17. package/src/completion-ring.tsx +119 -0
  18. package/src/copy-field.tsx +169 -0
  19. package/src/data-grid/column-options-menu.tsx +127 -0
  20. package/src/data-grid/data-grid.tsx +391 -0
  21. package/src/data-grid/drawer-panel.tsx +146 -0
  22. package/src/data-grid/index.ts +29 -0
  23. package/src/data-grid/sortable-parts.tsx +215 -0
  24. package/src/data-grid/table-body.tsx +238 -0
  25. package/src/data-grid/table-elements.tsx +135 -0
  26. package/src/data-grid/table-header.tsx +95 -0
  27. package/src/data-grid/table-overlays.tsx +78 -0
  28. package/src/data-grid/table-parts.tsx +15 -0
  29. package/src/data-grid/table-summary-footer.tsx +101 -0
  30. package/src/data-grid/table-view.tsx +176 -0
  31. package/src/data-grid/types.ts +141 -0
  32. package/src/data-grid/use-grid-columns.ts +311 -0
  33. package/src/data-grid/use-grid-editing.ts +154 -0
  34. package/src/device-frame.tsx +274 -0
  35. package/src/dropdown-button.tsx +221 -0
  36. package/src/dropdown-menu.tsx +450 -0
  37. package/src/empty-state.tsx +85 -0
  38. package/src/file-upload/file-drop-zone.tsx +156 -0
  39. package/src/file-upload/file-preview-grid.tsx +39 -0
  40. package/src/file-upload/file-preview-item.tsx +175 -0
  41. package/src/file-upload/file-thumbnail.tsx +98 -0
  42. package/src/file-upload/file-upload-button.tsx +95 -0
  43. package/src/file-upload/index.ts +13 -0
  44. package/src/file-upload/use-file-upload.ts +371 -0
  45. package/src/filter-bar.tsx +125 -0
  46. package/src/filter-dropdown.tsx +289 -0
  47. package/src/floating-drawer.tsx +420 -0
  48. package/src/folder-card.tsx +188 -0
  49. package/src/folder-tree-picker.tsx +511 -0
  50. package/src/format.ts +117 -0
  51. package/src/frame-stack.tsx +73 -0
  52. package/src/hero-form-card.tsx +471 -0
  53. package/src/index.ts +372 -0
  54. package/src/input.tsx +130 -0
  55. package/src/kbd.tsx +41 -0
  56. package/src/lib/layout.ts +22 -0
  57. package/src/lib/utils.ts +6 -0
  58. package/src/markdown.tsx +214 -0
  59. package/src/menu-button.tsx +142 -0
  60. package/src/meta-previews/CreativeMedia.tsx +119 -0
  61. package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
  62. package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
  63. package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
  64. package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
  65. package/src/meta-previews/MetaFeedPreview.tsx +294 -0
  66. package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
  67. package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
  68. package/src/meta-previews/MetaReelPreview.tsx +750 -0
  69. package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
  70. package/src/meta-previews/MetaSearchPreview.tsx +113 -0
  71. package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
  72. package/src/meta-previews/ProfileAvatar.tsx +79 -0
  73. package/src/meta-previews/defaults.ts +27 -0
  74. package/src/meta-previews/index.ts +37 -0
  75. package/src/meta-previews/types.ts +219 -0
  76. package/src/metric-card.tsx +117 -0
  77. package/src/modal.tsx +131 -0
  78. package/src/option-tile.tsx +252 -0
  79. package/src/page-hero.tsx +78 -0
  80. package/src/page-shell.tsx +440 -0
  81. package/src/pagination.tsx +86 -0
  82. package/src/pill-button.tsx +103 -0
  83. package/src/profile-section.tsx +389 -0
  84. package/src/progress-bar.tsx +84 -0
  85. package/src/radio.tsx +172 -0
  86. package/src/range-slider.tsx +161 -0
  87. package/src/resizable.tsx +282 -0
  88. package/src/search-bar.tsx +81 -0
  89. package/src/segmented-pill.tsx +143 -0
  90. package/src/shell.tsx +464 -0
  91. package/src/slider.tsx +107 -0
  92. package/src/sortable-table.tsx +198 -0
  93. package/src/sortable.tsx +137 -0
  94. package/src/stat-row.tsx +132 -0
  95. package/src/stats-grid.tsx +38 -0
  96. package/src/step-loader.tsx +141 -0
  97. package/src/styles.css +562 -0
  98. package/src/tabs.tsx +143 -0
  99. package/src/tag-chip-input.tsx +394 -0
  100. package/src/textarea.tsx +39 -0
  101. package/src/toast.tsx +59 -0
  102. package/src/tokens.css +579 -0
@@ -0,0 +1,1132 @@
1
+ import React from 'react';
2
+ import { ChevronUp, MoreHorizontal, X, Link2 } from 'lucide-react';
3
+ import type { PreviewAdData, PreviewPlatform } from './types';
4
+ import { ProfileAvatar } from './ProfileAvatar';
5
+ import { CreativeMedia } from './CreativeMedia';
6
+
7
+ interface MetaStoryPreviewProps {
8
+ platform: PreviewPlatform;
9
+ adData: PreviewAdData;
10
+ dominantColor?: string | undefined;
11
+ accentColor?: string | undefined;
12
+ isLight?: boolean | undefined;
13
+ }
14
+
15
+ /**
16
+ * Shared story header — consistent across FB Stories, IG Stories, Messenger Stories.
17
+ * Sized 10% smaller than Meta's actual preview for better fit in our grid.
18
+ */
19
+ interface StoryHeaderProps {
20
+ platform: PreviewPlatform;
21
+ profileImage: string;
22
+ brandName: string;
23
+ username: string;
24
+ isLight?: boolean | undefined;
25
+ }
26
+
27
+ const StoryHeader: React.FC<StoryHeaderProps> = ({ platform, profileImage, brandName, username, isLight = false }) => {
28
+ const isIG = platform === 'instagram';
29
+ // Display name: IG uses username, FB/Messenger use full page name
30
+ const displayName = isIG ? username : brandName;
31
+ // Sub text: FB = "Ad", Messenger = "Sponsored", IG = none (but aligned to top like others)
32
+ const subText = platform === 'facebook' ? 'Ad' : platform === 'messenger' ? 'Sponsored' : null;
33
+ // Icons: FB = ellipsis + X, Messenger = ellipsis only, IG = ellipsis + X
34
+ const showClose = platform !== 'messenger';
35
+
36
+ // All story headers: 25% smaller than Meta's native sizes
37
+ const profileSize = 30;
38
+ const nameSize = 10;
39
+ const subSize = 9;
40
+ const iconBtnSize = 24;
41
+ const iconSize = 18;
42
+
43
+ // Dynamic contrast: dark text on light gradient backgrounds
44
+ const textColor = isLight ? 'rgb(28, 30, 33)' : 'rgb(255, 255, 255)';
45
+ const subTextColor = isLight ? 'rgba(0, 0, 0, 0.6)' : 'rgba(255, 255, 255, 0.8)';
46
+ const iconColor = isLight ? 'rgb(28, 30, 33)' : 'rgb(255, 255, 255)';
47
+
48
+ return (
49
+ <div
50
+ style={{
51
+ position: 'absolute',
52
+ top: '14px',
53
+ left: '10px',
54
+ right: '10px',
55
+ display: 'flex',
56
+ alignItems: 'flex-start',
57
+ gap: '6px',
58
+ zIndex: 20,
59
+ filter: 'drop-shadow(0 1px 3px rgba(0, 0, 0, 0.4))',
60
+ }}
61
+ >
62
+ {/* Profile Image */}
63
+ <div
64
+ style={{
65
+ width: `${profileSize}px`,
66
+ height: `${profileSize}px`,
67
+ borderRadius: '50%',
68
+ overflow: 'hidden',
69
+ flexShrink: 0,
70
+ border: '2px solid rgba(255, 255, 255, 0.2)',
71
+ marginTop: '1px',
72
+ }}
73
+ >
74
+ <ProfileAvatar
75
+ src={profileImage}
76
+ name={brandName}
77
+ fontSize={9}
78
+ color={textColor}
79
+ background="rgb(100, 100, 100)"
80
+ />
81
+ </div>
82
+
83
+ {/* Name + Sub text — aligned to top */}
84
+ <div style={{ flex: 1, minWidth: 0, paddingTop: '1px' }}>
85
+ <p
86
+ style={{
87
+ color: textColor,
88
+ fontSize: `${nameSize}px`,
89
+ fontWeight: 600,
90
+ lineHeight: `${Math.round(nameSize * 1.3)}px`,
91
+ margin: 0,
92
+ overflow: 'hidden',
93
+ textOverflow: 'ellipsis',
94
+ whiteSpace: 'nowrap',
95
+ }}
96
+ >
97
+ {displayName}
98
+ </p>
99
+ {/* Sub text for FB/Messenger, empty line for IG to keep alignment */}
100
+ <p
101
+ style={{
102
+ color: subTextColor,
103
+ fontSize: `${subSize}px`,
104
+ fontWeight: 400,
105
+ lineHeight: `${Math.round(subSize * 1.3)}px`,
106
+ margin: 0,
107
+ visibility: subText ? 'visible' : 'hidden',
108
+ }}
109
+ >
110
+ {subText || '.'}
111
+ </p>
112
+ </div>
113
+
114
+ {/* Ellipsis */}
115
+ <button
116
+ style={{
117
+ width: `${iconBtnSize}px`,
118
+ height: `${iconBtnSize}px`,
119
+ display: 'flex',
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ color: iconColor,
123
+ background: 'none',
124
+ border: 'none',
125
+ cursor: 'pointer',
126
+ flexShrink: 0,
127
+ padding: 0,
128
+ }}
129
+ >
130
+ <MoreHorizontal style={{ width: `${iconSize}px`, height: `${iconSize}px` }} />
131
+ </button>
132
+
133
+ {/* Close X — FB and IG only */}
134
+ {showClose && (
135
+ <button
136
+ style={{
137
+ width: `${iconBtnSize}px`,
138
+ height: `${iconBtnSize}px`,
139
+ display: 'flex',
140
+ alignItems: 'center',
141
+ justifyContent: 'center',
142
+ color: iconColor,
143
+ background: 'none',
144
+ border: 'none',
145
+ cursor: 'pointer',
146
+ flexShrink: 0,
147
+ padding: 0,
148
+ }}
149
+ >
150
+ <X style={{ width: `${iconSize}px`, height: `${iconSize}px` }} />
151
+ </button>
152
+ )}
153
+ </div>
154
+ );
155
+ };
156
+
157
+ /**
158
+ * Meta Stories Preview (Facebook/Instagram/Messenger)
159
+ * Exact styling extracted from Facebook Creative Hub
160
+ * Reference: creative-spec/references/META_DESIGN_TOKENS.md
161
+ * Sources: facebook-stories-singleimage-1080x1080.html, instagram-mobile-stories-singleimage-1080x1080.html
162
+ * Facebook Stories Container: 318x566px
163
+ * Instagram Stories Container: 320x567px (inner: 318x565px)
164
+ */
165
+ export const MetaStoryPreview: React.FC<MetaStoryPreviewProps> = ({
166
+ platform,
167
+ adData,
168
+ dominantColor = 'rgb(40, 40, 40)',
169
+ }) => {
170
+ const [progressKey, setProgressKey] = React.useState(0);
171
+ const [instagramFrame, setInstagramFrame] = React.useState<'default' | 'expanded'>('default');
172
+ const [isTextExpanded, setIsTextExpanded] = React.useState(false);
173
+
174
+ // Facebook Stories exact design tokens - extracted from Creative Hub
175
+ const fbStory = {
176
+ // Container — bumped 318 → 320 to match peer previews (Feed, Reel, IG Story)
177
+ // so the AdPreview canvas no longer renders FB Stories 2px narrower.
178
+ containerWidth: 320,
179
+ containerHeight: 569,
180
+ containerBorder: 'none',
181
+ fontFamily: 'system-ui, -apple-system, "system-ui", ".SFNSText-Regular", sans-serif',
182
+ baseColor: 'rgb(28, 30, 33)',
183
+ baseFontSize: 12,
184
+ baseLineHeight: '16.08px',
185
+
186
+ // Background gradient (for 1:1 images centered) — dynamically matched to creative
187
+ backgroundGradient: `linear-gradient(to top, ${dominantColor}, rgb(255, 255, 255))`,
188
+
189
+ // Image
190
+ imageWidth: 318,
191
+ imageHeight: 318,
192
+ imageTop: 71, // Centers 1:1 image in 9:16 frame
193
+
194
+ // Bottom gradient overlay
195
+ bottomGradientTop: 399,
196
+ bottomGradientWidth: 318,
197
+ bottomGradientHeight: 167,
198
+ bottomGradient: 'linear-gradient(rgba(85, 85, 85, 0), rgba(85, 85, 85, 0.5))',
199
+
200
+ // Primary text overlay
201
+ textLeft: 15,
202
+ textRight: 15,
203
+ textTop: 419,
204
+ textWidth: 288,
205
+ textMaxWidth: 288,
206
+ textColor: 'rgb(255, 255, 255)',
207
+ textFont: 'Helvetica, Arial, sans-serif',
208
+ textSize: 14,
209
+ textWeight: 600,
210
+ textLineHeight: '19.6px',
211
+
212
+ // Progress bar
213
+ progressTop: 6,
214
+ progressLeft: 4,
215
+ progressRight: 4,
216
+ progressWidth: 310,
217
+ progressHeight: 2,
218
+ progressBg: 'rgba(255, 255, 255, 0.5)',
219
+ progressFillBg: 'rgb(255, 255, 255)',
220
+ progressRadius: 1,
221
+ };
222
+
223
+ // Instagram Stories design tokens
224
+ const igStory = {
225
+ // Container
226
+ containerWidth: 320,
227
+ containerHeight: 567,
228
+ innerWidth: 318,
229
+ innerHeight: 565,
230
+ containerBorder: 'none',
231
+ fontFamily: 'system-ui, -apple-system, "system-ui", ".SFNSText-Regular", sans-serif',
232
+ baseColor: 'rgb(28, 30, 33)',
233
+
234
+ // Background — dynamic gradient matched to creative
235
+ backgroundGradient: `linear-gradient(rgb(255, 255, 255), ${dominantColor})`,
236
+ overlayBg: 'rgba(0, 0, 0, 0.5)',
237
+
238
+ // Profile
239
+ profileSize: 28,
240
+ profileRadius: 14,
241
+ profileTop: 16,
242
+ profileLeft: 10,
243
+
244
+ // Username
245
+ usernameTop: 16,
246
+ usernameLeft: 46,
247
+ usernameWidth: 236,
248
+ usernameSize: 12,
249
+ usernameWeight: 700,
250
+ usernameLineHeight: '14px',
251
+ usernameColor: 'rgb(255, 255, 255)',
252
+
253
+ // Sponsored text at bottom
254
+ sponsoredTop: 535.602,
255
+ sponsoredPadding: '6px 6px 10px 12px',
256
+ sponsoredGradient: 'linear-gradient(rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.2))',
257
+ sponsoredSize: 10,
258
+ sponsoredLineHeight: '13.4px',
259
+ sponsoredColor: 'rgb(255, 255, 255)',
260
+
261
+ // CTA button (sticker style)
262
+ ctaButtonBg: 'rgb(255, 255, 255)',
263
+ ctaButtonWidth: 145.648,
264
+ ctaButtonHeight: 29.6953,
265
+ ctaButtonRadius: 15,
266
+ ctaButtonPadding: '6px 12px 5.4px',
267
+ ctaButtonFont: '"Instagram Sans"',
268
+ ctaButtonSize: 27,
269
+ ctaButtonWeight: 400,
270
+ ctaButtonLineHeight: '29.7px',
271
+ ctaButtonColor: 'rgb(0, 0, 0)',
272
+ ctaIconColor: 'rgb(0, 149, 246)',
273
+
274
+ // Progress bar
275
+ progressTop: 6,
276
+ progressLeft: 4,
277
+ progressWidth: 310,
278
+ progressHeight: 2,
279
+ progressBg: 'rgba(255, 255, 255, 0.5)',
280
+ progressFillBg: 'rgb(255, 255, 255)',
281
+ progressRadius: 1,
282
+
283
+ // Caption text
284
+ captionFont: 'Tahoma, Helvetica, Arial, sans-serif',
285
+ captionSize: 13,
286
+ captionWeight: 700,
287
+ captionLineHeight: '18.2px',
288
+ captionColor: 'rgb(0, 0, 0)',
289
+ captionRadius: 4,
290
+ captionPadding: '2px 8px 5px',
291
+ captionWidth: 245.68,
292
+ captionTop: 10,
293
+ captionLeft: 26,
294
+
295
+ // Expanded view center info
296
+ centerTitleSize: 20,
297
+ centerTitleWeight: 700,
298
+ centerTitleLineHeight: '26.8px',
299
+ centerTitleColor: 'rgb(255, 255, 255)',
300
+ centerLinkSize: 12,
301
+ centerLinkWeight: 500,
302
+ centerLinkLineHeight: '16.08px',
303
+ centerLinkColor: 'rgb(255, 255, 255)',
304
+ };
305
+
306
+ const username = platform === 'instagram'
307
+ ? adData.brandName.toLowerCase().replace(/[^a-z0-9]/g, '')
308
+ : adData.brandName;
309
+
310
+ // Truncate text to 65 characters for Instagram Stories
311
+ const truncateInstagramText = (text: string, limit: number = 65) => {
312
+ if (!text || text.length <= limit) return text;
313
+ return text.substring(0, limit);
314
+ };
315
+
316
+ // Progress bar animation for Messenger and Facebook Stories
317
+ React.useEffect(() => {
318
+ if (platform === 'instagram') return;
319
+
320
+ const interval = setInterval(() => {
321
+ setProgressKey((prev) => prev + 1);
322
+ }, 7000); // Reset every 7 seconds
323
+
324
+ return () => clearInterval(interval);
325
+ }, [platform]);
326
+
327
+ // Instagram Stories animation cycle
328
+ React.useEffect(() => {
329
+ if (platform !== 'instagram') return;
330
+
331
+ const cycle = () => {
332
+ // Default view (0-5s)
333
+ setInstagramFrame('default');
334
+ setProgressKey((prev) => prev + 1);
335
+
336
+ setTimeout(() => {
337
+ // Expanded view (5-12s)
338
+ setInstagramFrame('expanded');
339
+ }, 5000);
340
+ };
341
+
342
+ // Start cycle immediately
343
+ cycle();
344
+
345
+ // Repeat every 12 seconds
346
+ const interval = setInterval(cycle, 12000);
347
+
348
+ return () => clearInterval(interval);
349
+ }, [platform]);
350
+
351
+ // Messenger Stories Layout
352
+ if (platform === 'messenger') {
353
+ return (
354
+ <div
355
+ className="meta-preview"
356
+ style={{
357
+ display: 'flex',
358
+ alignItems: 'center',
359
+ justifyContent: 'center',
360
+ width: '100%',
361
+ height: '100%',
362
+ fontFamily: fbStory.fontFamily,
363
+ fontSize: `${fbStory.baseFontSize}px`,
364
+ color: fbStory.baseColor,
365
+ lineHeight: fbStory.baseLineHeight,
366
+ }}
367
+ >
368
+ <div
369
+ style={{
370
+ position: 'relative',
371
+ width: `${fbStory.containerWidth}px`, maxWidth: '100%',
372
+ height: `${fbStory.containerHeight}px`,
373
+ backgroundColor: 'rgb(0, 0, 0)',
374
+ border: fbStory.containerBorder,
375
+ overflow: 'hidden',
376
+ }}
377
+ >
378
+ {/* Top scrim for progress bar / header visibility */}
379
+ <div
380
+ style={{
381
+ position: 'absolute',
382
+ top: 0,
383
+ left: 0,
384
+ right: 0,
385
+ height: '70px',
386
+ background: 'linear-gradient(to bottom, rgba(0,0,0,0.35) 0%, transparent 100%)',
387
+ zIndex: 15,
388
+ pointerEvents: 'none',
389
+ }}
390
+ />
391
+
392
+ {/* Progress Bar */}
393
+ <div
394
+ style={{
395
+ position: 'absolute',
396
+ top: `${fbStory.progressTop}px`,
397
+ left: `${fbStory.progressLeft}px`,
398
+ right: `${fbStory.progressRight}px`,
399
+ width: `${fbStory.progressWidth}px`,
400
+ height: `${fbStory.progressHeight}px`,
401
+ backgroundColor: fbStory.progressBg,
402
+ borderRadius: `${fbStory.progressRadius}px`,
403
+ zIndex: 25,
404
+ }}
405
+ >
406
+ <div
407
+ key={progressKey}
408
+ style={{
409
+ height: `${fbStory.progressHeight}px`,
410
+ backgroundColor: fbStory.progressFillBg,
411
+ borderRadius: `${fbStory.progressRadius}px`,
412
+ animation: 'progressBar 7s linear forwards',
413
+ }}
414
+ />
415
+ </div>
416
+
417
+ <style>{`
418
+ @keyframes progressBar {
419
+ from { width: 0%; }
420
+ to { width: 100%; }
421
+ }
422
+ `}</style>
423
+
424
+ {/* Story Content */}
425
+ {adData.creativeImage && (
426
+ <CreativeMedia
427
+ src={adData.creativeImage}
428
+ mediaType={adData.creativeMediaType}
429
+ alt="Story ad"
430
+ autoPlay
431
+ showPlayOverlay={false}
432
+ style={{
433
+ position: 'absolute',
434
+ top: 0,
435
+ left: 0,
436
+ width: `${fbStory.containerWidth}px`, maxWidth: '100%',
437
+ height: `${fbStory.containerHeight}px`,
438
+ objectFit: 'contain',
439
+ backgroundColor: 'rgb(0, 0, 0)',
440
+ }}
441
+ />
442
+ )}
443
+
444
+ {/* Header — shared component */}
445
+ <StoryHeader platform="messenger" profileImage={adData.profileImage} brandName={adData.brandName} username={username} isLight={false} />
446
+
447
+ {/* Bottom CTA */}
448
+ <div
449
+ style={{
450
+ position: 'absolute',
451
+ bottom: '20px',
452
+ left: '16px',
453
+ right: '16px',
454
+ zIndex: 10,
455
+ }}
456
+ >
457
+ {/* Up Arrow */}
458
+ <div style={{ display: 'flex', justifyContent: 'center', marginBottom: '12px' }}>
459
+ <ChevronUp style={{ width: '24px', height: '24px', color: 'rgb(255, 255, 255)' }} strokeWidth={3} />
460
+ </div>
461
+
462
+ {/* CTA Button - Pill shape, centered */}
463
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
464
+ <a
465
+ href={adData.websiteUrl || '#'}
466
+ target="_blank"
467
+ rel="noopener noreferrer"
468
+ style={{
469
+ display: 'inline-block',
470
+ padding: '8px 24px',
471
+ backgroundColor: 'rgb(255, 255, 255)',
472
+ color: 'rgb(0, 0, 0)',
473
+ borderRadius: '50px',
474
+ fontWeight: 600,
475
+ fontSize: '14px',
476
+ textDecoration: 'none',
477
+ }}
478
+ >
479
+ {adData.callToAction}
480
+ </a>
481
+ </div>
482
+ </div>
483
+
484
+ {/* Placeholder if no image */}
485
+ {!adData.creativeImage && (
486
+ <div
487
+ style={{
488
+ position: 'absolute',
489
+ inset: 0,
490
+ display: 'flex',
491
+ alignItems: 'center',
492
+ justifyContent: 'center',
493
+ }}
494
+ >
495
+ <div style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.6)' }}>
496
+ <div
497
+ style={{
498
+ width: '96px',
499
+ height: '96px',
500
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
501
+ borderRadius: '8px',
502
+ margin: '0 auto 8px',
503
+ }}
504
+ />
505
+ <p style={{ fontSize: '14px', margin: 0 }}>Messenger Story</p>
506
+ <p style={{ fontSize: '12px', margin: '4px 0 0' }}>9:16 Format</p>
507
+ </div>
508
+ </div>
509
+ )}
510
+ </div>
511
+ </div>
512
+ );
513
+ }
514
+
515
+ // Facebook Stories Layout
516
+ if (platform === 'facebook') {
517
+ return (
518
+ <div
519
+ className="meta-preview"
520
+ style={{
521
+ display: 'flex',
522
+ alignItems: 'center',
523
+ justifyContent: 'center',
524
+ width: '100%',
525
+ height: '100%',
526
+ fontFamily: fbStory.fontFamily,
527
+ fontSize: `${fbStory.baseFontSize}px`,
528
+ color: fbStory.baseColor,
529
+ lineHeight: fbStory.baseLineHeight,
530
+ }}
531
+ >
532
+ <div
533
+ style={{
534
+ position: 'relative',
535
+ width: `${fbStory.containerWidth}px`, maxWidth: '100%',
536
+ height: `${fbStory.containerHeight}px`,
537
+ backgroundImage: fbStory.backgroundGradient,
538
+ border: fbStory.containerBorder,
539
+ overflow: 'hidden',
540
+ }}
541
+ >
542
+ {/* Top dark scrim — ensures progress bar and header are visible on light backgrounds */}
543
+ <div
544
+ style={{
545
+ position: 'absolute',
546
+ top: 0,
547
+ left: 0,
548
+ right: 0,
549
+ height: '70px',
550
+ background: 'linear-gradient(to bottom, rgba(0,0,0,0.35) 0%, transparent 100%)',
551
+ zIndex: 15,
552
+ pointerEvents: 'none',
553
+ }}
554
+ />
555
+
556
+ {/* Progress Bar */}
557
+ <div
558
+ style={{
559
+ position: 'absolute',
560
+ top: `${fbStory.progressTop}px`,
561
+ left: `${fbStory.progressLeft}px`,
562
+ right: `${fbStory.progressRight}px`,
563
+ width: `${fbStory.progressWidth}px`,
564
+ height: `${fbStory.progressHeight}px`,
565
+ backgroundColor: fbStory.progressBg,
566
+ borderRadius: `${fbStory.progressRadius}px`,
567
+ zIndex: 25,
568
+ }}
569
+ >
570
+ <div
571
+ key={progressKey}
572
+ style={{
573
+ height: `${fbStory.progressHeight}px`,
574
+ backgroundColor: fbStory.progressFillBg,
575
+ borderRadius: `${fbStory.progressRadius}px`,
576
+ animation: 'progressBar 7s linear forwards',
577
+ }}
578
+ />
579
+ </div>
580
+
581
+ <style>{`
582
+ @keyframes progressBar {
583
+ from { width: 0%; }
584
+ to { width: 100%; }
585
+ }
586
+ `}</style>
587
+
588
+ {/* Header — shared component */}
589
+ <StoryHeader platform="facebook" profileImage={adData.profileImage} brandName={adData.brandName} username={username} isLight={false} />
590
+
591
+ {/* Bottom gradient overlay */}
592
+ <div
593
+ style={{
594
+ position: 'absolute',
595
+ top: `${fbStory.bottomGradientTop}px`,
596
+ width: `${fbStory.bottomGradientWidth}px`,
597
+ height: `${fbStory.bottomGradientHeight}px`,
598
+ backgroundImage: fbStory.bottomGradient,
599
+ zIndex: 5,
600
+ }}
601
+ />
602
+
603
+ {/* Story Image - centered for 1:1 */}
604
+ {adData.creativeImage && (
605
+ <CreativeMedia
606
+ src={adData.creativeImage}
607
+ mediaType={adData.creativeMediaType}
608
+ alt="Story ad"
609
+ autoPlay
610
+ showPlayOverlay={false}
611
+ style={{
612
+ position: 'relative',
613
+ top: `${fbStory.imageTop}px`,
614
+ width: `${fbStory.imageWidth}px`,
615
+ height: `${fbStory.imageHeight}px`,
616
+ objectFit: 'cover',
617
+ display: 'block',
618
+ }}
619
+ />
620
+ )}
621
+
622
+ {/* Primary Text Overlay */}
623
+ <div
624
+ style={{
625
+ position: 'absolute',
626
+ top: `${fbStory.textTop}px`,
627
+ left: `${fbStory.textLeft}px`,
628
+ right: `${fbStory.textRight}px`,
629
+ width: `${fbStory.textWidth}px`,
630
+ maxWidth: `${fbStory.textMaxWidth}px`,
631
+ color: fbStory.textColor,
632
+ fontFamily: fbStory.textFont,
633
+ fontSize: `${fbStory.textSize}px`,
634
+ fontWeight: fbStory.textWeight,
635
+ lineHeight: fbStory.textLineHeight,
636
+ textAlign: 'left',
637
+ zIndex: 10,
638
+ }}
639
+ >
640
+ {adData.primaryText && (
641
+ <span style={{ marginLeft: '11px' }}>
642
+ {adData.primaryText.length > 100
643
+ ? adData.primaryText.substring(0, 100) + '...'
644
+ : adData.primaryText}
645
+ </span>
646
+ )}
647
+ </div>
648
+
649
+ {/* Bottom CTA Area */}
650
+ <div
651
+ style={{
652
+ position: 'absolute',
653
+ bottom: '20px',
654
+ left: '16px',
655
+ right: '16px',
656
+ zIndex: 10,
657
+ }}
658
+ >
659
+ {/* Up Arrow */}
660
+ <div style={{ display: 'flex', justifyContent: 'center', marginBottom: '12px' }}>
661
+ <ChevronUp style={{ width: '24px', height: '24px', color: 'rgb(255, 255, 255)' }} strokeWidth={3} />
662
+ </div>
663
+
664
+ {/* CTA Button - Rounded corners, centered */}
665
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
666
+ <a
667
+ href={adData.websiteUrl || '#'}
668
+ target="_blank"
669
+ rel="noopener noreferrer"
670
+ style={{
671
+ display: 'inline-flex',
672
+ alignItems: 'center',
673
+ gap: '8px',
674
+ padding: '10px 20px',
675
+ backgroundColor: 'rgb(255, 255, 255)',
676
+ color: 'rgb(56, 88, 152)',
677
+ borderRadius: '8px',
678
+ fontWeight: 600,
679
+ fontSize: '14px',
680
+ textDecoration: 'none',
681
+ }}
682
+ >
683
+ <Link2 style={{ width: '20px', height: '20px' }} />
684
+ <span>{adData.callToAction}</span>
685
+ </a>
686
+ </div>
687
+ </div>
688
+
689
+ {/* Placeholder if no image */}
690
+ {!adData.creativeImage && (
691
+ <div
692
+ style={{
693
+ position: 'absolute',
694
+ inset: 0,
695
+ display: 'flex',
696
+ alignItems: 'center',
697
+ justifyContent: 'center',
698
+ }}
699
+ >
700
+ <div style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.6)' }}>
701
+ <div
702
+ style={{
703
+ width: '96px',
704
+ height: '96px',
705
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
706
+ borderRadius: '8px',
707
+ margin: '0 auto 8px',
708
+ }}
709
+ />
710
+ <p style={{ fontSize: '14px', margin: 0 }}>Facebook Story</p>
711
+ <p style={{ fontSize: '12px', margin: '4px 0 0' }}>9:16 Format</p>
712
+ </div>
713
+ </div>
714
+ )}
715
+ </div>
716
+ </div>
717
+ );
718
+ }
719
+
720
+ // Instagram Stories Layout
721
+ return (
722
+ <div
723
+ className="meta-preview"
724
+ style={{
725
+ display: 'flex',
726
+ alignItems: 'center',
727
+ justifyContent: 'center',
728
+ width: '100%',
729
+ height: '100%',
730
+ fontFamily: igStory.fontFamily,
731
+ fontSize: '12px',
732
+ color: igStory.baseColor,
733
+ }}
734
+ >
735
+ <div
736
+ style={{
737
+ position: 'relative',
738
+ width: `${igStory.containerWidth}px`, maxWidth: '100%',
739
+ height: `${igStory.containerHeight}px`,
740
+ padding: '1px',
741
+ }}
742
+ >
743
+ <div
744
+ id="instagram_story_preview_frame"
745
+ style={{
746
+ position: 'relative',
747
+ width: `${igStory.innerWidth}px`, maxWidth: '100%',
748
+ height: `${igStory.innerHeight}px`,
749
+ backgroundImage: igStory.backgroundGradient,
750
+ border: igStory.containerBorder,
751
+ overflow: 'hidden',
752
+ }}
753
+ >
754
+ {/* Top scrim for progress bar / header visibility + bottom scrim for text */}
755
+ <div
756
+ style={{
757
+ position: 'absolute',
758
+ top: 0,
759
+ left: 0,
760
+ right: 0,
761
+ height: '70px',
762
+ background: 'linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, transparent 100%)',
763
+ zIndex: 15,
764
+ pointerEvents: 'none',
765
+ }}
766
+ />
767
+ <div
768
+ style={{
769
+ position: 'absolute',
770
+ bottom: 0,
771
+ left: 0,
772
+ right: 0,
773
+ height: '120px',
774
+ background: 'linear-gradient(to top, rgba(0,0,0,0.25) 0%, transparent 100%)',
775
+ zIndex: 1,
776
+ pointerEvents: 'none',
777
+ }}
778
+ />
779
+
780
+ {/* Progress Bar - exact Instagram positioning */}
781
+ <div
782
+ style={{
783
+ position: 'absolute',
784
+ top: `${igStory.progressTop}px`,
785
+ left: `${igStory.progressLeft}px`,
786
+ width: `${igStory.progressWidth}px`,
787
+ height: `${igStory.progressHeight}px`,
788
+ backgroundColor: igStory.progressBg,
789
+ borderRadius: `${igStory.progressRadius}px`,
790
+ zIndex: 20,
791
+ }}
792
+ >
793
+ <div
794
+ key={progressKey}
795
+ style={{
796
+ height: `${igStory.progressHeight}px`,
797
+ backgroundColor: igStory.progressFillBg,
798
+ borderRadius: `${igStory.progressRadius}px`,
799
+ animation: 'progressBar 12s linear forwards',
800
+ }}
801
+ />
802
+ </div>
803
+
804
+ <style>{`
805
+ @keyframes progressBar {
806
+ from { width: 0%; }
807
+ to { width: 100%; }
808
+ }
809
+ @keyframes fadeUp {
810
+ from {
811
+ opacity: 0;
812
+ transform: translateY(20px);
813
+ }
814
+ to {
815
+ opacity: 1;
816
+ transform: translateY(0);
817
+ }
818
+ }
819
+ @keyframes fadeIn {
820
+ from { opacity: 0; }
821
+ to { opacity: 1; }
822
+ }
823
+ `}</style>
824
+
825
+ {/* Header — shared component */}
826
+ {/* Header always white — top scrim ensures visibility */}
827
+ <StoryHeader platform="instagram" profileImage={adData.profileImage} brandName={adData.brandName} username={username} isLight={false} />
828
+
829
+ {/* Story Image - centered at top: 50px margin */}
830
+ <div
831
+ style={{
832
+ position: 'relative',
833
+ width: `${igStory.innerWidth}px`, maxWidth: '100%',
834
+ marginTop: '50px',
835
+ zIndex: 5,
836
+ }}
837
+ >
838
+ {adData.creativeImage ? (
839
+ <CreativeMedia
840
+ src={adData.creativeImage}
841
+ mediaType={adData.creativeMediaType}
842
+ alt="Story ad"
843
+ autoPlay
844
+ showPlayOverlay={false}
845
+ style={{
846
+ position: 'relative',
847
+ width: `${igStory.innerWidth}px`, maxWidth: '100%',
848
+ height: `${igStory.innerWidth}px`,
849
+ objectFit: 'cover',
850
+ display: 'block',
851
+ }}
852
+ />
853
+ ) : (
854
+ <div
855
+ style={{
856
+ width: `${igStory.innerWidth}px`, maxWidth: '100%',
857
+ height: `${igStory.innerWidth}px`,
858
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
859
+ display: 'flex',
860
+ alignItems: 'center',
861
+ justifyContent: 'center',
862
+ }}
863
+ >
864
+ <div style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.6)' }}>
865
+ <div
866
+ style={{
867
+ width: '96px',
868
+ height: '96px',
869
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
870
+ borderRadius: '8px',
871
+ margin: '0 auto 8px',
872
+ }}
873
+ />
874
+ <p style={{ fontSize: '14px', margin: 0 }}>Instagram Story</p>
875
+ <p style={{ fontSize: '12px', margin: '4px 0 0' }}>9:16 Format</p>
876
+ </div>
877
+ </div>
878
+ )}
879
+ </div>
880
+
881
+ {/* Caption/Primary Text Area */}
882
+ {adData.primaryText && (
883
+ <div
884
+ style={{
885
+ position: 'absolute',
886
+ top: '400px',
887
+ left: `${igStory.captionLeft}px`,
888
+ width: `${igStory.captionWidth}px`,
889
+ padding: igStory.captionPadding,
890
+ fontFamily: igStory.captionFont,
891
+ fontSize: `${igStory.captionSize}px`,
892
+ fontWeight: igStory.captionWeight,
893
+ lineHeight: igStory.captionLineHeight,
894
+ color: igStory.captionColor,
895
+ borderRadius: `${igStory.captionRadius}px`,
896
+ zIndex: 10,
897
+ }}
898
+ >
899
+ {isTextExpanded ? (
900
+ adData.primaryText
901
+ ) : (
902
+ <>
903
+ {truncateInstagramText(adData.primaryText, 50)}
904
+ {adData.primaryText.length > 50 && (
905
+ <button
906
+ onClick={() => setIsTextExpanded(true)}
907
+ style={{
908
+ display: 'inline-block',
909
+ background: 'none',
910
+ border: 'none',
911
+ color: igStory.captionColor,
912
+ fontFamily: igStory.captionFont,
913
+ fontSize: `${igStory.captionSize}px`,
914
+ fontWeight: 700,
915
+ cursor: 'pointer',
916
+ opacity: 0.8,
917
+ }}
918
+ >
919
+ more
920
+ </button>
921
+ )}
922
+ </>
923
+ )}
924
+ </div>
925
+ )}
926
+
927
+ {/* CTA Button Sticker - visible in default view */}
928
+ {instagramFrame === 'default' && (
929
+ <div
930
+ style={{
931
+ position: 'absolute',
932
+ bottom: '68px',
933
+ left: '50%',
934
+ transform: 'translateX(-50%)',
935
+ zIndex: 10,
936
+ }}
937
+ >
938
+ <div
939
+ style={{
940
+ display: 'flex',
941
+ alignItems: 'center',
942
+ justifyContent: 'center',
943
+ padding: igStory.ctaButtonPadding,
944
+ backgroundColor: igStory.ctaButtonBg,
945
+ borderRadius: `${igStory.ctaButtonRadius}px`,
946
+ color: igStory.ctaButtonColor,
947
+ fontFamily: igStory.ctaButtonFont,
948
+ fontSize: '14px',
949
+ fontWeight: igStory.ctaButtonWeight,
950
+ textAlign: 'center',
951
+ }}
952
+ >
953
+ <div style={{ marginRight: '6px', color: igStory.ctaIconColor }}>
954
+ <Link2 style={{ width: '16px', height: '16px' }} />
955
+ </div>
956
+ {adData.callToAction}
957
+ </div>
958
+ </div>
959
+ )}
960
+
961
+ {/* End scene — dark overlay with centered profile, name, URL, CTA */}
962
+ {instagramFrame === 'expanded' && (
963
+ <>
964
+ {/* Dark overlay that fades in */}
965
+ <div
966
+ style={{
967
+ position: 'absolute',
968
+ inset: 0,
969
+ backgroundColor: 'rgba(0, 0, 0, 0.55)',
970
+ zIndex: 30,
971
+ animation: 'fadeIn 1.5s ease-out forwards',
972
+ pointerEvents: 'none',
973
+ }}
974
+ />
975
+
976
+ {/* Centered content */}
977
+ <div
978
+ style={{
979
+ position: 'absolute',
980
+ top: 0,
981
+ left: 0,
982
+ right: 0,
983
+ bottom: '66px',
984
+ display: 'flex',
985
+ flexDirection: 'column',
986
+ alignItems: 'center',
987
+ justifyContent: 'center',
988
+ zIndex: 35,
989
+ animation: 'fadeUp 0.8s ease-out 0.3s both',
990
+ }}
991
+ >
992
+ {/* Profile image */}
993
+ <div
994
+ style={{
995
+ width: '64px',
996
+ height: '64px',
997
+ borderRadius: '50%',
998
+ overflow: 'hidden',
999
+ marginBottom: '12px',
1000
+ border: '2px solid rgba(255, 255, 255, 0.3)',
1001
+ }}
1002
+ >
1003
+ {adData.profileImage ? (
1004
+ <img
1005
+ src={adData.profileImage}
1006
+ alt={adData.brandName}
1007
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
1008
+ onError={(e) => { e.currentTarget.style.display = 'none'; }}
1009
+ />
1010
+ ) : (
1011
+ <div
1012
+ style={{
1013
+ width: '100%',
1014
+ height: '100%',
1015
+ display: 'flex',
1016
+ alignItems: 'center',
1017
+ justifyContent: 'center',
1018
+ backgroundColor: 'rgb(80, 80, 80)',
1019
+ color: 'white',
1020
+ fontSize: '24px',
1021
+ fontWeight: 600,
1022
+ }}
1023
+ >
1024
+ {adData.brandName.charAt(0).toUpperCase()}
1025
+ </div>
1026
+ )}
1027
+ </div>
1028
+
1029
+ {/* Page name */}
1030
+ <div
1031
+ style={{
1032
+ fontSize: '15px',
1033
+ fontWeight: 600,
1034
+ color: 'rgb(255, 255, 255)',
1035
+ textAlign: 'center',
1036
+ marginBottom: '2px',
1037
+ }}
1038
+ >
1039
+ {adData.brandName}
1040
+ </div>
1041
+
1042
+ {/* Display URL */}
1043
+ <div
1044
+ style={{
1045
+ fontSize: '12px',
1046
+ fontWeight: 400,
1047
+ color: 'rgba(255, 255, 255, 0.7)',
1048
+ textAlign: 'center',
1049
+ marginBottom: '16px',
1050
+ }}
1051
+ >
1052
+ {adData.displayLink}
1053
+ </div>
1054
+
1055
+ {/* CTA Button */}
1056
+ <div
1057
+ style={{
1058
+ display: 'flex',
1059
+ alignItems: 'center',
1060
+ justifyContent: 'center',
1061
+ padding: '8px 20px',
1062
+ backgroundColor: 'rgb(255, 255, 255)',
1063
+ borderRadius: '20px',
1064
+ color: 'rgb(0, 0, 0)',
1065
+ fontSize: '13px',
1066
+ fontWeight: 600,
1067
+ gap: '6px',
1068
+ }}
1069
+ >
1070
+ <Link2 style={{ width: '14px', height: '14px' }} />
1071
+ {adData.callToAction}
1072
+ </div>
1073
+ </div>
1074
+ </>
1075
+ )}
1076
+
1077
+ {/* Bottom bar — Ad label + action icons (2026 IG Stories update) */}
1078
+ <div
1079
+ style={{
1080
+ position: 'absolute',
1081
+ bottom: 0,
1082
+ left: 0,
1083
+ right: 0,
1084
+ height: '48px',
1085
+ minHeight: '48px',
1086
+ display: 'flex',
1087
+ alignItems: 'center',
1088
+ justifyContent: 'space-between',
1089
+ paddingLeft: '16px',
1090
+ paddingRight: '16px',
1091
+ backgroundColor: 'rgb(0, 0, 0)',
1092
+ zIndex: 25,
1093
+ }}
1094
+ >
1095
+ <span
1096
+ style={{
1097
+ color: 'rgb(255, 255, 255)',
1098
+ fontSize: '12px',
1099
+ lineHeight: '18px',
1100
+ }}
1101
+ >
1102
+ Ad
1103
+ </span>
1104
+ <div
1105
+ style={{
1106
+ display: 'flex',
1107
+ alignItems: 'center',
1108
+ gap: '12px',
1109
+ }}
1110
+ >
1111
+ {/* Heart */}
1112
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1113
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
1114
+ </svg>
1115
+ {/* Comment */}
1116
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1117
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
1118
+ </svg>
1119
+ {/* Send / Share */}
1120
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1121
+ <line x1="22" y1="2" x2="11" y2="13" />
1122
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
1123
+ </svg>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+ </div>
1128
+ </div>
1129
+ );
1130
+ };
1131
+
1132
+ MetaStoryPreview.displayName = 'MetaStoryPreview';