@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.
- package/package.json +2 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -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/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -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/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -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/__tests__/blocks.test.tsx +818 -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 +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -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/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- 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/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -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/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -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/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// Email editor type definitions
|
|
2
|
+
// Extends the original block types with sections, rows, and enhanced block properties
|
|
3
|
+
|
|
4
|
+
export type BlockType =
|
|
5
|
+
| 'text'
|
|
6
|
+
| 'metrics'
|
|
7
|
+
| 'divider'
|
|
8
|
+
| 'cta'
|
|
9
|
+
| 'image'
|
|
10
|
+
| 'spacer'
|
|
11
|
+
| 'social'
|
|
12
|
+
| 'header'
|
|
13
|
+
| 'footer'
|
|
14
|
+
|
|
15
|
+
export type ColumnLayout = '1' | '2' | '3' | '2-1' | '1-2'
|
|
16
|
+
|
|
17
|
+
export interface BlockStyle {
|
|
18
|
+
backgroundColor?: string
|
|
19
|
+
paddingTop?: number
|
|
20
|
+
paddingBottom?: number
|
|
21
|
+
paddingLeft?: number
|
|
22
|
+
paddingRight?: number
|
|
23
|
+
marginTop?: number
|
|
24
|
+
marginBottom?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BaseBlock {
|
|
28
|
+
id: string
|
|
29
|
+
type: BlockType
|
|
30
|
+
style?: BlockStyle
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Text Block ---
|
|
34
|
+
export interface TextBlock extends BaseBlock {
|
|
35
|
+
type: 'text'
|
|
36
|
+
content: string // HTML content from Tiptap
|
|
37
|
+
fontSize?: number
|
|
38
|
+
fontFamily?: string
|
|
39
|
+
lineHeight?: number
|
|
40
|
+
textColor?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Metrics Block (kept from existing) ---
|
|
44
|
+
export interface MetricItem {
|
|
45
|
+
id: string
|
|
46
|
+
label: string
|
|
47
|
+
value: string
|
|
48
|
+
change?: string
|
|
49
|
+
changeType?: 'positive' | 'negative' | 'neutral'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MetricsBlock extends BaseBlock {
|
|
53
|
+
type: 'metrics'
|
|
54
|
+
title?: string
|
|
55
|
+
metrics: MetricItem[]
|
|
56
|
+
columns: 2 | 3 | 4
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Divider Block ---
|
|
60
|
+
export interface DividerBlock extends BaseBlock {
|
|
61
|
+
type: 'divider'
|
|
62
|
+
dividerStyle: 'solid' | 'dashed' | 'dotted' | 'space'
|
|
63
|
+
color?: string
|
|
64
|
+
thickness?: number
|
|
65
|
+
width?: number // percentage 0-100
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- CTA / Button Block ---
|
|
69
|
+
export interface CTABlock extends BaseBlock {
|
|
70
|
+
type: 'cta'
|
|
71
|
+
text: string
|
|
72
|
+
url: string
|
|
73
|
+
buttonColor?: string
|
|
74
|
+
textColor?: string
|
|
75
|
+
borderRadius?: number
|
|
76
|
+
paddingH?: number
|
|
77
|
+
paddingV?: number
|
|
78
|
+
alignment: 'left' | 'center' | 'right'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Image Block ---
|
|
82
|
+
export interface ImageBlock extends BaseBlock {
|
|
83
|
+
type: 'image'
|
|
84
|
+
url: string
|
|
85
|
+
alt: string
|
|
86
|
+
caption?: string
|
|
87
|
+
alignment: 'left' | 'center' | 'right'
|
|
88
|
+
width: number // percentage 0-100
|
|
89
|
+
linkUrl?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Spacer Block ---
|
|
93
|
+
export interface SpacerBlock extends BaseBlock {
|
|
94
|
+
type: 'spacer'
|
|
95
|
+
height: number // px
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Social Block ---
|
|
99
|
+
export interface SocialLink {
|
|
100
|
+
id: string
|
|
101
|
+
platform: 'linkedin' | 'twitter' | 'facebook' | 'instagram' | 'youtube' | 'github' | 'website'
|
|
102
|
+
url: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SocialBlock extends BaseBlock {
|
|
106
|
+
type: 'social'
|
|
107
|
+
links: SocialLink[]
|
|
108
|
+
iconSize?: number
|
|
109
|
+
alignment: 'left' | 'center' | 'right'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Header Block ---
|
|
113
|
+
export interface HeaderBlock extends BaseBlock {
|
|
114
|
+
type: 'header'
|
|
115
|
+
logoUrl?: string
|
|
116
|
+
companyName: string
|
|
117
|
+
alignment: 'left' | 'center' | 'right'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Footer Block ---
|
|
121
|
+
export interface FooterBlock extends BaseBlock {
|
|
122
|
+
type: 'footer'
|
|
123
|
+
companyName: string
|
|
124
|
+
address?: string
|
|
125
|
+
showUnsubscribe: boolean
|
|
126
|
+
unsubscribeUrl?: string
|
|
127
|
+
alignment: 'left' | 'center' | 'right'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type Block =
|
|
131
|
+
| TextBlock
|
|
132
|
+
| MetricsBlock
|
|
133
|
+
| DividerBlock
|
|
134
|
+
| CTABlock
|
|
135
|
+
| ImageBlock
|
|
136
|
+
| SpacerBlock
|
|
137
|
+
| SocialBlock
|
|
138
|
+
| HeaderBlock
|
|
139
|
+
| FooterBlock
|
|
140
|
+
|
|
141
|
+
// --- Row / Section ---
|
|
142
|
+
export interface Row {
|
|
143
|
+
id: string
|
|
144
|
+
layout: ColumnLayout
|
|
145
|
+
columns: Block[][] // array of columns, each column is an array of blocks
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface Section {
|
|
149
|
+
id: string
|
|
150
|
+
rows: Row[]
|
|
151
|
+
backgroundColor?: string
|
|
152
|
+
paddingTop?: number
|
|
153
|
+
paddingBottom?: number
|
|
154
|
+
paddingLeft?: number
|
|
155
|
+
paddingRight?: number
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Global Styles ---
|
|
159
|
+
export interface GlobalStyles {
|
|
160
|
+
backgroundColor: string
|
|
161
|
+
contentWidth: number
|
|
162
|
+
fontFamily: string
|
|
163
|
+
theme: 'clean' | 'minimal' | 'bold'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Editor State ---
|
|
167
|
+
export interface EditorSelection {
|
|
168
|
+
sectionIndex: number
|
|
169
|
+
rowIndex: number
|
|
170
|
+
columnIndex: number
|
|
171
|
+
blockIndex: number
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Merge Field Definition (re-export compatible) ---
|
|
175
|
+
export interface MergeFieldDefinition {
|
|
176
|
+
key: string
|
|
177
|
+
label: string
|
|
178
|
+
example: string
|
|
179
|
+
category: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Helper Functions ---
|
|
183
|
+
|
|
184
|
+
let _idCounter = 0
|
|
185
|
+
|
|
186
|
+
function generateId(prefix: string): string {
|
|
187
|
+
_idCounter++
|
|
188
|
+
return `${prefix}-${Date.now()}-${_idCounter}-${Math.random().toString(36).substr(2, 6)}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createBlock(type: BlockType): Block {
|
|
192
|
+
const id = generateId('block')
|
|
193
|
+
|
|
194
|
+
switch (type) {
|
|
195
|
+
case 'text':
|
|
196
|
+
return { id, type: 'text', content: '' }
|
|
197
|
+
case 'metrics':
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
type: 'metrics',
|
|
201
|
+
title: 'Key Metrics',
|
|
202
|
+
metrics: [
|
|
203
|
+
{ id: generateId('metric'), label: 'Revenue', value: '$0', change: '+0%', changeType: 'neutral' },
|
|
204
|
+
{ id: generateId('metric'), label: 'Users', value: '0', change: '+0%', changeType: 'neutral' },
|
|
205
|
+
],
|
|
206
|
+
columns: 2,
|
|
207
|
+
}
|
|
208
|
+
case 'divider':
|
|
209
|
+
return { id, type: 'divider', dividerStyle: 'solid', thickness: 1, color: '#d1d5db', width: 100 }
|
|
210
|
+
case 'cta':
|
|
211
|
+
return {
|
|
212
|
+
id,
|
|
213
|
+
type: 'cta',
|
|
214
|
+
text: 'Learn More',
|
|
215
|
+
url: '',
|
|
216
|
+
buttonColor: '#2563eb',
|
|
217
|
+
textColor: '#ffffff',
|
|
218
|
+
borderRadius: 6,
|
|
219
|
+
paddingH: 24,
|
|
220
|
+
paddingV: 12,
|
|
221
|
+
alignment: 'center',
|
|
222
|
+
}
|
|
223
|
+
case 'image':
|
|
224
|
+
return {
|
|
225
|
+
id,
|
|
226
|
+
type: 'image',
|
|
227
|
+
url: '',
|
|
228
|
+
alt: '',
|
|
229
|
+
caption: '',
|
|
230
|
+
alignment: 'center',
|
|
231
|
+
width: 100,
|
|
232
|
+
}
|
|
233
|
+
case 'spacer':
|
|
234
|
+
return { id, type: 'spacer', height: 32 }
|
|
235
|
+
case 'social':
|
|
236
|
+
return {
|
|
237
|
+
id,
|
|
238
|
+
type: 'social',
|
|
239
|
+
links: [
|
|
240
|
+
{ id: generateId('social'), platform: 'linkedin', url: '' },
|
|
241
|
+
{ id: generateId('social'), platform: 'twitter', url: '' },
|
|
242
|
+
],
|
|
243
|
+
iconSize: 24,
|
|
244
|
+
alignment: 'center',
|
|
245
|
+
}
|
|
246
|
+
case 'header':
|
|
247
|
+
return {
|
|
248
|
+
id,
|
|
249
|
+
type: 'header',
|
|
250
|
+
logoUrl: '',
|
|
251
|
+
companyName: 'Your Company',
|
|
252
|
+
alignment: 'center',
|
|
253
|
+
}
|
|
254
|
+
case 'footer':
|
|
255
|
+
return {
|
|
256
|
+
id,
|
|
257
|
+
type: 'footer',
|
|
258
|
+
companyName: 'Your Company',
|
|
259
|
+
address: '',
|
|
260
|
+
showUnsubscribe: true,
|
|
261
|
+
alignment: 'center',
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function createRow(layout: ColumnLayout = '1'): Row {
|
|
267
|
+
const colCount = getColumnCount(layout)
|
|
268
|
+
return {
|
|
269
|
+
id: generateId('row'),
|
|
270
|
+
layout,
|
|
271
|
+
columns: Array.from({ length: colCount }, () => []),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function createSection(): Section {
|
|
276
|
+
return {
|
|
277
|
+
id: generateId('section'),
|
|
278
|
+
rows: [createRow('1')],
|
|
279
|
+
paddingTop: 16,
|
|
280
|
+
paddingBottom: 16,
|
|
281
|
+
paddingLeft: 0,
|
|
282
|
+
paddingRight: 0,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function getColumnCount(layout: ColumnLayout): number {
|
|
287
|
+
switch (layout) {
|
|
288
|
+
case '1':
|
|
289
|
+
return 1
|
|
290
|
+
case '2':
|
|
291
|
+
case '2-1':
|
|
292
|
+
case '1-2':
|
|
293
|
+
return 2
|
|
294
|
+
case '3':
|
|
295
|
+
return 3
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function getColumnWidths(layout: ColumnLayout): number[] {
|
|
300
|
+
switch (layout) {
|
|
301
|
+
case '1':
|
|
302
|
+
return [100]
|
|
303
|
+
case '2':
|
|
304
|
+
return [50, 50]
|
|
305
|
+
case '3':
|
|
306
|
+
return [33.33, 33.33, 33.34]
|
|
307
|
+
case '2-1':
|
|
308
|
+
return [66.67, 33.33]
|
|
309
|
+
case '1-2':
|
|
310
|
+
return [33.33, 66.67]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- Serialization (backward compatible) ---
|
|
315
|
+
|
|
316
|
+
// Convert flat Block[] (old format) to Section[] (new format)
|
|
317
|
+
export function migrateFromLegacy(legacyBlocks: Block[]): Section[] {
|
|
318
|
+
if (legacyBlocks.length === 0) return [createSection()]
|
|
319
|
+
|
|
320
|
+
const section = createSection()
|
|
321
|
+
|
|
322
|
+
// Each old block becomes a single block in a 1-column row
|
|
323
|
+
section.rows = legacyBlocks.map((block) => {
|
|
324
|
+
const newRow = createRow('1')
|
|
325
|
+
// Migrate old divider 'style' field to 'dividerStyle'
|
|
326
|
+
if (block.type === 'divider') {
|
|
327
|
+
const oldBlock = block as unknown as { style: string }
|
|
328
|
+
const divBlock = block as unknown as DividerBlock
|
|
329
|
+
if (!divBlock.dividerStyle && oldBlock.style) {
|
|
330
|
+
divBlock.dividerStyle = oldBlock.style as DividerBlock['dividerStyle']
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Migrate old CTA to new format
|
|
334
|
+
if (block.type === 'cta') {
|
|
335
|
+
const oldBlock = block as unknown as { style?: string }
|
|
336
|
+
const ctaBlock = block as unknown as CTABlock
|
|
337
|
+
if (!ctaBlock.buttonColor && oldBlock.style) {
|
|
338
|
+
const colorMap: Record<string, string> = {
|
|
339
|
+
primary: '#2563eb',
|
|
340
|
+
secondary: '#f3f4f6',
|
|
341
|
+
outline: '#ffffff',
|
|
342
|
+
}
|
|
343
|
+
ctaBlock.buttonColor = colorMap[oldBlock.style] || '#2563eb'
|
|
344
|
+
ctaBlock.textColor = oldBlock.style === 'primary' ? '#ffffff' : '#1f2937'
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Migrate old image width
|
|
348
|
+
if (block.type === 'image') {
|
|
349
|
+
const oldBlock = block as unknown as { width?: string | number }
|
|
350
|
+
const imgBlock = block as unknown as ImageBlock
|
|
351
|
+
if (typeof oldBlock.width === 'string') {
|
|
352
|
+
const widthMap: Record<string, number> = { small: 50, medium: 75, large: 90, full: 100 }
|
|
353
|
+
imgBlock.width = widthMap[oldBlock.width] || 100
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
newRow.columns[0] = [block]
|
|
357
|
+
return newRow
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
return [section]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Convert Section[] back to flat Block[] for backward compatibility
|
|
364
|
+
export function flattenToLegacy(sections: Section[]): Block[] {
|
|
365
|
+
const blocks: Block[] = []
|
|
366
|
+
for (const section of sections) {
|
|
367
|
+
for (const row of section.rows) {
|
|
368
|
+
for (const column of row.columns) {
|
|
369
|
+
blocks.push(...column)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return blocks
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function serializeSections(sections: Section[]): string {
|
|
377
|
+
return JSON.stringify(sections)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function deserializeSections(json: string | null): Section[] {
|
|
381
|
+
if (!json) return [createSection()]
|
|
382
|
+
try {
|
|
383
|
+
const parsed = JSON.parse(json)
|
|
384
|
+
// Detect legacy flat block array format
|
|
385
|
+
if (Array.isArray(parsed) && parsed.length > 0 && 'type' in parsed[0] && !('rows' in parsed[0])) {
|
|
386
|
+
return migrateFromLegacy(parsed as Block[])
|
|
387
|
+
}
|
|
388
|
+
return parsed as Section[]
|
|
389
|
+
} catch {
|
|
390
|
+
return [createSection()]
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Also keep backward-compatible serialize/deserialize that work with flat blocks
|
|
395
|
+
export function serializeBlocks(blocks: Block[]): string {
|
|
396
|
+
return JSON.stringify(blocks)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function deserializeBlocks(json: string | null): Block[] {
|
|
400
|
+
if (!json) return []
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(json)
|
|
403
|
+
} catch {
|
|
404
|
+
return []
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export const DEFAULT_GLOBAL_STYLES: GlobalStyles = {
|
|
409
|
+
backgroundColor: '#f3f4f6',
|
|
410
|
+
contentWidth: 600,
|
|
411
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
412
|
+
theme: 'clean',
|
|
413
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Section,
|
|
3
|
+
GlobalStyles,
|
|
4
|
+
DEFAULT_GLOBAL_STYLES,
|
|
5
|
+
createSection,
|
|
6
|
+
createRow,
|
|
7
|
+
createBlock,
|
|
8
|
+
} from '../types'
|
|
9
|
+
|
|
10
|
+
// Preset themes
|
|
11
|
+
export interface ThemePreset {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
globalStyles: GlobalStyles
|
|
16
|
+
sectionDefaults: {
|
|
17
|
+
backgroundColor?: string
|
|
18
|
+
paddingTop?: number
|
|
19
|
+
paddingBottom?: number
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const THEME_PRESETS: ThemePreset[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'clean',
|
|
26
|
+
name: 'Clean',
|
|
27
|
+
description: 'White background with subtle gray accents',
|
|
28
|
+
globalStyles: {
|
|
29
|
+
...DEFAULT_GLOBAL_STYLES,
|
|
30
|
+
backgroundColor: '#f3f4f6',
|
|
31
|
+
theme: 'clean',
|
|
32
|
+
},
|
|
33
|
+
sectionDefaults: {
|
|
34
|
+
paddingTop: 16,
|
|
35
|
+
paddingBottom: 16,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'minimal',
|
|
40
|
+
name: 'Minimal',
|
|
41
|
+
description: 'Pure white with thin borders',
|
|
42
|
+
globalStyles: {
|
|
43
|
+
...DEFAULT_GLOBAL_STYLES,
|
|
44
|
+
backgroundColor: '#ffffff',
|
|
45
|
+
theme: 'minimal',
|
|
46
|
+
},
|
|
47
|
+
sectionDefaults: {
|
|
48
|
+
paddingTop: 12,
|
|
49
|
+
paddingBottom: 12,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'bold',
|
|
54
|
+
name: 'Bold',
|
|
55
|
+
description: 'Dark background with vivid accents',
|
|
56
|
+
globalStyles: {
|
|
57
|
+
...DEFAULT_GLOBAL_STYLES,
|
|
58
|
+
backgroundColor: '#1f2937',
|
|
59
|
+
theme: 'bold',
|
|
60
|
+
},
|
|
61
|
+
sectionDefaults: {
|
|
62
|
+
backgroundColor: '#111827',
|
|
63
|
+
paddingTop: 20,
|
|
64
|
+
paddingBottom: 20,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
// Starter template: basic investor update
|
|
70
|
+
export function createInvestorUpdateTemplate(): Section[] {
|
|
71
|
+
const headerBlock = createBlock('header')
|
|
72
|
+
const introText = createBlock('text')
|
|
73
|
+
if (introText.type === 'text') {
|
|
74
|
+
introText.content = '<p>Hi {{investor.firstName}},</p><p>Here is our latest update for {{date.quarter}}.</p>'
|
|
75
|
+
}
|
|
76
|
+
const metricsBlock = createBlock('metrics')
|
|
77
|
+
const bodyText = createBlock('text')
|
|
78
|
+
if (bodyText.type === 'text') {
|
|
79
|
+
bodyText.content = '<h2>Highlights</h2><ul><li>Key achievement one</li><li>Key achievement two</li></ul>'
|
|
80
|
+
}
|
|
81
|
+
const ctaBlock = createBlock('cta')
|
|
82
|
+
const divider = createBlock('divider')
|
|
83
|
+
const footerBlock = createBlock('footer')
|
|
84
|
+
|
|
85
|
+
const headerSection = createSection()
|
|
86
|
+
headerSection.rows = [createRow('1')]
|
|
87
|
+
headerSection.rows[0].columns[0] = [headerBlock]
|
|
88
|
+
|
|
89
|
+
const introSection = createSection()
|
|
90
|
+
introSection.rows = [createRow('1')]
|
|
91
|
+
introSection.rows[0].columns[0] = [introText]
|
|
92
|
+
|
|
93
|
+
const metricsSection = createSection()
|
|
94
|
+
metricsSection.rows = [createRow('1')]
|
|
95
|
+
metricsSection.rows[0].columns[0] = [metricsBlock]
|
|
96
|
+
|
|
97
|
+
const bodySection = createSection()
|
|
98
|
+
bodySection.rows = [createRow('1')]
|
|
99
|
+
bodySection.rows[0].columns[0] = [bodyText]
|
|
100
|
+
|
|
101
|
+
const ctaSection = createSection()
|
|
102
|
+
ctaSection.rows = [createRow('1')]
|
|
103
|
+
ctaSection.rows[0].columns[0] = [ctaBlock]
|
|
104
|
+
|
|
105
|
+
const footerSection = createSection()
|
|
106
|
+
footerSection.rows = [createRow('1'), createRow('1')]
|
|
107
|
+
footerSection.rows[0].columns[0] = [divider]
|
|
108
|
+
footerSection.rows[1].columns[0] = [footerBlock]
|
|
109
|
+
|
|
110
|
+
return [headerSection, introSection, metricsSection, bodySection, ctaSection, footerSection]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Empty starter
|
|
114
|
+
export function createEmptyTemplate(): Section[] {
|
|
115
|
+
return [createSection()]
|
|
116
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Section } from '../types'
|
|
2
|
+
|
|
3
|
+
const MAX_HISTORY = 50
|
|
4
|
+
|
|
5
|
+
export interface UndoRedoState {
|
|
6
|
+
past: Section[][]
|
|
7
|
+
present: Section[]
|
|
8
|
+
future: Section[][]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createUndoRedoState(initial: Section[]): UndoRedoState {
|
|
12
|
+
return {
|
|
13
|
+
past: [],
|
|
14
|
+
present: initial,
|
|
15
|
+
future: [],
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pushState(state: UndoRedoState, next: Section[]): UndoRedoState {
|
|
20
|
+
const past = [...state.past, state.present]
|
|
21
|
+
if (past.length > MAX_HISTORY) {
|
|
22
|
+
past.shift()
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
past,
|
|
26
|
+
present: next,
|
|
27
|
+
future: [],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function undo(state: UndoRedoState): UndoRedoState {
|
|
32
|
+
if (state.past.length === 0) return state
|
|
33
|
+
const previous = state.past[state.past.length - 1]
|
|
34
|
+
const newPast = state.past.slice(0, -1)
|
|
35
|
+
return {
|
|
36
|
+
past: newPast,
|
|
37
|
+
present: previous,
|
|
38
|
+
future: [state.present, ...state.future],
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function redo(state: UndoRedoState): UndoRedoState {
|
|
43
|
+
if (state.future.length === 0) return state
|
|
44
|
+
const next = state.future[0]
|
|
45
|
+
const newFuture = state.future.slice(1)
|
|
46
|
+
return {
|
|
47
|
+
past: [...state.past, state.present],
|
|
48
|
+
present: next,
|
|
49
|
+
future: newFuture,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function canUndo(state: UndoRedoState): boolean {
|
|
54
|
+
return state.past.length > 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function canRedo(state: UndoRedoState): boolean {
|
|
58
|
+
return state.future.length > 0
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Zap } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export interface EnrichButtonProps {
|
|
5
|
+
onClick: () => void
|
|
6
|
+
isLoading?: boolean
|
|
7
|
+
label?: string
|
|
8
|
+
size?: 'sm' | 'md'
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EnrichButton({
|
|
13
|
+
onClick,
|
|
14
|
+
isLoading = false,
|
|
15
|
+
label = 'Enrich',
|
|
16
|
+
size = 'sm',
|
|
17
|
+
className = '',
|
|
18
|
+
}: EnrichButtonProps) {
|
|
19
|
+
const sizeClasses = size === 'sm'
|
|
20
|
+
? 'px-3 py-1.5 text-xs'
|
|
21
|
+
: 'px-4 py-2 text-sm'
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
onClick={onClick}
|
|
26
|
+
disabled={isLoading}
|
|
27
|
+
className={`flex items-center gap-1.5 font-medium text-primary-700 bg-primary-50 hover:bg-primary-100 rounded-md disabled:opacity-50 transition-colors ${sizeClasses} ${className}`}
|
|
28
|
+
>
|
|
29
|
+
<Zap className={size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} />
|
|
30
|
+
{isLoading ? 'Enriching...' : label}
|
|
31
|
+
</button>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Clock, RefreshCw } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export interface QueueStatus {
|
|
5
|
+
pending: number
|
|
6
|
+
processing: number
|
|
7
|
+
completed: number
|
|
8
|
+
failed: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EnrichmentProgressProps {
|
|
12
|
+
queueStatus: QueueStatus | null
|
|
13
|
+
isLoading?: boolean
|
|
14
|
+
onRefresh?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STATUS_ITEMS: Array<{
|
|
18
|
+
key: keyof QueueStatus
|
|
19
|
+
label: string
|
|
20
|
+
color: string
|
|
21
|
+
}> = [
|
|
22
|
+
{ key: 'pending', label: 'Pending', color: 'text-yellow-600' },
|
|
23
|
+
{ key: 'processing', label: 'Processing', color: 'text-blue-600' },
|
|
24
|
+
{ key: 'completed', label: 'Completed', color: 'text-green-600' },
|
|
25
|
+
{ key: 'failed', label: 'Failed', color: 'text-red-600' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export function EnrichmentProgress({
|
|
29
|
+
queueStatus,
|
|
30
|
+
isLoading = false,
|
|
31
|
+
onRefresh,
|
|
32
|
+
}: EnrichmentProgressProps) {
|
|
33
|
+
if (!queueStatus) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6 text-center text-gray-500">
|
|
36
|
+
<Clock className="w-8 h-8 mx-auto mb-2 text-gray-300" />
|
|
37
|
+
<p className="text-sm">Queue status unavailable</p>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
{onRefresh && (
|
|
45
|
+
<div className="flex justify-end mb-3">
|
|
46
|
+
<button
|
|
47
|
+
onClick={onRefresh}
|
|
48
|
+
disabled={isLoading}
|
|
49
|
+
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 disabled:opacity-50"
|
|
50
|
+
>
|
|
51
|
+
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
52
|
+
Refresh
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
57
|
+
{STATUS_ITEMS.map(({ key, label, color }) => (
|
|
58
|
+
<div key={key} className="bg-white rounded-lg border border-gray-200 p-4 text-center">
|
|
59
|
+
<div className={`text-2xl font-bold ${color}`}>{queueStatus[key]}</div>
|
|
60
|
+
<div className="text-sm text-gray-600 mt-1">{label}</div>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|