@startsimpli/ui 0.4.7 → 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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/account/__tests__/account.test.tsx +315 -0
  4. package/src/components/command-palette/CommandGroup.tsx +23 -0
  5. package/src/components/command-palette/CommandPalette.tsx +183 -200
  6. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  7. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  8. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  9. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  10. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  11. package/src/components/command-palette/index.ts +6 -0
  12. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  13. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  14. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  15. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  16. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  17. package/src/components/dashboard/index.ts +6 -0
  18. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  19. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  20. package/src/components/dialog/index.ts +3 -0
  21. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  22. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  23. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  24. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  25. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  26. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  27. package/src/components/email-editor/editor-sidebar.tsx +6 -731
  28. package/src/components/email-editor/email-editor.tsx +78 -467
  29. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  30. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  31. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  32. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  33. package/src/components/email-editor/index.ts +1 -0
  34. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  35. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  36. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  37. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  38. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  39. package/src/components/email-editor/panels/index.ts +3 -0
  40. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  41. package/src/components/gantt/GanttBoardView.tsx +71 -0
  42. package/src/components/gantt/GanttChart.tsx +134 -881
  43. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  44. package/src/components/gantt/GanttListView.tsx +63 -0
  45. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  46. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  47. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  48. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  49. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  50. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  51. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  52. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  53. package/src/components/gantt/index.ts +10 -0
  54. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  55. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  56. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  57. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  58. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  59. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  60. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  61. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,181 @@
