@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,340 @@
1
+ import React from 'react';
2
+ import type { PreviewAdData } from './types';
3
+ import { CreativeMedia } from './CreativeMedia';
4
+ import { ProfileAvatar } from './ProfileAvatar';
5
+
6
+ interface InstagramExplorePreviewProps {
7
+ adData: PreviewAdData;
8
+ }
9
+
10
+ export const InstagramExplorePreview: React.FC<InstagramExplorePreviewProps> = ({
11
+ adData,
12
+ }) => {
13
+ return (
14
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
15
+ <div
16
+ style={{
17
+ color: 'rgb(28, 30, 33)',
18
+ fontSize: '12px',
19
+ lineHeight: '16px',
20
+ position: 'relative',
21
+ width: '320px',
22
+ maxWidth: '100%',
23
+ backgroundColor: 'rgb(255, 255, 255)',
24
+ }}
25
+ >
26
+ <div style={{ height: '564px', overflow: 'hidden', width: '320px', maxWidth: '100%' }}>
27
+ <div
28
+ style={{
29
+ flexWrap: 'wrap',
30
+ height: '566px',
31
+ justifyContent: 'space-between',
32
+ width: '320px', maxWidth: '100%',
33
+ display: 'flex',
34
+ }}
35
+ >
36
+ {/* Column 1 - Single tall tile */}
37
+ <div
38
+ style={{
39
+ backgroundColor: 'rgb(242, 242, 242)',
40
+ height: '174px',
41
+ marginBottom: '2px',
42
+ position: 'relative',
43
+ width: '106px',
44
+ }}
45
+ />
46
+
47
+ {/* Column 2 - Two tiles */}
48
+ <div style={{ height: '176px', width: '106px' }}>
49
+ <div
50
+ style={{
51
+ backgroundColor: 'rgb(242, 242, 242)',
52
+ height: '68px',
53
+ marginBottom: '2px',
54
+ position: 'relative',
55
+ width: '106px',
56
+ }}
57
+ />
58
+ <div
59
+ style={{
60
+ backgroundColor: 'rgb(242, 242, 242)',
61
+ height: '106px',
62
+ marginBottom: '2px',
63
+ position: 'relative',
64
+ width: '106px',
65
+ }}
66
+ />
67
+ </div>
68
+
69
+ {/* Column 3 - Two tiles */}
70
+ <div style={{ height: '176px', width: '106px' }}>
71
+ <div
72
+ style={{
73
+ backgroundColor: 'rgb(242, 242, 242)',
74
+ height: '68px',
75
+ marginBottom: '2px',
76
+ position: 'relative',
77
+ width: '106px',
78
+ }}
79
+ />
80
+ <div
81
+ style={{
82
+ backgroundColor: 'rgb(242, 242, 242)',
83
+ height: '106px',
84
+ marginBottom: '2px',
85
+ position: 'relative',
86
+ width: '106px',
87
+ }}
88
+ />
89
+ </div>
90
+
91
+ {/* Column 4 - Two tiles (placeholder) */}
92
+ <div style={{ height: '214px', width: '106px' }}>
93
+ <div
94
+ style={{
95
+ backgroundColor: 'rgb(242, 242, 242)',
96
+ height: '106px',
97
+ marginBottom: '2px',
98
+ position: 'relative',
99
+ width: '106px',
100
+ }}
101
+ />
102
+ <div
103
+ style={{
104
+ backgroundColor: 'rgb(242, 242, 242)',
105
+ height: '106px',
106
+ marginBottom: '2px',
107
+ position: 'relative',
108
+ width: '106px',
109
+ }}
110
+ />
111
+ </div>
112
+
113
+ {/* Column 5 - Two tiles (AD TILE is first) */}
114
+ <div style={{ height: '214px', width: '106px' }}>
115
+ {/* AD TILE */}
116
+ <div
117
+ style={{
118
+ backgroundColor: 'rgb(242, 242, 242)',
119
+ height: '106px',
120
+ marginBottom: '2px',
121
+ position: 'relative',
122
+ width: '106px',
123
+ }}
124
+ >
125
+ {/* Ad Image */}
126
+ <div
127
+ style={{
128
+ height: '106px',
129
+ overflow: 'hidden',
130
+ position: 'relative',
131
+ width: '106px',
132
+ display: 'inline-block',
133
+ }}
134
+ >
135
+ {adData.creativeImage ? (
136
+ <CreativeMedia
137
+ src={adData.creativeImage}
138
+ mediaType={adData.creativeMediaType}
139
+ alt="Ad creative"
140
+ style={{
141
+ height: '100%',
142
+ objectFit: 'cover',
143
+ position: 'absolute',
144
+ width: '100%',
145
+ zIndex: 0,
146
+ }}
147
+ />
148
+ ) : (
149
+ <div
150
+ style={{
151
+ height: '100%',
152
+ width: '100%',
153
+ backgroundColor: 'rgb(218, 220, 222)',
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ }}
158
+ >
159
+ <span style={{ fontSize: '10px', color: 'rgb(160, 160, 160)' }}>1:1</span>
160
+ </div>
161
+ )}
162
+ </div>
163
+
164
+ {/* Gradient overlay */}
165
+ <div
166
+ style={{
167
+ backgroundImage: 'linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4))',
168
+ bottom: '24px',
169
+ height: '84px',
170
+ position: 'absolute',
171
+ width: '100%',
172
+ }}
173
+ />
174
+
175
+ {/* Content overlay */}
176
+ <div
177
+ style={{
178
+ flexDirection: 'column',
179
+ height: '100%',
180
+ position: 'absolute',
181
+ width: '100%',
182
+ display: 'flex',
183
+ top: 0,
184
+ left: 0,
185
+ }}
186
+ >
187
+ {/* Sponsored tag - top right */}
188
+ <div
189
+ style={{
190
+ height: '18px',
191
+ justifyContent: 'flex-end',
192
+ width: '100%',
193
+ display: 'flex',
194
+ }}
195
+ >
196
+ <div style={{ height: '18px', width: '72px' }}>
197
+ <div style={{ height: '16px', width: '72px' }}>
198
+ <div
199
+ style={{
200
+ color: 'rgb(255, 255, 255)',
201
+ fontWeight: 500,
202
+ height: '16px',
203
+ marginLeft: '2px',
204
+ marginRight: '2px',
205
+ marginTop: '6px',
206
+ paddingLeft: '6px',
207
+ paddingRight: '6px',
208
+ width: '58px',
209
+ }}
210
+ >
211
+ Sponsored
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ {/* Profile image - bottom left */}
218
+ <div
219
+ style={{
220
+ height: '40px',
221
+ justifyContent: 'flex-start',
222
+ marginTop: 'auto',
223
+ width: '40px',
224
+ display: 'flex',
225
+ }}
226
+ >
227
+ <div style={{ height: '40px', width: '36px' }}>
228
+ <ProfileAvatar
229
+ src={adData.profileImage}
230
+ name={adData.brandName}
231
+ fontSize={11}
232
+ background="linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%)"
233
+ style={{
234
+ height: '24px',
235
+ width: '24px',
236
+ marginLeft: '6px',
237
+ marginRight: '6px',
238
+ marginTop: '10px',
239
+ borderRadius: '24px',
240
+ }}
241
+ />
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ {/* Second tile in this column */}
248
+ <div
249
+ style={{
250
+ backgroundColor: 'rgb(242, 242, 242)',
251
+ height: '106px',
252
+ marginBottom: '2px',
253
+ position: 'relative',
254
+ width: '106px',
255
+ }}
256
+ />
257
+ </div>
258
+
259
+ {/* Column 6 - Two tiles */}
260
+ <div style={{ height: '214px', width: '106px' }}>
261
+ <div
262
+ style={{
263
+ backgroundColor: 'rgb(242, 242, 242)',
264
+ height: '106px',
265
+ marginBottom: '2px',
266
+ position: 'relative',
267
+ width: '106px',
268
+ }}
269
+ />
270
+ <div
271
+ style={{
272
+ backgroundColor: 'rgb(242, 242, 242)',
273
+ height: '106px',
274
+ marginBottom: '2px',
275
+ position: 'relative',
276
+ width: '106px',
277
+ }}
278
+ />
279
+ </div>
280
+
281
+ {/* Column 7 - Single tall tile */}
282
+ <div
283
+ style={{
284
+ backgroundColor: 'rgb(242, 242, 242)',
285
+ height: '174px',
286
+ marginBottom: '2px',
287
+ position: 'relative',
288
+ width: '106px',
289
+ }}
290
+ />
291
+
292
+ {/* Column 8 - Two tiles */}
293
+ <div style={{ height: '176px', width: '106px' }}>
294
+ <div
295
+ style={{
296
+ backgroundColor: 'rgb(242, 242, 242)',
297
+ height: '106px',
298
+ marginBottom: '2px',
299
+ position: 'relative',
300
+ width: '106px',
301
+ }}
302
+ />
303
+ <div
304
+ style={{
305
+ backgroundColor: 'rgb(242, 242, 242)',
306
+ height: '68px',
307
+ marginBottom: '2px',
308
+ position: 'relative',
309
+ width: '106px',
310
+ }}
311
+ />
312
+ </div>
313
+
314
+ {/* Column 9 - Two tiles */}
315
+ <div style={{ height: '176px', width: '106px' }}>
316
+ <div
317
+ style={{
318
+ backgroundColor: 'rgb(242, 242, 242)',
319
+ height: '106px',
320
+ marginBottom: '2px',
321
+ position: 'relative',
322
+ width: '106px',
323
+ }}
324
+ />
325
+ <div
326
+ style={{
327
+ backgroundColor: 'rgb(242, 242, 242)',
328
+ height: '68px',
329
+ marginBottom: '2px',
330
+ position: 'relative',
331
+ width: '106px',
332
+ }}
333
+ />
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ );
340
+ };
@@ -0,0 +1,245 @@
1
+ import { memo, useState } from 'react'
2
+ import { ProfileAvatar } from './ProfileAvatar'
3
+ import type { CarouselViewProps } from './types'
4
+ import { PREVIEW_DEFAULTS } from './defaults'
5
+
6
+ /**
7
+ * Instagram Feed — CAROUSEL (presentational). ADR-119 Decision 1.
8
+ *
9
+ * Pure: no carousel controller, no crop system. Cards, active index + setter,
10
+ * and each slide's image (`renderImage(i)` slot) come from props. IG feed chrome
11
+ * around a full-width swipeable track: a `1/N` counter, dot indicators centered
12
+ * in the action row, and shared CTA / caption / engagement below. Square slides
13
+ * (Meta carousel is 1:1). Meta-exact hex — `packages/ui` is token-boundary
14
+ * exempt (ADR-119 D5).
15
+ */
16
+
17
+ const WIDTH = 320
18
+ const FONT =
19
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
20
+
21
+ const IG_RING =
22
+ 'linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%)'
23
+
24
+ const C = {
25
+ ink: 'rgb(38, 38, 38)',
26
+ sub: 'rgb(115, 115, 115)',
27
+ hair: 'rgb(239, 239, 239)',
28
+ imageBg: '#FAFAFA',
29
+ dot: 'rgb(196, 196, 196)',
30
+ dotActive: '#0095F6',
31
+ }
32
+
33
+ // Illustrative placeholder engagement — preview chrome, not live data.
34
+ const LIKES = '234'
35
+ const COMMENTS = 12
36
+ const POSTED = '2 hours ago'
37
+
38
+ export const InstagramFeedCarousel = memo(function InstagramFeedCarousel({
39
+ cards,
40
+ active,
41
+ onActive,
42
+ renderImage,
43
+ brandName,
44
+ profileImage,
45
+ primaryText,
46
+ callToAction,
47
+ }: CarouselViewProps) {
48
+ const [expanded, setExpanded] = useState(false)
49
+
50
+ const maxIndex = Math.max(0, cards.length - 1)
51
+ const i = Math.min(Math.max(0, active), maxIndex)
52
+ const imageHeight = WIDTH // square slides
53
+ const translate = -(i * WIDTH)
54
+
55
+ const brand = brandName || PREVIEW_DEFAULTS.brandName
56
+ const handle = brand.trim().toLowerCase().replace(/\s+/g, '')
57
+ const caption = primaryText || PREVIEW_DEFAULTS.primaryText
58
+ const LIMIT = 100
59
+ const clipped = caption.length > LIMIT && !expanded
60
+
61
+ return (
62
+ <div style={{ width: WIDTH, background: '#fff', fontFamily: FONT, color: C.ink }}>
63
+ {/* Header */}
64
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px' }}>
65
+ <div style={{ width: 30, height: 30, borderRadius: '50%', overflow: 'hidden', flexShrink: 0 }}>
66
+ <ProfileAvatar src={profileImage} name={brand} fontSize={12} background={IG_RING} />
67
+ </div>
68
+ <div style={{ flex: 1, minWidth: 0 }}>
69
+ <div style={{ fontSize: 13, fontWeight: 600, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
70
+ {brand}
71
+ </div>
72
+ <div style={{ fontSize: 12, lineHeight: '15px', color: C.sub }}>Sponsored</div>
73
+ </div>
74
+ <MenuDots />
75
+ </div>
76
+
77
+ {/* Image carousel */}
78
+ <div style={{ position: 'relative', width: WIDTH, height: imageHeight, overflow: 'hidden', background: C.imageBg }}>
79
+ <div style={{ display: 'flex', height: '100%', transform: `translateX(${translate}px)`, transition: 'transform 300ms cubic-bezier(0.22, 1, 0.36, 1)' }}>
80
+ {cards.map((card, k) => (
81
+ <div key={card.key} style={{ width: WIDTH, height: imageHeight, flexShrink: 0, overflow: 'hidden' }}>
82
+ {renderImage(k)}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ {/* Counter pill */}
87
+ <div style={{ position: 'absolute', top: 10, right: 10, padding: '2px 9px', borderRadius: 11, background: 'rgba(0,0,0,0.55)', color: '#fff', fontSize: 12, fontWeight: 600 }}>
88
+ {i + 1}/{cards.length}
89
+ </div>
90
+ {i > 0 && <Arrow dir="left" onClick={() => onActive(i - 1)} top={imageHeight / 2} />}
91
+ {i < maxIndex && <Arrow dir="right" onClick={() => onActive(i + 1)} top={imageHeight / 2} />}
92
+ </div>
93
+
94
+ {/* CTA bar */}
95
+ <button
96
+ type="button"
97
+ style={{
98
+ display: 'flex',
99
+ alignItems: 'center',
100
+ justifyContent: 'space-between',
101
+ width: '100%',
102
+ padding: '11px 14px',
103
+ border: 0,
104
+ borderBottom: `1px solid ${C.hair}`,
105
+ background: 'none',
106
+ cursor: 'pointer',
107
+ font: 'inherit',
108
+ textAlign: 'left',
109
+ }}
110
+ >
111
+ <span style={{ fontSize: 14, fontWeight: 600, color: C.ink }}>{callToAction || PREVIEW_DEFAULTS.callToAction}</span>
112
+ <Chevron />
113
+ </button>
114
+
115
+ {/* Action row — heart/comment/share · dots (centered) · bookmark */}
116
+ <div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '9px 12px 5px' }}>
117
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
118
+ <Heart />
119
+ <CommentBubble />
120
+ <Send />
121
+ </div>
122
+ <div style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', display: 'flex', alignItems: 'center', gap: 5 }}>
123
+ {cards.map((card, k) => (
124
+ <span key={card.key} style={{ width: 6, height: 6, borderRadius: '50%', background: k === i ? C.dotActive : C.dot }} />
125
+ ))}
126
+ </div>
127
+ <Bookmark />
128
+ </div>
129
+
130
+ {/* Likes */}
131
+ <div style={{ padding: '0 12px', fontSize: 13, fontWeight: 600, color: C.ink, lineHeight: '18px' }}>{LIKES} likes</div>
132
+
133
+ {/* Caption */}
134
+ {caption && (
135
+ <div style={{ padding: '3px 12px 0', fontSize: 13, lineHeight: '18px', fontWeight: 400, color: 'rgb(20, 20, 20)' }}>
136
+ <span style={{ fontWeight: 600 }}>{handle}</span>{' '}
137
+ {clipped ? caption.slice(0, LIMIT).trimEnd() : caption}
138
+ {clipped && (
139
+ <button type="button" onClick={() => setExpanded(true)} style={{ border: 0, background: 'none', padding: 0, marginLeft: 4, color: C.sub, fontWeight: 400, fontSize: 13, cursor: 'pointer', fontFamily: FONT }}>
140
+ more
141
+ </button>
142
+ )}
143
+ </div>
144
+ )}
145
+
146
+ {/* Comments */}
147
+ <div style={{ padding: '5px 12px 0', fontSize: 13, color: C.sub }}>View all {COMMENTS} comments</div>
148
+
149
+ {/* Timestamp */}
150
+ <div style={{ padding: '4px 12px 12px', fontSize: 11, color: C.sub }}>{POSTED}</div>
151
+ </div>
152
+ )
153
+ })
154
+
155
+ /* ------------------------------- icons / bits ------------------------------- */
156
+
157
+ const stroke = {
158
+ fill: 'none',
159
+ stroke: 'rgb(38, 38, 38)',
160
+ strokeWidth: 1.8,
161
+ strokeLinecap: 'round' as const,
162
+ strokeLinejoin: 'round' as const,
163
+ }
164
+
165
+ function Arrow({ dir, onClick, top }: { dir: 'left' | 'right'; onClick: () => void; top: number }) {
166
+ return (
167
+ <button
168
+ type="button"
169
+ onClick={onClick}
170
+ aria-label={dir === 'left' ? 'Previous' : 'Next'}
171
+ style={{
172
+ position: 'absolute',
173
+ top,
174
+ transform: 'translateY(-50%)',
175
+ ...(dir === 'left' ? { left: 8 } : { right: 8 }),
176
+ width: 26,
177
+ height: 26,
178
+ borderRadius: '50%',
179
+ border: 0,
180
+ background: 'rgba(255,255,255,0.9)',
181
+ boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
182
+ display: 'flex',
183
+ alignItems: 'center',
184
+ justifyContent: 'center',
185
+ cursor: 'pointer',
186
+ color: 'rgb(38,38,38)',
187
+ }}
188
+ >
189
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
190
+ {dir === 'left' ? <polyline points="15 18 9 12 15 6" /> : <polyline points="9 18 15 12 9 6" />}
191
+ </svg>
192
+ </button>
193
+ )
194
+ }
195
+
196
+ function MenuDots() {
197
+ return (
198
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="rgb(38,38,38)" style={{ display: 'block', flexShrink: 0 }}>
199
+ <circle cx="5" cy="12" r="1.4" />
200
+ <circle cx="12" cy="12" r="1.4" />
201
+ <circle cx="19" cy="12" r="1.4" />
202
+ </svg>
203
+ )
204
+ }
205
+
206
+ function Chevron() {
207
+ return (
208
+ <svg width="16" height="16" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
209
+ <polyline points="9 18 15 12 9 6" />
210
+ </svg>
211
+ )
212
+ }
213
+
214
+ function Heart() {
215
+ return (
216
+ <svg width="24" height="24" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
217
+ <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" />
218
+ </svg>
219
+ )
220
+ }
221
+
222
+ function CommentBubble() {
223
+ return (
224
+ <svg width="24" height="24" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
225
+ <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" />
226
+ </svg>
227
+ )
228
+ }
229
+
230
+ function Send() {
231
+ return (
232
+ <svg width="24" height="24" viewBox="0 0 24 24" {...stroke} style={{ display: 'block' }}>
233
+ <line x1="22" y1="2" x2="11" y2="13" />
234
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
235
+ </svg>
236
+ )
237
+ }
238
+
239
+ function Bookmark() {
240
+ return (
241
+ <svg width="24" height="24" viewBox="0 0 24 24" {...stroke} style={{ display: 'block', flexShrink: 0 }}>
242
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
243
+ </svg>
244
+ )
245
+ }