@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,294 @@
1
+ import { memo, useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import type { PreviewAdData } from './types'
4
+ import { CreativeMedia } from './CreativeMedia'
5
+ import { ProfileAvatar } from './ProfileAvatar'
6
+ import { withPreviewDefaults } from './defaults'
7
+
8
+ /**
9
+ * Facebook Feed — single image (sponsored). GREENFIELD rebuild.
10
+ *
11
+ * Not the Creative-Hub export extraction. Faithful to the real FB feed ad chrome
12
+ * but built cleanly: a 320px card with natural full-width flow (no fixed
13
+ * 300px / containerWidth-24 inner rows, no divider margins), Meta-exact hex
14
+ * (self-contained — no design tokens), a system font stack. Layout, top → bottom:
15
+ * header · primary text · creative · link card · divider · action bar
16
+ */
17
+
18
+ interface MetaFeedPreviewProps {
19
+ adData: PreviewAdData
20
+ format: '1:1' | '4:5' | '9:16'
21
+ }
22
+
23
+ const WIDTH = 320
24
+ const FONT =
25
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
26
+
27
+ const C = {
28
+ name: 'rgb(8, 8, 8)',
29
+ sub: 'rgb(101, 103, 107)',
30
+ body: 'rgb(0, 0, 0)',
31
+ seeMore: '#1877F2',
32
+ imageBg: '#F0F2F5',
33
+ imageEdge: 'rgba(0, 0, 0, 0.06)',
34
+ card: '#F7F8FA',
35
+ link: 'rgb(101, 103, 107)',
36
+ headline: 'rgb(28, 32, 40)',
37
+ desc: 'rgb(101, 103, 107)',
38
+ ctaBtnBg: 'rgb(228, 230, 235)',
39
+ ctaBtnInk: 'rgb(8, 8, 9)',
40
+ divider: 'rgb(219, 221, 224)',
41
+ action: 'rgb(97, 103, 112)',
42
+ }
43
+
44
+ export const MetaFeedPreview = memo(function MetaFeedPreview({
45
+ adData: rawAdData,
46
+ format,
47
+ }: MetaFeedPreviewProps) {
48
+ const adData = withPreviewDefaults(rawAdData)
49
+ const [expanded, setExpanded] = useState(false)
50
+
51
+ const imageHeight = format === '4:5' ? 400 : WIDTH // 1:1 → 320, 4:5 → 400
52
+ const text = adData.primaryText ?? ''
53
+ const LIMIT = 125
54
+ const clipped = text.length > LIMIT && !expanded
55
+
56
+ return (
57
+ <div style={{ width: WIDTH, background: '#fff', fontFamily: FONT, color: C.body }}>
58
+ {/* Header */}
59
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px' }}>
60
+ <div style={{ width: 40, height: 40, borderRadius: '50%', overflow: 'hidden', flexShrink: 0 }}>
61
+ <ProfileAvatar src={adData.profileImage} name={adData.brandName} fontSize={15} />
62
+ </div>
63
+ <div style={{ flex: 1, minWidth: 0 }}>
64
+ <div
65
+ style={{
66
+ fontSize: 14,
67
+ fontWeight: 600,
68
+ lineHeight: '18px',
69
+ color: C.name,
70
+ overflow: 'hidden',
71
+ textOverflow: 'ellipsis',
72
+ whiteSpace: 'nowrap',
73
+ }}
74
+ >
75
+ {adData.brandName}
76
+ </div>
77
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 1, color: C.sub }}>
78
+ <span style={{ fontSize: 12, lineHeight: '15px' }}>Sponsored</span>
79
+ <span style={{ fontSize: 12 }}>·</span>
80
+ <Globe />
81
+ </div>
82
+ </div>
83
+ <Dots />
84
+ </div>
85
+
86
+ {/* Primary text */}
87
+ {text && (
88
+ <div style={{ padding: '0 12px 10px', fontSize: 14, fontWeight: 400, lineHeight: '19px', color: C.body, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
89
+ {clipped ? text.slice(0, LIMIT).trimEnd() : text}
90
+ {clipped && (
91
+ <>
92
+ {'… '}
93
+ <button
94
+ type="button"
95
+ onClick={() => setExpanded(true)}
96
+ style={{ border: 0, background: 'none', padding: 0, color: C.seeMore, fontWeight: 600, fontSize: 14, cursor: 'pointer', fontFamily: FONT }}
97
+ >
98
+ See more
99
+ </button>
100
+ </>
101
+ )}
102
+ </div>
103
+ )}
104
+
105
+ {/* Creative */}
106
+ <div
107
+ style={{
108
+ width: WIDTH,
109
+ height: imageHeight,
110
+ background: C.imageBg,
111
+ borderTop: `1px solid ${C.imageEdge}`,
112
+ borderBottom: `1px solid ${C.imageEdge}`,
113
+ overflow: 'hidden',
114
+ }}
115
+ >
116
+ {adData.creativeImage ? (
117
+ <CreativeMedia
118
+ src={adData.creativeImage}
119
+ mediaType={adData.creativeMediaType}
120
+ alt=""
121
+ style={{ width: WIDTH, height: imageHeight, objectFit: 'cover', display: 'block' }}
122
+ />
123
+ ) : (
124
+ <Placeholder format={format} />
125
+ )}
126
+ </div>
127
+
128
+ {/* Link card */}
129
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 12, background: C.card }}>
130
+ <div style={{ flex: 1, minWidth: 0 }}>
131
+ {adData.displayLink && (
132
+ <div
133
+ style={{
134
+ fontSize: 12,
135
+ lineHeight: '16px',
136
+ color: C.link,
137
+ textTransform: 'uppercase',
138
+ overflow: 'hidden',
139
+ textOverflow: 'ellipsis',
140
+ whiteSpace: 'nowrap',
141
+ }}
142
+ >
143
+ {adData.displayLink}
144
+ </div>
145
+ )}
146
+ {adData.headline && (
147
+ <div
148
+ style={{
149
+ fontSize: 15,
150
+ fontWeight: 700,
151
+ lineHeight: '19px',
152
+ color: C.headline,
153
+ marginTop: 1,
154
+ overflow: 'hidden',
155
+ textOverflow: 'ellipsis',
156
+ display: '-webkit-box',
157
+ WebkitLineClamp: 2,
158
+ WebkitBoxOrient: 'vertical',
159
+ }}
160
+ >
161
+ {adData.headline}
162
+ </div>
163
+ )}
164
+ {adData.description && (
165
+ <div
166
+ style={{
167
+ fontSize: 13,
168
+ lineHeight: '17px',
169
+ color: C.desc,
170
+ marginTop: 2,
171
+ overflow: 'hidden',
172
+ textOverflow: 'ellipsis',
173
+ whiteSpace: 'nowrap',
174
+ }}
175
+ >
176
+ {adData.description}
177
+ </div>
178
+ )}
179
+ </div>
180
+ <div
181
+ style={{
182
+ flexShrink: 0,
183
+ display: 'inline-flex',
184
+ alignItems: 'center',
185
+ height: 36,
186
+ padding: '0 14px',
187
+ borderRadius: 6,
188
+ background: C.ctaBtnBg,
189
+ fontSize: 14,
190
+ fontWeight: 600,
191
+ color: C.ctaBtnInk,
192
+ whiteSpace: 'nowrap',
193
+ cursor: 'pointer',
194
+ }}
195
+ >
196
+ {adData.callToAction || 'Learn more'}
197
+ </div>
198
+ </div>
199
+
200
+ {/* Divider */}
201
+ <div style={{ height: 1, background: C.divider }} />
202
+
203
+ {/* Action bar */}
204
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-around', padding: '6px 8px' }}>
205
+ <Action label="Like"><ThumbsUp /></Action>
206
+ <Action label="Comment"><Bubble /></Action>
207
+ <Action label="Share"><ShareArrow /></Action>
208
+ </div>
209
+ </div>
210
+ )
211
+ })
212
+
213
+ /* ------------------------------- icons / bits ------------------------------- */
214
+
215
+ const stroke = {
216
+ fill: 'none',
217
+ stroke: C.action,
218
+ strokeWidth: 2,
219
+ strokeLinecap: 'round' as const,
220
+ strokeLinejoin: 'round' as const,
221
+ }
222
+
223
+ function Action({ label, children }: { label: string; children: ReactNode }) {
224
+ return (
225
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', cursor: 'pointer' }}>
226
+ {children}
227
+ <span style={{ fontSize: 13, fontWeight: 600, color: C.action }}>{label}</span>
228
+ </div>
229
+ )
230
+ }
231
+
232
+ function Dots() {
233
+ return (
234
+ <svg width="20" height="20" viewBox="0 0 24 24" fill={C.sub} style={{ display: 'block', flexShrink: 0 }}>
235
+ <circle cx="5" cy="12" r="1.6" />
236
+ <circle cx="12" cy="12" r="1.6" />
237
+ <circle cx="19" cy="12" r="1.6" />
238
+ </svg>
239
+ )
240
+ }
241
+
242
+ function Globe() {
243
+ return (
244
+ <svg width="12" height="12" viewBox="0 0 16 16" fill={C.sub} style={{ display: 'block' }}>
245
+ <path d="M8 0a8 8 0 1 0 8 8 8 8 0 0 0-8-8zm5.9 7H11a13 13 0 0 0-.9-4.4A6 6 0 0 1 13.9 7zM8 14c-.7 0-1.8-1.8-2-5h4c-.2 3.2-1.3 5-2 5zM6 7c.2-3.2 1.3-5 2-5s1.8 1.8 2 5zm-.1-4.4A13 13 0 0 0 5 7H2.1a6 6 0 0 1 3.8-4.4zM2.1 9H5a13 13 0 0 0 .9 4.4A6 6 0 0 1 2.1 9zm8 4.4a13 13 0 0 0 .9-4.4h2.9a6 6 0 0 1-3.8 4.4z" />
246
+ </svg>
247
+ )
248
+ }
249
+
250
+ function ThumbsUp() {
251
+ return (
252
+ <svg width="18" height="18" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
253
+ <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
254
+ </svg>
255
+ )
256
+ }
257
+
258
+ function Bubble() {
259
+ return (
260
+ <svg width="18" height="18" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
261
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
262
+ </svg>
263
+ )
264
+ }
265
+
266
+ function ShareArrow() {
267
+ return (
268
+ <svg width="18" height="18" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
269
+ <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
270
+ <polyline points="16 6 12 2 8 6" />
271
+ <line x1="12" y1="2" x2="12" y2="15" />
272
+ </svg>
273
+ )
274
+ }
275
+
276
+ function Placeholder({ format }: { format: string }) {
277
+ return (
278
+ <div
279
+ style={{
280
+ width: '100%',
281
+ height: '100%',
282
+ display: 'flex',
283
+ flexDirection: 'column',
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ gap: 8,
287
+ color: 'rgb(140, 140, 140)',
288
+ }}
289
+ >
290
+ <div style={{ width: 56, height: 56, borderRadius: 10, background: '#E4E6EB' }} />
291
+ <span style={{ fontSize: 13 }}>Ad creative · {format}</span>
292
+ </div>
293
+ )
294
+ }
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import { ThumbsUp, MessageCircle, Share2, MoreHorizontal, X } from 'lucide-react';
3
+ import { CreativeMedia } from './CreativeMedia';
4
+ import type { PreviewAdData } from './types';
5
+
6
+ interface MetaInstreamPreviewProps {
7
+ adData: PreviewAdData;
8
+ }
9
+
10
+ export const MetaInstreamPreview: React.FC<MetaInstreamPreviewProps> = ({
11
+ adData,
12
+ }) => {
13
+ // Truncate text helper
14
+ const truncateText = (text: string, limit: number) => {
15
+ if (!text || text.length <= limit) return text;
16
+ return text.substring(0, limit);
17
+ };
18
+
19
+ return (
20
+ <div className="meta-preview w-full h-full">
21
+ <div
22
+ className="bg-white rounded-md overflow-hidden w-full h-full flex flex-col"
23
+ style={{}}
24
+ >
25
+ {/* Skeleton Header */}
26
+ <div className="p-3 flex items-center gap-2.5">
27
+ <div className="w-10 h-10 rounded-full bg-gray-200 animate-pulse flex-shrink-0" />
28
+ <div className="flex-1 min-w-0">
29
+ <div className="h-3 bg-gray-200 rounded w-32 mb-2 animate-pulse" />
30
+ <div className="h-2 bg-gray-200 rounded w-24 animate-pulse" />
31
+ </div>
32
+ <button className="w-8 h-8 flex items-center justify-center text-gray-400">
33
+ <MoreHorizontal className="w-5 h-5" />
34
+ </button>
35
+ </div>
36
+
37
+ {/* Main Video/Image Creative - Square 1:1 */}
38
+ <div className="relative w-full" style={{ paddingBottom: '100%' }}>
39
+ {adData.creativeImage ? (
40
+ <CreativeMedia
41
+ src={adData.creativeImage}
42
+ mediaType={adData.creativeMediaType}
43
+ alt="In-stream ad"
44
+ className="absolute inset-0 w-full h-full object-cover"
45
+ autoPlay
46
+ />
47
+ ) : (
48
+ <div className="absolute inset-0 bg-gray-100 flex items-center justify-center">
49
+ <div className="text-center text-gray-400">
50
+ <div className="w-16 h-16 bg-gray-200 rounded-lg mx-auto mb-2" />
51
+ <p className="text-sm">1:1 Video</p>
52
+ </div>
53
+ </div>
54
+ )}
55
+ </div>
56
+
57
+ {/* Thumbnail Section */}
58
+ <div className="p-3 border-t border-gray-200">
59
+ <div className="flex gap-3">
60
+ {/* Thumbnail */}
61
+ <div className="w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden bg-gray-100">
62
+ {adData.creativeImage ? (
63
+ <CreativeMedia
64
+ src={adData.creativeImage}
65
+ mediaType={adData.creativeMediaType}
66
+ alt="Thumbnail"
67
+ className="w-full h-full object-cover"
68
+ showPlayOverlay={false}
69
+ />
70
+ ) : (
71
+ <div className="w-full h-full bg-gray-200" />
72
+ )}
73
+ </div>
74
+
75
+ {/* Ad Content */}
76
+ <div className="flex-1 min-w-0">
77
+ {/* Headline */}
78
+ <h3 className="font-semibold text-sm text-gray-900 mb-1 line-clamp-2">
79
+ {truncateText(adData.headline, 60)}...
80
+ </h3>
81
+
82
+ {/* Sponsored + Display Link */}
83
+ <div className="flex items-center gap-1 text-xs text-gray-500 mb-1">
84
+ <span>Sponsored</span>
85
+ <span>·</span>
86
+ <span className="truncate">{adData.displayLink}</span>
87
+ </div>
88
+
89
+ {/* Primary Text - truncated */}
90
+ <p className="text-xs text-gray-600 line-clamp-2">
91
+ {truncateText(adData.primaryText, 80)}...
92
+ </p>
93
+ </div>
94
+
95
+ {/* X and Ellipsis buttons */}
96
+ <div className="flex flex-col gap-1 flex-shrink-0">
97
+ <button className="w-6 h-6 flex items-center justify-center text-gray-400 hover:text-gray-600">
98
+ <X className="w-4 h-4" />
99
+ </button>
100
+ <button className="w-6 h-6 flex items-center justify-center text-gray-400 hover:text-gray-600">
101
+ <MoreHorizontal className="w-4 h-4" />
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ {/* Skeleton engagement section */}
108
+ <div className="px-3 py-2 border-t border-gray-200">
109
+ <div className="flex items-center gap-4">
110
+ <div className="h-2 bg-gray-200 rounded w-16 animate-pulse" />
111
+ <div className="h-2 bg-gray-200 rounded w-20 animate-pulse" />
112
+ </div>
113
+ </div>
114
+
115
+ {/* Action Buttons */}
116
+ <div className="px-3 py-2 border-t border-gray-200">
117
+ <div className="flex items-center justify-around">
118
+ <button className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:bg-gray-50 rounded-md transition-colors">
119
+ <ThumbsUp className="w-5 h-5" />
120
+ <span className="text-sm font-medium">Like</span>
121
+ </button>
122
+ <button className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:bg-gray-50 rounded-md transition-colors">
123
+ <MessageCircle className="w-5 h-5" />
124
+ <span className="text-sm font-medium">Comment</span>
125
+ </button>
126
+ <button className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:bg-gray-50 rounded-md transition-colors">
127
+ <Share2 className="w-5 h-5" />
128
+ <span className="text-sm font-medium">Share</span>
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ );
135
+ };