@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.
- package/package.json +2 -1
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandPalette.tsx +344 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +3 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/index.ts +14 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +791 -0
- package/src/components/email-editor/email-editor.tsx +886 -0
- package/src/components/email-editor/index.ts +50 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttChart.tsx +25 -25
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- 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
|
+
}
|