@startsimpli/ui 0.4.5 → 0.4.7

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 (69) hide show
  1. package/package.json +2 -1
  2. package/src/components/ActivityTimeline.tsx +173 -0
  3. package/src/components/LogActivityDialog.tsx +303 -0
  4. package/src/components/QuickLogButtons.tsx +32 -0
  5. package/src/components/badge/StageBadge.tsx +31 -0
  6. package/src/components/badge/index.ts +3 -0
  7. package/src/components/command-palette/CommandPalette.tsx +344 -0
  8. package/src/components/command-palette/command-palette-context.tsx +51 -0
  9. package/src/components/command-palette/index.ts +3 -0
  10. package/src/components/compose/compose-header.tsx +72 -0
  11. package/src/components/compose/compose-loading.tsx +13 -0
  12. package/src/components/compose/index.ts +6 -0
  13. package/src/components/compose/save-status-indicator.tsx +57 -0
  14. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  15. package/src/components/compose/subject-input.tsx +25 -0
  16. package/src/components/compose/useAutoSave.ts +93 -0
  17. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  18. package/src/components/dashboard/DashboardSection.tsx +32 -0
  19. package/src/components/dashboard/MetricCard.tsx +129 -0
  20. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  21. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  22. package/src/components/dashboard/index.ts +14 -0
  23. package/src/components/email-dialogs/index.ts +14 -0
  24. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  25. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  26. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  27. package/src/components/email-dialogs/template-picker.tsx +225 -0
  28. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  29. package/src/components/email-editor/add-block-menu.tsx +151 -0
  30. package/src/components/email-editor/block-toolbar.tsx +73 -0
  31. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  32. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  33. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  34. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  35. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  36. package/src/components/email-editor/blocks/index.ts +9 -0
  37. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  38. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  39. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  40. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  41. package/src/components/email-editor/editor-sidebar.tsx +791 -0
  42. package/src/components/email-editor/email-editor.tsx +886 -0
  43. package/src/components/email-editor/index.ts +50 -0
  44. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  45. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  46. package/src/components/email-editor/types.ts +413 -0
  47. package/src/components/email-editor/utils/defaults.ts +116 -0
  48. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  49. package/src/components/enrichment/EnrichButton.tsx +33 -0
  50. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  51. package/src/components/enrichment/QualityBadge.tsx +43 -0
  52. package/src/components/enrichment/index.ts +8 -0
  53. package/src/components/gantt/GanttChart.tsx +25 -25
  54. package/src/components/gantt/types.ts +5 -5
  55. package/src/components/index.ts +46 -0
  56. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  57. package/src/components/integrations/IntegrationCard.tsx +92 -0
  58. package/src/components/integrations/index.ts +5 -0
  59. package/src/components/kanban/KanbanBoard.tsx +103 -0
  60. package/src/components/kanban/index.ts +2 -0
  61. package/src/components/lists/CreateListDialog.tsx +158 -0
  62. package/src/components/lists/ListCard.tsx +77 -0
  63. package/src/components/lists/index.ts +5 -0
  64. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  65. package/src/components/pipeline/index.ts +2 -0
  66. package/src/components/settings/SettingsCard.tsx +33 -0
  67. package/src/components/settings/SettingsLayout.tsx +28 -0
  68. package/src/components/settings/SettingsNav.tsx +42 -0
  69. package/src/components/settings/index.ts +6 -0