1
+ import { useState, useCallback } from 'react'
2
+ import type { Section, EditorSelection } from '../types'
3
+
4
+ interface DragSource {
5
+ sourceSection: number
6
+ sourceRow: number
7
+ sourceCol: number
8
+ sourceBlock: number
9
+ }
10
+
11
+ interface DragTarget {
12
+ sectionIndex: number
13
+ rowIndex: number
14
+ colIndex: number
15
+ blockIndex: number
16
+ }
17
+
18
+ interface UseDragDropOptions {
19
+ sections: Section[]
20
+ commitChange: (newSections: Section[]) => void
21
+ setSelection: (sel: EditorSelection | null) => void
22
+ }
23
+
24
+ interface UseDragDropReturn {
25
+ dragState: DragSource | null
26
+ dragOverTarget: DragTarget | null
27
+ setDragOverTarget: React.Dispatch<React.SetStateAction<DragTarget | null>>
28
+ handleDragStart: (
29
+ sectionIndex: number,
30
+ rowIndex: number,
31
+ colIndex: number,
32
+ blockIndex: number
33
+ ) => void
34
+ handleDragOver: (
35
+ e: React.DragEvent,
36
+ sectionIndex: number,
37
+ rowIndex: number,
38
+ colIndex: number,
39
+ blockIndex: number
40
+ ) => void
41
+ handleDrop: (
42
+ e: React.DragEvent,
43
+ targetSection: number,
44
+ targetRow: number,
45
+ targetCol: number,
46
+ targetBlock: number
47
+ ) => void
48
+ handleDragEnd: () => void
49
+ }
50
+
51
+ export function useDragDrop({
52
+ sections,
53
+ commitChange,
54
+ setSelection,
55
+ }: UseDragDropOptions): UseDragDropReturn {
56
+ const [dragState, setDragState] = useState<DragSource | null>(null)
57
+ const [dragOverTarget, setDragOverTarget] = useState<DragTarget | null>(null)
58
+
59
+ const handleDragStart = useCallback(
60
+ (sectionIndex: number, rowIndex: number, colIndex: number, blockIndex: number) => {
61
+ setDragState({
62
+ sourceSection: sectionIndex,
63
+ sourceRow: rowIndex,
64
+ sourceCol: colIndex,
65
+ sourceBlock: blockIndex,
66
+ })
67
+ },
68
+ []
69
+ )
70
+
71
+ const handleDragOver = useCallback(
72
+ (
73
+ e: React.DragEvent,
74
+ sectionIndex: number,
75
+ rowIndex: number,
76
+ colIndex: number,
77
+ blockIndex: number
78
+ ) => {
79
+ e.preventDefault()
80
+ setDragOverTarget({ sectionIndex, rowIndex, colIndex, blockIndex })
81
+ },
82
+ []
83
+ )
84
+
85
+ const handleDrop = useCallback(
86
+ (
87
+ e: React.DragEvent,
88
+ targetSection: number,
89
+ targetRow: number,
90
+ targetCol: number,
91
+ targetBlock: number
92
+ ) => {
93
+ e.preventDefault()
94
+ if (!dragState) return
95
+
96
+ const { sourceSection, sourceRow, sourceCol, sourceBlock } = dragState
97
+
98
+ // Get the block being moved
99
+ const block =
100
+ sections[sourceSection]?.rows[sourceRow]?.columns[sourceCol]?.[sourceBlock]
101
+ if (!block) return
102
+
103
+ // Remove from source
104
+ let newSections = sections.map((section, si) => {
105
+ if (si !== sourceSection) return section
106
+ return {
107
+ ...section,
108
+ rows: section.rows.map((row, ri) => {
109
+ if (ri !== sourceRow) return row
110
+ return {
111
+ ...row,
112
+ columns: row.columns.map((col, ci) => {
113
+ if (ci !== sourceCol) return col
114
+ return col.filter((_, bi) => bi !== sourceBlock)
115
+ }),
116
+ }
117
+ }),
118
+ }
119
+ })
120
+
121
+ // Adjust target index if removing from same column before target
122
+ let adjustedTargetBlock = targetBlock
123
+ if (
124
+ sourceSection === targetSection &&
125
+ sourceRow === targetRow &&
126
+ sourceCol === targetCol &&
127
+ sourceBlock < targetBlock
128
+ ) {
129
+ adjustedTargetBlock -= 1
130
+ }
131
+
132
+ // Insert at target
133
+ newSections = newSections.map((section, si) => {
134
+ if (si !== targetSection) return section
135
+ return {
136
+ ...section,
137
+ rows: section.rows.map((row, ri) => {
138
+ if (ri !== targetRow) return row
139
+ return {
140
+ ...row,
141
+ columns: row.columns.map((col, ci) => {
142
+ if (ci !== targetCol) return col
143
+ return [
144
+ ...col.slice(0, adjustedTargetBlock),
145
+ block,
146
+ ...col.slice(adjustedTargetBlock),
147
+ ]
148
+ }),
149
+ }
150
+ }),
151
+ }
152
+ })
153
+
154
+ commitChange(newSections)
155
+ setDragState(null)
156
+ setDragOverTarget(null)
157
+ setSelection({
158
+ sectionIndex: targetSection,
159
+ rowIndex: targetRow,
160
+ columnIndex: targetCol,
161
+ blockIndex: adjustedTargetBlock,
162
+ })
163
+ },
164
+ [dragState, sections, commitChange, setSelection]
165
+ )
166
+
167
+ const handleDragEnd = useCallback(() => {
168
+ setDragState(null)
169
+ setDragOverTarget(null)
170
+ }, [])
171
+
172
+ return {
173
+ dragState,
174
+ dragOverTarget,
175
+ setDragOverTarget,
176
+ handleDragStart,
177
+ handleDragOver,
178
+ handleDrop,
179
+ handleDragEnd,
180
+ }
181
+ }
@@ -0,0 +1,426 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react'
2
+ import {
3
+ Section,
4
+ Block,
5
+ BlockType,
6
+ ColumnLayout,
7
+ EditorSelection,
8
+ createBlock,
9
+ createRow,
10
+ createSection,
11
+ getColumnCount,
12
+ } from '../types'
13
+ import {
14
+ UndoRedoState,
15
+ createUndoRedoState,
16
+ pushState,
17
+ undo as undoState,
18
+ redo as redoState,
19
+ canUndo as canUndoFn,
20
+ canRedo as canRedoFn,
21
+ } from '../utils/undo-redo'
22
+
23
+ export interface UseEmailEditorStateOptions {
24
+ initialSections: Section[]
25
+ onChange?: (sections: Section[]) => void
26
+ }
27
+
28
+ export interface UseEmailEditorStateReturn {
29
+ sections: Section[]
30
+ selection: EditorSelection | null
31
+ setSelection: (s: EditorSelection | null) => void
32
+ updateBlock: (
33
+ sectionIdx: number,
34
+ rowIdx: number,
35
+ colIdx: number,
36
+ blockIdx: number,
37
+ updates: Partial<Block>
38
+ ) => void
39
+ addBlock: (
40
+ sectionIdx: number,
41
+ rowIdx: number,
42
+ colIdx: number,
43
+ type: BlockType,
44
+ afterBlockIdx?: number
45
+ ) => void
46
+ removeBlock: (
47
+ sectionIdx: number,
48
+ rowIdx: number,
49
+ colIdx: number,
50
+ blockIdx: number
51
+ ) => void
52
+ duplicateBlock: (
53
+ sectionIdx: number,
54
+ rowIdx: number,
55
+ colIdx: number,
56
+ blockIdx: number
57
+ ) => void
58
+ moveBlock: (
59
+ sectionIdx: number,
60
+ rowIdx: number,
61
+ colIdx: number,
62
+ blockIdx: number,
63
+ direction: 'up' | 'down'
64
+ ) => void
65
+ addSection: (afterIdx?: number) => void
66
+ removeSection: (sectionIdx: number) => void
67
+ addRow: (sectionIdx: number, layout?: ColumnLayout) => void
68
+ changeRowLayout: (
69
+ sectionIdx: number,
70
+ rowIdx: number,
71
+ layout: ColumnLayout
72
+ ) => void
73
+ removeRow: (sectionIdx: number, rowIdx: number) => void
74
+ undo: () => void
75
+ redo: () => void
76
+ canUndo: boolean
77
+ canRedo: boolean
78
+ /** Low-level commit for consumers that need direct section mutation (e.g. drag-and-drop). */
79
+ commitChange: (newSections: Section[]) => void
80
+ }
81
+
82
+ export function useEmailEditorState({
83
+ initialSections,
84
+ onChange,
85
+ }: UseEmailEditorStateOptions): UseEmailEditorStateReturn {
86
+ const [selection, setSelection] = useState<EditorSelection | null>(null)
87
+ const historyRef = useRef<UndoRedoState>(createUndoRedoState(initialSections))
88
+
89
+ // Sync history when sections change externally
90
+ useEffect(() => {
91
+ historyRef.current = { ...historyRef.current, present: initialSections }
92
+ }, [initialSections])
93
+
94
+ const sections = initialSections
95
+
96
+ const commitChange = useCallback(
97
+ (newSections: Section[]) => {
98
+ historyRef.current = pushState(historyRef.current, newSections)
99
+ onChange?.(newSections)
100
+ },
101
+ [onChange]
102
+ )
103
+
104
+ const handleUndo = useCallback(() => {
105
+ const newState = undoState(historyRef.current)
106
+ historyRef.current = newState
107
+ onChange?.(newState.present)
108
+ }, [onChange])
109
+
110
+ const handleRedo = useCallback(() => {
111
+ const newState = redoState(historyRef.current)
112
+ historyRef.current = newState
113
+ onChange?.(newState.present)
114
+ }, [onChange])
115
+
116
+ // Keyboard shortcuts for undo/redo
117
+ useEffect(() => {
118
+ const handler = (e: KeyboardEvent) => {
119
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
120
+ e.preventDefault()
121
+ handleUndo()
122
+ }
123
+ if (
124
+ (e.metaKey || e.ctrlKey) &&
125
+ (e.key === 'y' || (e.key === 'z' && e.shiftKey))
126
+ ) {
127
+ e.preventDefault()
128
+ handleRedo()
129
+ }
130
+ }
131
+ window.addEventListener('keydown', handler)
132
+ return () => window.removeEventListener('keydown', handler)
133
+ }, [handleUndo, handleRedo])
134
+
135
+ // --- Mutation helpers ---
136
+
137
+ const updateBlock = useCallback(
138
+ (
139
+ sectionIdx: number,
140
+ rowIdx: number,
141
+ colIdx: number,
142
+ blockIdx: number,
143
+ updates: Partial<Block>
144
+ ) => {
145
+ const newSections = sections.map((section, si) => {
146
+ if (si !== sectionIdx) return section
147
+ return {
148
+ ...section,
149
+ rows: section.rows.map((row, ri) => {
150
+ if (ri !== rowIdx) return row
151
+ return {
152
+ ...row,
153
+ columns: row.columns.map((col, ci) => {
154
+ if (ci !== colIdx) return col
155
+ return col.map((block, bi) =>
156
+ bi === blockIdx ? ({ ...block, ...updates } as Block) : block
157
+ )
158
+ }),
159
+ }
160
+ }),
161
+ }
162
+ })
163
+ commitChange(newSections)
164
+ },
165
+ [sections, commitChange]
166
+ )
167
+
168
+ const addBlock = useCallback(
169
+ (
170
+ sectionIdx: number,
171
+ rowIdx: number,
172
+ colIdx: number,
173
+ blockType: BlockType,
174
+ afterBlockIdx?: number
175
+ ) => {
176
+ const newBlock = createBlock(blockType)
177
+ const newSections = sections.map((section, si) => {
178
+ if (si !== sectionIdx) return section
179
+ return {
180
+ ...section,
181
+ rows: section.rows.map((row, ri) => {
182
+ if (ri !== rowIdx) return row
183
+ return {
184
+ ...row,
185
+ columns: row.columns.map((col, ci) => {
186
+ if (ci !== colIdx) return col
187
+ const insertAt =
188
+ afterBlockIdx !== undefined ? afterBlockIdx + 1 : col.length
189
+ return [
190
+ ...col.slice(0, insertAt),
191
+ newBlock,
192
+ ...col.slice(insertAt),
193
+ ]
194
+ }),
195
+ }
196
+ }),
197
+ }
198
+ })
199
+ commitChange(newSections)
200
+ // Select the new block
201
+ const insertAt =
202
+ afterBlockIdx !== undefined
203
+ ? afterBlockIdx + 1
204
+ : sections[sectionIdx].rows[rowIdx].columns[colIdx].length
205
+ setSelection({
206
+ sectionIndex: sectionIdx,
207
+ rowIndex: rowIdx,
208
+ columnIndex: colIdx,
209
+ blockIndex: insertAt,
210
+ })
211
+ },
212
+ [sections, commitChange]
213
+ )
214
+
215
+ const removeBlock = useCallback(
216
+ (
217
+ sectionIdx: number,
218
+ rowIdx: number,
219
+ colIdx: number,
220
+ blockIdx: number
221
+ ) => {
222
+ const newSections = sections.map((section, si) => {
223
+ if (si !== sectionIdx) return section
224
+ return {
225
+ ...section,
226
+ rows: section.rows.map((row, ri) => {
227
+ if (ri !== rowIdx) return row
228
+ return {
229
+ ...row,
230
+ columns: row.columns.map((col, ci) => {
231
+ if (ci !== colIdx) return col
232
+ return col.filter((_, bi) => bi !== blockIdx)
233
+ }),
234
+ }
235
+ }),
236
+ }
237
+ })
238
+ commitChange(newSections)
239
+ setSelection(null)
240
+ },
241
+ [sections, commitChange]
242
+ )
243
+
244
+ const duplicateBlock = useCallback(
245
+ (
246
+ sectionIdx: number,
247
+ rowIdx: number,
248
+ colIdx: number,
249
+ blockIdx: number
250
+ ) => {
251
+ const block =
252
+ sections[sectionIdx].rows[rowIdx].columns[colIdx][blockIdx]
253
+ if (!block) return
254
+ const copy: Block = {
255
+ ...JSON.parse(JSON.stringify(block)),
256
+ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
257
+ }
258
+ const newSections = sections.map((section, si) => {
259
+ if (si !== sectionIdx) return section
260
+ return {
261
+ ...section,
262
+ rows: section.rows.map((row, ri) => {
263
+ if (ri !== rowIdx) return row
264
+ return {
265
+ ...row,
266
+ columns: row.columns.map((col, ci) => {
267
+ if (ci !== colIdx) return col
268
+ return [
269
+ ...col.slice(0, blockIdx + 1),
270
+ copy,
271
+ ...col.slice(blockIdx + 1),
272
+ ]
273
+ }),
274
+ }
275
+ }),
276
+ }
277
+ })
278
+ commitChange(newSections)
279
+ setSelection({
280
+ sectionIndex: sectionIdx,
281
+ rowIndex: rowIdx,
282
+ columnIndex: colIdx,
283
+ blockIndex: blockIdx + 1,
284
+ })
285
+ },
286
+ [sections, commitChange]
287
+ )
288
+
289
+ const moveBlock = useCallback(
290
+ (
291
+ sectionIdx: number,
292
+ rowIdx: number,
293
+ colIdx: number,
294
+ blockIdx: number,
295
+ direction: 'up' | 'down'
296
+ ) => {
297
+ const col = sections[sectionIdx].rows[rowIdx].columns[colIdx]
298
+ const newIndex = direction === 'up' ? blockIdx - 1 : blockIdx + 1
299
+ if (newIndex < 0 || newIndex >= col.length) return
300
+ const newCol = [...col]
301
+ const [moved] = newCol.splice(blockIdx, 1)
302
+ newCol.splice(newIndex, 0, moved)
303
+ const newSections = sections.map((section, si) => {
304
+ if (si !== sectionIdx) return section
305
+ return {
306
+ ...section,
307
+ rows: section.rows.map((row, ri) => {
308
+ if (ri !== rowIdx) return row
309
+ return {
310
+ ...row,
311
+ columns: row.columns.map((c, ci) =>
312
+ ci === colIdx ? newCol : c
313
+ ),
314
+ }
315
+ }),
316
+ }
317
+ })
318
+ commitChange(newSections)
319
+ setSelection({
320
+ sectionIndex: sectionIdx,
321
+ rowIndex: rowIdx,
322
+ columnIndex: colIdx,
323
+ blockIndex: newIndex,
324
+ })
325
+ },
326
+ [sections, commitChange]
327
+ )
328
+
329
+ // --- Section/Row operations ---
330
+
331
+ const addSection = useCallback(
332
+ (afterIdx?: number) => {
333
+ const newSection = createSection()
334
+ const insertAfter = afterIdx ?? sections.length - 1
335
+ const newSections = [
336
+ ...sections.slice(0, insertAfter + 1),
337
+ newSection,
338
+ ...sections.slice(insertAfter + 1),
339
+ ]
340
+ commitChange(newSections)
341
+ },
342
+ [sections, commitChange]
343
+ )
344
+
345
+ const removeSection = useCallback(
346
+ (index: number) => {
347
+ if (sections.length <= 1) return
348
+ commitChange(sections.filter((_, i) => i !== index))
349
+ setSelection(null)
350
+ },
351
+ [sections, commitChange]
352
+ )
353
+
354
+ const addRow = useCallback(
355
+ (sectionIdx: number, layout: ColumnLayout = '1') => {
356
+ const newRow = createRow(layout)
357
+ const newSections = sections.map((section, si) => {
358
+ if (si !== sectionIdx) return section
359
+ return { ...section, rows: [...section.rows, newRow] }
360
+ })
361
+ commitChange(newSections)
362
+ },
363
+ [sections, commitChange]
364
+ )
365
+
366
+ const changeRowLayout = useCallback(
367
+ (sectionIdx: number, rowIdx: number, layout: ColumnLayout) => {
368
+ const newColCount = getColumnCount(layout)
369
+ const newSections = sections.map((section, si) => {
370
+ if (si !== sectionIdx) return section
371
+ return {
372
+ ...section,
373
+ rows: section.rows.map((row, ri) => {
374
+ if (ri !== rowIdx) return row
375
+ const columns = [...row.columns]
376
+ // Add or trim columns as needed
377
+ while (columns.length < newColCount) columns.push([])
378
+ // If reducing columns, merge extra columns into last
379
+ if (columns.length > newColCount) {
380
+ const extra = columns.splice(newColCount - 1)
381
+ columns[newColCount - 1] = extra.flat()
382
+ }
383
+ return { ...row, layout, columns }
384
+ }),
385
+ }
386
+ })
387
+ commitChange(newSections)
388
+ },
389
+ [sections, commitChange]
390
+ )
391
+
392
+ const removeRow = useCallback(
393
+ (sectionIdx: number, rowIdx: number) => {
394
+ const section = sections[sectionIdx]
395
+ if (section.rows.length <= 1) return
396
+ const newSections = sections.map((s, si) => {
397
+ if (si !== sectionIdx) return s
398
+ return { ...s, rows: s.rows.filter((_, ri) => ri !== rowIdx) }
399
+ })
400
+ commitChange(newSections)
401
+ setSelection(null)
402
+ },
403
+ [sections, commitChange]
404
+ )
405
+
406
+ return {
407
+ sections,
408
+ selection,
409
+ setSelection,
410
+ updateBlock,
411
+ addBlock,
412
+ removeBlock,
413
+ duplicateBlock,
414
+ moveBlock,
415
+ addSection,
416
+ removeSection,
417
+ addRow,
418
+ changeRowLayout,
419
+ removeRow,
420
+ undo: handleUndo,
421
+ redo: handleRedo,
422
+ canUndo: canUndoFn(historyRef.current),
423
+ canRedo: canRedoFn(historyRef.current),
424
+ commitChange,
425
+ }
426
+ }
@@ -1,5 +1,6 @@
1
1
  // Email Editor - barrel export
2
2
  export { EmailEditor } from './email-editor'
3
+ export { BlockRenderer } from './BlockRenderer'
3
4
 
4
5
  // Types
5
6
  export type {