@startsimpli/ui 0.4.6 → 0.4.8

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