@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,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
+ }