@@ -0,0 +1,791 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+ import {
5
+ Block,
6
+ TextBlock,
7
+ DividerBlock,
8
+ CTABlock,
9
+ ImageBlock,
10
+ SpacerBlock,
11
+ SocialBlock,
12
+ HeaderBlock,
13
+ FooterBlock,
14
+ SocialLink,
15
+ GlobalStyles,
16
+ BlockStyle,
17
+ } from './types'
18
+ import { Button } from '../ui/button'
19
+ import { Input } from '../ui/input'
20
+ import { Label } from '../ui/label'
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '../ui/select'
28
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
29
+ import { Plus, Trash2, X, Settings, Palette } from 'lucide-react'
30
+ import { THEME_PRESETS } from './utils/defaults'
31
+ import { PLATFORM_ICONS } from './blocks/social-block'
32
+ import { cn } from '../../lib/utils'
33
+
34
+ interface EditorSidebarProps {
35
+ selectedBlock: Block | null
36
+ onBlockChange: (block: Block) => void
37
+ globalStyles: GlobalStyles
38
+ onGlobalStylesChange: (styles: GlobalStyles) => void
39
+ onClose: () => void
40
+ }
41
+
42
+ export function EditorSidebar({
43
+ selectedBlock,
44
+ onBlockChange,
45
+ globalStyles,
46
+ onGlobalStylesChange,
47
+ onClose,
48
+ }: EditorSidebarProps) {
49
+ const [activeTab, setActiveTab] = useState(selectedBlock ? 'block' : 'global')
50
+
51
+ return (
52
+ <div className="w-72 border-l bg-background flex flex-col h-full overflow-hidden">
53
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-full">
54
+ <div className="flex items-center justify-between px-3 pt-3 pb-2 border-b">
55
+ <TabsList className="h-8">
56
+ <TabsTrigger value="block" className="text-xs h-7 px-2" disabled={!selectedBlock}>
57
+ <Settings className="h-3 w-3 mr-1" />
58
+ Block
59
+ </TabsTrigger>
60
+ <TabsTrigger value="global" className="text-xs h-7 px-2">
61
+ <Palette className="h-3 w-3 mr-1" />
62
+ Design
63
+ </TabsTrigger>
64
+ </TabsList>
65
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={onClose}>
66
+ <X className="h-3.5 w-3.5" />
67
+ </Button>
68
+ </div>
69
+
70
+ <TabsContent value="block" className="flex-1 overflow-y-auto p-3 space-y-4 mt-0">
71
+ {selectedBlock ? (
72
+ <BlockSettings block={selectedBlock} onChange={onBlockChange} />
73
+ ) : (
74
+ <p className="text-sm text-muted-foreground text-center py-8">
75
+ Select a block to edit its settings
76
+ </p>
77
+ )}
78
+ </TabsContent>
79
+
80
+ <TabsContent value="global" className="flex-1 overflow-y-auto p-3 space-y-4 mt-0">
81
+ <GlobalStylesPanel
82
+ globalStyles={globalStyles}
83
+ onChange={onGlobalStylesChange}
84
+ />
85
+ </TabsContent>
86
+ </Tabs>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ // --- Block Settings ---
92
+ function BlockSettings({ block, onChange }: { block: Block; onChange: (b: Block) => void }) {
93
+ const updateStyle = useCallback(
94
+ (updates: Partial<BlockStyle>) => {
95
+ onChange({ ...block, style: { ...block.style, ...updates } })
96
+ },
97
+ [block, onChange]
98
+ )
99
+
100
+ return (
101
+ <div className="space-y-4">
102
+ <div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
103
+ {block.type} Settings
104
+ </div>
105
+
106
+ {/* Type-specific settings */}
107
+ {block.type === 'text' && (
108
+ <TextSettings block={block} onChange={(b) => onChange(b)} />
109
+ )}
110
+ {block.type === 'divider' && (
111
+ <DividerSettings block={block} onChange={(b) => onChange(b)} />
112
+ )}
113
+ {block.type === 'cta' && (
114
+ <ButtonSettings block={block} onChange={(b) => onChange(b)} />
115
+ )}
116
+ {block.type === 'image' && (
117
+ <ImageSettings block={block} onChange={(b) => onChange(b)} />
118
+ )}
119
+ {block.type === 'spacer' && (
120
+ <SpacerSettings block={block} onChange={(b) => onChange(b)} />
121
+ )}
122
+ {block.type === 'social' && (
123
+ <SocialSettings block={block} onChange={(b) => onChange(b)} />
124
+ )}
125
+ {block.type === 'header' && (
126
+ <HeaderSettings block={block} onChange={(b) => onChange(b)} />
127
+ )}
128
+ {block.type === 'footer' && (
129
+ <FooterSettings block={block} onChange={(b) => onChange(b)} />
130
+ )}
131
+
132
+ {/* Common: spacing/background */}
133
+ <div className="border-t pt-4 space-y-3">
134
+ <div className="text-xs font-medium text-muted-foreground">Spacing</div>
135
+ <div className="grid grid-cols-2 gap-2">
136
+ <div className="space-y-1">
137
+ <Label className="text-xs">Pad Top</Label>
138
+ <Input
139
+ type="number"
140
+ value={block.style?.paddingTop ?? 0}
141
+ onChange={(e) => updateStyle({ paddingTop: parseInt(e.target.value) || 0 })}
142
+ className="h-8 text-xs"
143
+ />
144
+ </div>
145
+ <div className="space-y-1">
146
+ <Label className="text-xs">Pad Bottom</Label>
147
+ <Input
148
+ type="number"
149
+ value={block.style?.paddingBottom ?? 0}
150
+ onChange={(e) => updateStyle({ paddingBottom: parseInt(e.target.value) || 0 })}
151
+ className="h-8 text-xs"
152
+ />
153
+ </div>
154
+ </div>
155
+ <div className="space-y-1">
156
+ <Label className="text-xs">Background</Label>
157
+ <div className="flex gap-2">
158
+ <input
159
+ type="color"
160
+ value={block.style?.backgroundColor || '#ffffff'}
161
+ onChange={(e) => updateStyle({ backgroundColor: e.target.value })}
162
+ className="h-8 w-8 rounded border cursor-pointer"
163
+ />
164
+ <Input
165
+ value={block.style?.backgroundColor || ''}
166
+ onChange={(e) => updateStyle({ backgroundColor: e.target.value })}
167
+ placeholder="transparent"
168
+ className="h-8 text-xs"
169
+ />
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ )
175
+ }
176
+
177
+ // --- Text settings ---
178
+ function TextSettings({ block, onChange }: { block: TextBlock; onChange: (b: TextBlock) => void }) {
179
+ return (
180
+ <div className="space-y-3">
181
+ <div className="space-y-1">
182
+ <Label className="text-xs">Font Size (px)</Label>
183
+ <Input
184
+ type="number"
185
+ value={block.fontSize || 16}
186
+ onChange={(e) => onChange({ ...block, fontSize: parseInt(e.target.value) || 16 })}
187
+ className="h-8 text-xs"
188
+ />
189
+ </div>
190
+ <div className="space-y-1">
191
+ <Label className="text-xs">Line Height</Label>
192
+ <Input
193
+ type="number"
194
+ step="0.1"
195
+ value={block.lineHeight || 1.6}
196
+ onChange={(e) => onChange({ ...block, lineHeight: parseFloat(e.target.value) || 1.6 })}
197
+ className="h-8 text-xs"
198
+ />
199
+ </div>
200
+ <div className="space-y-1">
201
+ <Label className="text-xs">Text Color</Label>
202
+ <div className="flex gap-2">
203
+ <input
204
+ type="color"
205
+ value={block.textColor || '#1f2937'}
206
+ onChange={(e) => onChange({ ...block, textColor: e.target.value })}
207
+ className="h-8 w-8 rounded border cursor-pointer"
208
+ />
209
+ <Input
210
+ value={block.textColor || '#1f2937'}
211
+ onChange={(e) => onChange({ ...block, textColor: e.target.value })}
212
+ className="h-8 text-xs"
213
+ />
214
+ </div>
215
+ </div>
216
+ <div className="space-y-1">
217
+ <Label className="text-xs">Font Family</Label>
218
+ <Select
219
+ value={block.fontFamily || 'default'}
220
+ onValueChange={(v) => onChange({ ...block, fontFamily: v === 'default' ? undefined : v })}
221
+ >
222
+ <SelectTrigger className="h-8 text-xs">
223
+ <SelectValue />
224
+ </SelectTrigger>
225
+ <SelectContent>
226
+ <SelectItem value="default">Default</SelectItem>
227
+ <SelectItem value="Arial, sans-serif">Arial</SelectItem>
228
+ <SelectItem value="Georgia, serif">Georgia</SelectItem>
229
+ <SelectItem value="'Courier New', monospace">Courier New</SelectItem>
230
+ <SelectItem value="Verdana, sans-serif">Verdana</SelectItem>
231
+ <SelectItem value="'Trebuchet MS', sans-serif">Trebuchet MS</SelectItem>
232
+ </SelectContent>
233
+ </Select>
234
+ </div>
235
+ </div>
236
+ )
237
+ }
238
+
239
+ // --- Divider settings ---
240
+ function DividerSettings({ block, onChange }: { block: DividerBlock; onChange: (b: DividerBlock) => void }) {
241
+ return (
242
+ <div className="space-y-3">
243
+ <div className="space-y-1">
244
+ <Label className="text-xs">Style</Label>
245
+ <Select
246
+ value={block.dividerStyle}
247
+ onValueChange={(v) => onChange({ ...block, dividerStyle: v as DividerBlock['dividerStyle'] })}
248
+ >
249
+ <SelectTrigger className="h-8 text-xs">
250
+ <SelectValue />
251
+ </SelectTrigger>
252
+ <SelectContent>
253
+ <SelectItem value="solid">Solid</SelectItem>
254
+ <SelectItem value="dashed">Dashed</SelectItem>
255
+ <SelectItem value="dotted">Dotted</SelectItem>
256
+ <SelectItem value="space">Space</SelectItem>
257
+ </SelectContent>
258
+ </Select>
259
+ </div>
260
+ <div className="space-y-1">
261
+ <Label className="text-xs">Color</Label>
262
+ <div className="flex gap-2">
263
+ <input
264
+ type="color"
265
+ value={block.color || '#d1d5db'}
266
+ onChange={(e) => onChange({ ...block, color: e.target.value })}
267
+ className="h-8 w-8 rounded border cursor-pointer"
268
+ />
269
+ <Input
270
+ value={block.color || '#d1d5db'}
271
+ onChange={(e) => onChange({ ...block, color: e.target.value })}
272
+ className="h-8 text-xs"
273
+ />
274
+ </div>
275
+ </div>
276
+ <div className="space-y-1">
277
+ <Label className="text-xs">Thickness (px)</Label>
278
+ <Input
279
+ type="number"
280
+ value={block.thickness || 1}
281
+ onChange={(e) => onChange({ ...block, thickness: parseInt(e.target.value) || 1 })}
282
+ className="h-8 text-xs"
283
+ min={1}
284
+ max={10}
285
+ />
286
+ </div>
287
+ <div className="space-y-1">
288
+ <Label className="text-xs">Width (%)</Label>
289
+ <Input
290
+ type="number"
291
+ value={block.width || 100}
292
+ onChange={(e) => onChange({ ...block, width: parseInt(e.target.value) || 100 })}
293
+ className="h-8 text-xs"
294
+ min={10}
295
+ max={100}
296
+ />
297
+ </div>
298
+ </div>
299
+ )
300
+ }
301
+
302
+ // --- Button settings ---
303
+ function ButtonSettings({ block, onChange }: { block: CTABlock; onChange: (b: CTABlock) => void }) {
304
+ return (
305
+ <div className="space-y-3">
306
+ <div className="space-y-1">
307
+ <Label className="text-xs">Button Text</Label>
308
+ <Input
309
+ value={block.text}
310
+ onChange={(e) => onChange({ ...block, text: e.target.value })}
311
+ className="h-8 text-xs"
312
+ />
313
+ </div>
314
+ <div className="space-y-1">
315
+ <Label className="text-xs">URL</Label>
316
+ <Input
317
+ value={block.url}
318
+ onChange={(e) => onChange({ ...block, url: e.target.value })}
319
+ placeholder="https://..."
320
+ className="h-8 text-xs"
321
+ />
322
+ </div>
323
+ <div className="space-y-1">
324
+ <Label className="text-xs">Button Color</Label>
325
+ <div className="flex gap-2">
326
+ <input
327
+ type="color"
328
+ value={block.buttonColor || '#2563eb'}
329
+ onChange={(e) => onChange({ ...block, buttonColor: e.target.value })}
330
+ className="h-8 w-8 rounded border cursor-pointer"
331
+ />
332
+ <Input
333
+ value={block.buttonColor || '#2563eb'}
334
+ onChange={(e) => onChange({ ...block, buttonColor: e.target.value })}
335
+ className="h-8 text-xs"
336
+ />
337
+ </div>
338
+ </div>
339
+ <div className="space-y-1">
340
+ <Label className="text-xs">Text Color</Label>
341
+ <div className="flex gap-2">
342
+ <input
343
+ type="color"
344
+ value={block.textColor || '#ffffff'}
345
+ onChange={(e) => onChange({ ...block, textColor: e.target.value })}
346
+ className="h-8 w-8 rounded border cursor-pointer"
347
+ />
348
+ <Input
349
+ value={block.textColor || '#ffffff'}
350
+ onChange={(e) => onChange({ ...block, textColor: e.target.value })}
351
+ className="h-8 text-xs"
352
+ />
353
+ </div>
354
+ </div>
355
+ <div className="space-y-1">
356
+ <Label className="text-xs">Border Radius (px)</Label>
357
+ <Input
358
+ type="number"
359
+ value={block.borderRadius ?? 6}
360
+ onChange={(e) => onChange({ ...block, borderRadius: parseInt(e.target.value) || 0 })}
361
+ className="h-8 text-xs"
362
+ min={0}
363
+ max={50}
364
+ />
365
+ </div>
366
+ <div className="grid grid-cols-2 gap-2">
367
+ <div className="space-y-1">
368
+ <Label className="text-xs">Pad H</Label>
369
+ <Input
370
+ type="number"
371
+ value={block.paddingH ?? 24}
372
+ onChange={(e) => onChange({ ...block, paddingH: parseInt(e.target.value) || 0 })}
373
+ className="h-8 text-xs"
374
+ />
375
+ </div>
376
+ <div className="space-y-1">
377
+ <Label className="text-xs">Pad V</Label>
378
+ <Input
379
+ type="number"
380
+ value={block.paddingV ?? 12}
381
+ onChange={(e) => onChange({ ...block, paddingV: parseInt(e.target.value) || 0 })}
382
+ className="h-8 text-xs"
383
+ />
384
+ </div>
385
+ </div>
386
+ <div className="space-y-1">
387
+ <Label className="text-xs">Alignment</Label>
388
+ <Select
389
+ value={block.alignment}
390
+ onValueChange={(v) => onChange({ ...block, alignment: v as 'left' | 'center' | 'right' })}
391
+ >
392
+ <SelectTrigger className="h-8 text-xs">
393
+ <SelectValue />
394
+ </SelectTrigger>
395
+ <SelectContent>
396
+ <SelectItem value="left">Left</SelectItem>
397
+ <SelectItem value="center">Center</SelectItem>
398
+ <SelectItem value="right">Right</SelectItem>
399
+ </SelectContent>
400
+ </Select>
401
+ </div>
402
+ </div>
403
+ )
404
+ }
405
+
406
+ // --- Image settings ---
407
+ function ImageSettings({ block, onChange }: { block: ImageBlock; onChange: (b: ImageBlock) => void }) {
408
+ return (
409
+ <div className="space-y-3">
410
+ <div className="space-y-1">
411
+ <Label className="text-xs">Image URL</Label>
412
+ <Input
413
+ value={block.url}
414
+ onChange={(e) => onChange({ ...block, url: e.target.value })}
415
+ placeholder="https://..."
416
+ className="h-8 text-xs"
417
+ />
418
+ </div>
419
+ <div className="space-y-1">
420
+ <Label className="text-xs">Alt Text</Label>
421
+ <Input
422
+ value={block.alt}
423
+ onChange={(e) => onChange({ ...block, alt: e.target.value })}
424
+ className="h-8 text-xs"
425
+ />
426
+ </div>
427
+ <div className="space-y-1">
428
+ <Label className="text-xs">Caption</Label>
429
+ <Input
430
+ value={block.caption || ''}
431
+ onChange={(e) => onChange({ ...block, caption: e.target.value })}
432
+ className="h-8 text-xs"
433
+ />
434
+ </div>
435
+ <div className="space-y-1">
436
+ <Label className="text-xs">Link URL (wraps image)</Label>
437
+ <Input
438
+ value={block.linkUrl || ''}
439
+ onChange={(e) => onChange({ ...block, linkUrl: e.target.value })}
440
+ placeholder="https://..."
441
+ className="h-8 text-xs"
442
+ />
443
+ </div>
444
+ <div className="space-y-1">
445
+ <Label className="text-xs">Width (%)</Label>
446
+ <Input
447
+ type="number"
448
+ value={block.width || 100}
449
+ onChange={(e) => onChange({ ...block, width: parseInt(e.target.value) || 100 })}
450
+ className="h-8 text-xs"
451
+ min={10}
452
+ max={100}
453
+ />
454
+ </div>
455
+ <div className="space-y-1">
456
+ <Label className="text-xs">Alignment</Label>
457
+ <Select
458
+ value={block.alignment}
459
+ onValueChange={(v) => onChange({ ...block, alignment: v as 'left' | 'center' | 'right' })}
460
+ >
461
+ <SelectTrigger className="h-8 text-xs">
462
+ <SelectValue />
463
+ </SelectTrigger>
464
+ <SelectContent>
465
+ <SelectItem value="left">Left</SelectItem>
466
+ <SelectItem value="center">Center</SelectItem>
467
+ <SelectItem value="right">Right</SelectItem>
468
+ </SelectContent>
469
+ </Select>
470
+ </div>
471
+ </div>
472
+ )
473
+ }
474
+
475
+ // --- Spacer settings ---
476
+ function SpacerSettings({ block, onChange }: { block: SpacerBlock; onChange: (b: SpacerBlock) => void }) {
477
+ return (
478
+ <div className="space-y-3">
479
+ <div className="space-y-1">
480
+ <Label className="text-xs">Height (px)</Label>
481
+ <Input
482
+ type="number"
483
+ value={block.height || 32}
484
+ onChange={(e) => onChange({ ...block, height: parseInt(e.target.value) || 8 })}
485
+ className="h-8 text-xs"
486
+ min={8}
487
+ max={200}
488
+ />
489
+ </div>
490
+ </div>
491
+ )
492
+ }
493
+
494
+ // --- Social settings ---
495
+ function SocialSettings({ block, onChange }: { block: SocialBlock; onChange: (b: SocialBlock) => void }) {
496
+ const addLink = () => {
497
+ const link: SocialLink = {
498
+ id: `social-${Date.now()}`,
499
+ platform: 'website',
500
+ url: '',
501
+ }
502
+ onChange({ ...block, links: [...block.links, link] })
503
+ }
504
+
505
+ const removeLink = (id: string) => {
506
+ onChange({ ...block, links: block.links.filter((l) => l.id !== id) })
507
+ }
508
+
509
+ const updateLink = (id: string, updates: Partial<SocialLink>) => {
510
+ onChange({
511
+ ...block,
512
+ links: block.links.map((l) => (l.id === id ? { ...l, ...updates } : l)),
513
+ })
514
+ }
515
+
516
+ const platforms: SocialLink['platform'][] = [
517
+ 'linkedin', 'twitter', 'facebook', 'instagram', 'youtube', 'github', 'website',
518
+ ]
519
+
520
+ return (
521
+ <div className="space-y-3">
522
+ <div className="space-y-1">
523
+ <Label className="text-xs">Alignment</Label>
524
+ <Select
525
+ value={block.alignment}
526
+ onValueChange={(v) => onChange({ ...block, alignment: v as 'left' | 'center' | 'right' })}
527
+ >
528
+ <SelectTrigger className="h-8 text-xs">
529
+ <SelectValue />
530
+ </SelectTrigger>
531
+ <SelectContent>
532
+ <SelectItem value="left">Left</SelectItem>
533
+ <SelectItem value="center">Center</SelectItem>
534
+ <SelectItem value="right">Right</SelectItem>
535
+ </SelectContent>
536
+ </Select>
537
+ </div>
538
+ <div className="space-y-1">
539
+ <Label className="text-xs">Icon Size</Label>
540
+ <Input
541
+ type="number"
542
+ value={block.iconSize || 24}
543
+ onChange={(e) => onChange({ ...block, iconSize: parseInt(e.target.value) || 24 })}
544
+ className="h-8 text-xs"
545
+ min={16}
546
+ max={48}
547
+ />
548
+ </div>
549
+ <div className="space-y-2">
550
+ <Label className="text-xs">Links</Label>
551
+ {block.links.map((link) => (
552
+ <div key={link.id} className="flex gap-1 items-center">
553
+ <Select
554
+ value={link.platform}
555
+ onValueChange={(v) =>
556
+ updateLink(link.id, { platform: v as SocialLink['platform'] })
557
+ }
558
+ >
559
+ <SelectTrigger className="h-7 text-xs w-24 shrink-0">
560
+ <SelectValue />
561
+ </SelectTrigger>
562
+ <SelectContent>
563
+ {platforms.map((p) => (
564
+ <SelectItem key={p} value={p} className="text-xs">
565
+ {p.charAt(0).toUpperCase() + p.slice(1)}
566
+ </SelectItem>
567
+ ))}
568
+ </SelectContent>
569
+ </Select>
570
+ <Input
571
+ value={link.url}
572
+ onChange={(e) => updateLink(link.id, { url: e.target.value })}
573
+ placeholder="URL"
574
+ className="h-7 text-xs"
575
+ />
576
+ <Button
577
+ variant="ghost"
578
+ size="icon"
579
+ className="h-7 w-7 shrink-0"
580
+ onClick={() => removeLink(link.id)}
581
+ >
582
+ <Trash2 className="h-3 w-3" />
583
+ </Button>
584
+ </div>
585
+ ))}
586
+ <Button variant="outline" size="sm" onClick={addLink} className="w-full h-7 text-xs">
587
+ <Plus className="mr-1 h-3 w-3" />
588
+ Add Link
589
+ </Button>
590
+ </div>
591
+ </div>
592
+ )
593
+ }
594
+
595
+ // --- Header settings ---
596
+ function HeaderSettings({ block, onChange }: { block: HeaderBlock; onChange: (b: HeaderBlock) => void }) {
597
+ return (
598
+ <div className="space-y-3">
599
+ <div className="space-y-1">
600
+ <Label className="text-xs">Company Name</Label>
601
+ <Input
602
+ value={block.companyName}
603
+ onChange={(e) => onChange({ ...block, companyName: e.target.value })}
604
+ className="h-8 text-xs"
605
+ />
606
+ </div>
607
+ <div className="space-y-1">
608
+ <Label className="text-xs">Logo URL</Label>
609
+ <Input
610
+ value={block.logoUrl || ''}
611
+ onChange={(e) => onChange({ ...block, logoUrl: e.target.value })}
612
+ placeholder="https://..."
613
+ className="h-8 text-xs"
614
+ />
615
+ </div>
616
+ <div className="space-y-1">
617
+ <Label className="text-xs">Alignment</Label>
618
+ <Select
619
+ value={block.alignment}
620
+ onValueChange={(v) => onChange({ ...block, alignment: v as 'left' | 'center' | 'right' })}
621
+ >
622
+ <SelectTrigger className="h-8 text-xs">
623
+ <SelectValue />
624
+ </SelectTrigger>
625
+ <SelectContent>
626
+ <SelectItem value="left">Left</SelectItem>
627
+ <SelectItem value="center">Center</SelectItem>
628
+ <SelectItem value="right">Right</SelectItem>
629
+ </SelectContent>
630
+ </Select>
631
+ </div>
632
+ </div>
633
+ )
634
+ }
635
+
636
+ // --- Footer settings ---
637
+ function FooterSettings({ block, onChange }: { block: FooterBlock; onChange: (b: FooterBlock) => void }) {
638
+ return (
639
+ <div className="space-y-3">
640
+ <div className="space-y-1">
641
+ <Label className="text-xs">Company Name</Label>
642
+ <Input
643
+ value={block.companyName}
644
+ onChange={(e) => onChange({ ...block, companyName: e.target.value })}
645
+ className="h-8 text-xs"
646
+ />
647
+ </div>
648
+ <div className="space-y-1">
649
+ <Label className="text-xs">Address</Label>
650
+ <Input
651
+ value={block.address || ''}
652
+ onChange={(e) => onChange({ ...block, address: e.target.value })}
653
+ className="h-8 text-xs"
654
+ />
655
+ </div>
656
+ <div className="flex items-center gap-2">
657
+ <input
658
+ type="checkbox"
659
+ checked={block.showUnsubscribe}
660
+ onChange={(e) => onChange({ ...block, showUnsubscribe: e.target.checked })}
661
+ className="rounded"
662
+ id="show-unsub"
663
+ />
664
+ <Label className="text-xs" htmlFor="show-unsub">Show unsubscribe link</Label>
665
+ </div>
666
+ {block.showUnsubscribe && (
667
+ <div className="space-y-1">
668
+ <Label className="text-xs">Unsubscribe URL</Label>
669
+ <Input
670
+ value={block.unsubscribeUrl || ''}
671
+ onChange={(e) => onChange({ ...block, unsubscribeUrl: e.target.value })}
672
+ placeholder="https://..."
673
+ className="h-8 text-xs"
674
+ />
675
+ </div>
676
+ )}
677
+ <div className="space-y-1">
678
+ <Label className="text-xs">Alignment</Label>
679
+ <Select
680
+ value={block.alignment}
681
+ onValueChange={(v) => onChange({ ...block, alignment: v as 'left' | 'center' | 'right' })}
682
+ >
683
+ <SelectTrigger className="h-8 text-xs">
684
+ <SelectValue />
685
+ </SelectTrigger>
686
+ <SelectContent>
687
+ <SelectItem value="left">Left</SelectItem>
688
+ <SelectItem value="center">Center</SelectItem>
689
+ <SelectItem value="right">Right</SelectItem>
690
+ </SelectContent>
691
+ </Select>
692
+ </div>
693
+ </div>
694
+ )
695
+ }
696
+
697
+ // --- Global Styles Panel ---
698
+ function GlobalStylesPanel({
699
+ globalStyles,
700
+ onChange,
701
+ }: {
702
+ globalStyles: GlobalStyles
703
+ onChange: (s: GlobalStyles) => void
704
+ }) {
705
+ return (
706
+ <div className="space-y-4">
707
+ <div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
708
+ Email Design
709
+ </div>
710
+
711
+ <div className="space-y-1">
712
+ <Label className="text-xs">Background Color</Label>
713
+ <div className="flex gap-2">
714
+ <input
715
+ type="color"
716
+ value={globalStyles.backgroundColor}
717
+ onChange={(e) => onChange({ ...globalStyles, backgroundColor: e.target.value })}
718
+ className="h-8 w-8 rounded border cursor-pointer"
719
+ />
720
+ <Input
721
+ value={globalStyles.backgroundColor}
722
+ onChange={(e) => onChange({ ...globalStyles, backgroundColor: e.target.value })}
723
+ className="h-8 text-xs"
724
+ />
725
+ </div>
726
+ </div>
727
+
728
+ <div className="space-y-1">
729
+ <Label className="text-xs">Content Width (px)</Label>
730
+ <Input
731
+ type="number"
732
+ value={globalStyles.contentWidth}
733
+ onChange={(e) =>
734
+ onChange({ ...globalStyles, contentWidth: parseInt(e.target.value) || 600 })
735
+ }
736
+ className="h-8 text-xs"
737
+ min={400}
738
+ max={800}
739
+ />
740
+ </div>
741
+
742
+ <div className="space-y-1">
743
+ <Label className="text-xs">Font Family</Label>
744
+ <Select
745
+ value={globalStyles.fontFamily}
746
+ onValueChange={(v) => onChange({ ...globalStyles, fontFamily: v })}
747
+ >
748
+ <SelectTrigger className="h-8 text-xs">
749
+ <SelectValue />
750
+ </SelectTrigger>
751
+ <SelectContent>
752
+ <SelectItem value='-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'>
753
+ System (Default)
754
+ </SelectItem>
755
+ <SelectItem value="Arial, sans-serif">Arial</SelectItem>
756
+ <SelectItem value="Georgia, serif">Georgia</SelectItem>
757
+ <SelectItem value="Verdana, sans-serif">Verdana</SelectItem>
758
+ <SelectItem value="'Trebuchet MS', sans-serif">Trebuchet MS</SelectItem>
759
+ </SelectContent>
760
+ </Select>
761
+ </div>
762
+
763
+ <div className="border-t pt-4 space-y-2">
764
+ <div className="text-xs font-medium text-muted-foreground">Themes</div>
765
+ <div className="grid gap-2">
766
+ {THEME_PRESETS.map((preset) => (
767
+ <button
768
+ key={preset.id}
769
+ onClick={() => onChange(preset.globalStyles)}
770
+ className={cn(
771
+ 'flex items-center gap-3 p-2 rounded border text-left text-xs transition-colors',
772
+ globalStyles.theme === preset.id
773
+ ? 'border-primary bg-primary/5'
774
+ : 'border-border hover:bg-muted'
775
+ )}
776
+ >
777
+ <div
778
+ className="h-6 w-6 rounded border"
779
+ style={{ backgroundColor: preset.globalStyles.backgroundColor }}
780
+ />
781
+ <div>
782
+ <div className="font-medium">{preset.name}</div>
783
+ <div className="text-muted-foreground">{preset.description}</div>
784
+ </div>
785
+ </button>
786
+ ))}
787
+ </div>
788
+ </div>
789
+ </div>
790
+ )
791
+ }