@startsimpli/ui 0.4.7 → 0.4.9

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 (62) hide show
  1. package/package.json +21 -23
  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/safe-html.tsx +9 -8
  61. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  62. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -1,44 +1,23 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useState, useCallback, useRef, useEffect } from 'react'
4
+ import { useState, useCallback } from 'react'
5
5
  import {
6
6
  Section,
7
7
  Block,
8
- BlockType,
9
8
  ColumnLayout,
10
9
  GlobalStyles,
11
10
  MergeFieldDefinition,
11
+ EditorSelection,
12
12
  DEFAULT_GLOBAL_STYLES,
13
- createBlock,
14
- createRow,
15
- createSection,
16
- getColumnCount,
17
13
  getColumnWidths,
18
14
  } from './types'
19
- import {
20
- TextBlockEditor,
21
- MetricsBlockEditor,
22
- DividerBlockEditor,
23
- ButtonBlockEditor,
24
- ImageBlockEditor,
25
- SpacerBlockEditor,
26
- SocialBlockEditor,
27
- HeaderBlockEditor,
28
- FooterBlockEditor,
29
- } from './blocks'
15
+ import { useDragDrop } from './hooks/useDragDrop'
16
+ import { useEmailEditorState } from './hooks/useEmailEditorState'
17
+ import { BlockRenderer } from './BlockRenderer'
30
18
  import { BlockToolbar } from './block-toolbar'
31
19
  import { AddBlockMenu } from './add-block-menu'
32
20
  import { EditorSidebar } from './editor-sidebar'
33
- import {
34
- UndoRedoState,
35
- createUndoRedoState,
36
- pushState,
37
- undo as undoState,
38
- redo as redoState,
39
- canUndo,
40
- canRedo,
41
- } from './utils/undo-redo'
42
21
  import { Button } from '../ui/button'
43
22
  import {
44
23
  Select,
@@ -59,14 +38,6 @@ import {
59
38
  } from 'lucide-react'
60
39
  import { cn } from '../../lib/utils'
61
40
 
62
- // --- Types ---
63
- interface EditorSelection {
64
- sectionIndex: number
65
- rowIndex: number
66
- columnIndex: number
67
- blockIndex: number
68
- }
69
-
70
41
  interface EmailEditorProps {
71
42
  sections: Section[]
72
43
  onChange: (sections: Section[]) => void
@@ -85,7 +56,7 @@ interface EmailEditorProps {
85
56
 
86
57
  // --- Main Editor ---
87
58
  export function EmailEditor({
88
- sections,
59
+ sections: sectionsProp,
89
60
  onChange,
90
61
  globalStyles = DEFAULT_GLOBAL_STYLES,
91
62
  onGlobalStylesChange,
@@ -93,64 +64,38 @@ export function EmailEditor({
93
64
  readOnly = false,
94
65
  renderTextEditor,
95
66
  }: EmailEditorProps) {
96
- const [selection, setSelection] = useState<EditorSelection | null>(null)
97
- const [showSidebar, setShowSidebar] = useState(false)
98
- const [dragState, setDragState] = useState<{
99
- sourceSection: number
100
- sourceRow: number
101
- sourceCol: number
102
- sourceBlock: number
103
- } | null>(null)
104
- const [dragOverTarget, setDragOverTarget] = useState<{
105
- sectionIndex: number
106
- rowIndex: number
107
- colIndex: number
108
- blockIndex: number
109
- } | null>(null)
110
-
111
- const historyRef = useRef<UndoRedoState>(createUndoRedoState(sections))
112
-
113
- // Sync history when sections change externally
114
- useEffect(() => {
115
- historyRef.current = { ...historyRef.current, present: sections }
116
- }, [sections])
117
-
118
- const commitChange = useCallback(
119
- (newSections: Section[]) => {
120
- historyRef.current = pushState(historyRef.current, newSections)
121
- onChange(newSections)
122
- },
123
- [onChange]
124
- )
67
+ const {
68
+ sections,
69
+ selection,
70
+ setSelection,
71
+ updateBlock,
72
+ addBlock,
73
+ removeBlock,
74
+ duplicateBlock,
75
+ moveBlock,
76
+ addSection,
77
+ removeSection,
78
+ addRow,
79
+ changeRowLayout,
80
+ removeRow,
81
+ undo: handleUndo,
82
+ redo: handleRedo,
83
+ canUndo: canUndoVal,
84
+ canRedo: canRedoVal,
85
+ commitChange,
86
+ } = useEmailEditorState({ initialSections: sectionsProp, onChange })
125
87
 
126
- const handleUndo = useCallback(() => {
127
- const newState = undoState(historyRef.current)
128
- historyRef.current = newState
129
- onChange(newState.present)
130
- }, [onChange])
131
-
132
- const handleRedo = useCallback(() => {
133
- const newState = redoState(historyRef.current)
134
- historyRef.current = newState
135
- onChange(newState.present)
136
- }, [onChange])
88
+ const [showSidebar, setShowSidebar] = useState(false)
137
89
 
138
- // Keyboard shortcuts for undo/redo
139
- useEffect(() => {
140
- if (readOnly) return
141
- const handler = (e: KeyboardEvent) => {
142
- if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
143
- e.preventDefault()
144
- handleUndo()
145
- }
146
- if ((e.metaKey || e.ctrlKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
147
- e.preventDefault()
148
- handleRedo()
149
- }
150
- }
151
- window.addEventListener('keydown', handler)
152
- return () => window.removeEventListener('keydown', handler)
153
- }, [readOnly, handleUndo, handleRedo])
90
+ const {
91
+ dragState,
92
+ dragOverTarget,
93
+ setDragOverTarget,
94
+ handleDragStart,
95
+ handleDragOver,
96
+ handleDrop,
97
+ handleDragEnd,
98
+ } = useDragDrop({ sections, commitChange, setSelection })
154
99
 
155
100
  // Get selected block
156
101
  const selectedBlock = selection
@@ -159,371 +104,18 @@ export function EmailEditor({
159
104
  ] ?? null
160
105
  : null
161
106
 
162
- // --- Mutation helpers ---
163
-
164
- const updateBlock = useCallback(
165
- (sel: EditorSelection, updatedBlock: Block) => {
166
- const newSections = sections.map((section, si) => {
167
- if (si !== sel.sectionIndex) return section
168
- return {
169
- ...section,
170
- rows: section.rows.map((row, ri) => {
171
- if (ri !== sel.rowIndex) return row
172
- return {
173
- ...row,
174
- columns: row.columns.map((col, ci) => {
175
- if (ci !== sel.columnIndex) return col
176
- return col.map((block, bi) => (bi === sel.blockIndex ? updatedBlock : block))
177
- }),
178
- }
179
- }),
180
- }
181
- })
182
- commitChange(newSections)
183
- },
184
- [sections, commitChange]
185
- )
186
-
187
- const addBlockToColumn = useCallback(
188
- (
189
- sectionIndex: number,
190
- rowIndex: number,
191
- colIndex: number,
192
- blockType: BlockType,
193
- afterBlockIndex?: number
194
- ) => {
195
- const newBlock = createBlock(blockType)
196
- const newSections = sections.map((section, si) => {
197
- if (si !== sectionIndex) return section
198
- return {
199
- ...section,
200
- rows: section.rows.map((row, ri) => {
201
- if (ri !== rowIndex) return row
202
- return {
203
- ...row,
204
- columns: row.columns.map((col, ci) => {
205
- if (ci !== colIndex) return col
206
- const insertAt = afterBlockIndex !== undefined ? afterBlockIndex + 1 : col.length
207
- return [...col.slice(0, insertAt), newBlock, ...col.slice(insertAt)]
208
- }),
209
- }
210
- }),
211
- }
212
- })
213
- commitChange(newSections)
214
- // Select the new block
215
- const insertAt = afterBlockIndex !== undefined ? afterBlockIndex + 1 : sections[sectionIndex].rows[rowIndex].columns[colIndex].length
216
- setSelection({
217
- sectionIndex,
218
- rowIndex,
219
- columnIndex: colIndex,
220
- blockIndex: insertAt,
221
- })
107
+ // --- Block update handler ---
108
+ const handleBlockUpdate = useCallback(
109
+ (sel: EditorSelection, updates: Partial<Block>) => {
110
+ const current =
111
+ sections[sel.sectionIndex]?.rows[sel.rowIndex]?.columns[sel.columnIndex]?.[sel.blockIndex]
112
+ if (!current) return
113
+ updateBlock(sel.sectionIndex, sel.rowIndex, sel.columnIndex, sel.blockIndex, {
114
+ ...current,
115
+ ...updates,
116
+ } as Block)
222
117
  },
223
- [sections, commitChange]
224
- )
225
-
226
- const removeBlock = useCallback(
227
- (sel: EditorSelection) => {
228
- const newSections = sections.map((section, si) => {
229
- if (si !== sel.sectionIndex) return section
230
- return {
231
- ...section,
232
- rows: section.rows.map((row, ri) => {
233
- if (ri !== sel.rowIndex) return row
234
- return {
235
- ...row,
236
- columns: row.columns.map((col, ci) => {
237
- if (ci !== sel.columnIndex) return col
238
- return col.filter((_, bi) => bi !== sel.blockIndex)
239
- }),
240
- }
241
- }),
242
- }
243
- })
244
- commitChange(newSections)
245
- setSelection(null)
246
- },
247
- [sections, commitChange]
248
- )
249
-
250
- const duplicateBlock = useCallback(
251
- (sel: EditorSelection) => {
252
- const block = sections[sel.sectionIndex].rows[sel.rowIndex].columns[sel.columnIndex][sel.blockIndex]
253
- if (!block) return
254
- const copy: Block = { ...JSON.parse(JSON.stringify(block)), id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 6)}` }
255
- const newSections = sections.map((section, si) => {
256
- if (si !== sel.sectionIndex) return section
257
- return {
258
- ...section,
259
- rows: section.rows.map((row, ri) => {
260
- if (ri !== sel.rowIndex) return row
261
- return {
262
- ...row,
263
- columns: row.columns.map((col, ci) => {
264
- if (ci !== sel.columnIndex) return col
265
- return [...col.slice(0, sel.blockIndex + 1), copy, ...col.slice(sel.blockIndex + 1)]
266
- }),
267
- }
268
- }),
269
- }
270
- })
271
- commitChange(newSections)
272
- setSelection({ ...sel, blockIndex: sel.blockIndex + 1 })
273
- },
274
- [sections, commitChange]
275
- )
276
-
277
- const moveBlockInColumn = useCallback(
278
- (sel: EditorSelection, direction: 'up' | 'down') => {
279
- const col = sections[sel.sectionIndex].rows[sel.rowIndex].columns[sel.columnIndex]
280
- const newIndex = direction === 'up' ? sel.blockIndex - 1 : sel.blockIndex + 1
281
- if (newIndex < 0 || newIndex >= col.length) return
282
- const newCol = [...col]
283
- const [moved] = newCol.splice(sel.blockIndex, 1)
284
- newCol.splice(newIndex, 0, moved)
285
- const newSections = sections.map((section, si) => {
286
- if (si !== sel.sectionIndex) return section
287
- return {
288
- ...section,
289
- rows: section.rows.map((row, ri) => {
290
- if (ri !== sel.rowIndex) return row
291
- return {
292
- ...row,
293
- columns: row.columns.map((col, ci) =>
294
- ci === sel.columnIndex ? newCol : col
295
- ),
296
- }
297
- }),
298
- }
299
- })
300
- commitChange(newSections)
301
- setSelection({ ...sel, blockIndex: newIndex })
302
- },
303
- [sections, commitChange]
304
- )
305
-
306
- // --- Section/Row operations ---
307
- const addSection = useCallback(
308
- (afterIndex: number) => {
309
- const newSection = createSection()
310
- const newSections = [
311
- ...sections.slice(0, afterIndex + 1),
312
- newSection,
313
- ...sections.slice(afterIndex + 1),
314
- ]
315
- commitChange(newSections)
316
- },
317
- [sections, commitChange]
318
- )
319
-
320
- const removeSection = useCallback(
321
- (index: number) => {
322
- if (sections.length <= 1) return
323
- commitChange(sections.filter((_, i) => i !== index))
324
- setSelection(null)
325
- },
326
- [sections, commitChange]
327
- )
328
-
329
- const addRow = useCallback(
330
- (sectionIndex: number, layout: ColumnLayout = '1') => {
331
- const newRow = createRow(layout)
332
- const newSections = sections.map((section, si) => {
333
- if (si !== sectionIndex) return section
334
- return { ...section, rows: [...section.rows, newRow] }
335
- })
336
- commitChange(newSections)
337
- },
338
- [sections, commitChange]
339
- )
340
-
341
- const changeRowLayout = useCallback(
342
- (sectionIndex: number, rowIndex: number, layout: ColumnLayout) => {
343
- const newColCount = getColumnCount(layout)
344
- const newSections = sections.map((section, si) => {
345
- if (si !== sectionIndex) return section
346
- return {
347
- ...section,
348
- rows: section.rows.map((row, ri) => {
349
- if (ri !== rowIndex) return row
350
- const columns = [...row.columns]
351
- // Add or trim columns as needed
352
- while (columns.length < newColCount) columns.push([])
353
- // If reducing columns, merge extra columns into last
354
- if (columns.length > newColCount) {
355
- const extra = columns.splice(newColCount - 1)
356
- columns[newColCount - 1] = extra.flat()
357
- }
358
- return { ...row, layout, columns }
359
- }),
360
- }
361
- })
362
- commitChange(newSections)
363
- },
364
- [sections, commitChange]
365
- )
366
-
367
- const removeRow = useCallback(
368
- (sectionIndex: number, rowIndex: number) => {
369
- const section = sections[sectionIndex]
370
- if (section.rows.length <= 1) return
371
- const newSections = sections.map((s, si) => {
372
- if (si !== sectionIndex) return s
373
- return { ...s, rows: s.rows.filter((_, ri) => ri !== rowIndex) }
374
- })
375
- commitChange(newSections)
376
- setSelection(null)
377
- },
378
- [sections, commitChange]
379
- )
380
-
381
- // --- DnD handlers ---
382
- const handleDragStart = useCallback(
383
- (sectionIndex: number, rowIndex: number, colIndex: number, blockIndex: number) => {
384
- setDragState({
385
- sourceSection: sectionIndex,
386
- sourceRow: rowIndex,
387
- sourceCol: colIndex,
388
- sourceBlock: blockIndex,
389
- })
390
- },
391
- []
392
- )
393
-
394
- const handleDragOver = useCallback(
395
- (
396
- e: React.DragEvent,
397
- sectionIndex: number,
398
- rowIndex: number,
399
- colIndex: number,
400
- blockIndex: number
401
- ) => {
402
- e.preventDefault()
403
- setDragOverTarget({ sectionIndex, rowIndex, colIndex, blockIndex })
404
- },
405
- []
406
- )
407
-
408
- const handleDrop = useCallback(
409
- (
410
- e: React.DragEvent,
411
- targetSection: number,
412
- targetRow: number,
413
- targetCol: number,
414
- targetBlock: number
415
- ) => {
416
- e.preventDefault()
417
- if (!dragState) return
418
-
419
- const { sourceSection, sourceRow, sourceCol, sourceBlock } = dragState
420
-
421
- // Get the block being moved
422
- const block =
423
- sections[sourceSection]?.rows[sourceRow]?.columns[sourceCol]?.[sourceBlock]
424
- if (!block) return
425
-
426
- // Remove from source
427
- let newSections = sections.map((section, si) => {
428
- if (si !== sourceSection) return section
429
- return {
430
- ...section,
431
- rows: section.rows.map((row, ri) => {
432
- if (ri !== sourceRow) return row
433
- return {
434
- ...row,
435
- columns: row.columns.map((col, ci) => {
436
- if (ci !== sourceCol) return col
437
- return col.filter((_, bi) => bi !== sourceBlock)
438
- }),
439
- }
440
- }),
441
- }
442
- })
443
-
444
- // Adjust target index if removing from same column before target
445
- let adjustedTargetBlock = targetBlock
446
- if (
447
- sourceSection === targetSection &&
448
- sourceRow === targetRow &&
449
- sourceCol === targetCol &&
450
- sourceBlock < targetBlock
451
- ) {
452
- adjustedTargetBlock -= 1
453
- }
454
-
455
- // Insert at target
456
- newSections = newSections.map((section, si) => {
457
- if (si !== targetSection) return section
458
- return {
459
- ...section,
460
- rows: section.rows.map((row, ri) => {
461
- if (ri !== targetRow) return row
462
- return {
463
- ...row,
464
- columns: row.columns.map((col, ci) => {
465
- if (ci !== targetCol) return col
466
- return [
467
- ...col.slice(0, adjustedTargetBlock),
468
- block,
469
- ...col.slice(adjustedTargetBlock),
470
- ]
471
- }),
472
- }
473
- }),
474
- }
475
- })
476
-
477
- commitChange(newSections)
478
- setDragState(null)
479
- setDragOverTarget(null)
480
- setSelection({
481
- sectionIndex: targetSection,
482
- rowIndex: targetRow,
483
- columnIndex: targetCol,
484
- blockIndex: adjustedTargetBlock,
485
- })
486
- },
487
- [dragState, sections, commitChange]
488
- )
489
-
490
- const handleDragEnd = useCallback(() => {
491
- setDragState(null)
492
- setDragOverTarget(null)
493
- }, [])
494
-
495
- // --- Render block by type ---
496
- const renderBlock = useCallback(
497
- (block: Block, sel: EditorSelection) => {
498
- const commonProps = {
499
- isEditing: !readOnly,
500
- onChange: (updated: Block) => updateBlock(sel, updated),
501
- }
502
-
503
- switch (block.type) {
504
- case 'text':
505
- return <TextBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} renderEditor={renderTextEditor} />
506
- case 'metrics':
507
- return <MetricsBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
508
- case 'divider':
509
- return <DividerBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
510
- case 'cta':
511
- return <ButtonBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
512
- case 'image':
513
- return <ImageBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
514
- case 'spacer':
515
- return <SpacerBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
516
- case 'social':
517
- return <SocialBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
518
- case 'header':
519
- return <HeaderBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
520
- case 'footer':
521
- return <FooterBlockEditor block={block} {...commonProps} onChange={(b) => updateBlock(sel, b)} />
522
- default:
523
- return null
524
- }
525
- },
526
- [readOnly, updateBlock, renderTextEditor]
118
+ [sections, updateBlock]
527
119
  )
528
120
 
529
121
  // --- Read-only mode ---
@@ -558,7 +150,18 @@ export function EmailEditor({
558
150
  <div key={ci} style={{ width: `${widths[ci]}%` }}>
559
151
  {col.map((block, bi) => (
560
152
  <div key={block.id}>
561
- {renderBlock(block, { sectionIndex: si, rowIndex: ri, columnIndex: ci, blockIndex: bi })}
153
+ <BlockRenderer
154
+ block={block}
155
+ editing={!readOnly}
156
+ onChange={(updates) =>
157
+ handleBlockUpdate(
158
+ { sectionIndex: si, rowIndex: ri, columnIndex: ci, blockIndex: bi },
159
+ updates
160
+ )
161
+ }
162
+ mergeFields={mergeFields ?? []}
163
+ renderTextEditor={renderTextEditor}
164
+ />
562
165
  </div>
563
166
  ))}
564
167
  </div>
@@ -584,7 +187,7 @@ export function EmailEditor({
584
187
  size="icon"
585
188
  className="h-8 w-8"
586
189
  onClick={handleUndo}
587
- disabled={!canUndo(historyRef.current)}
190
+ disabled={!canUndoVal}
588
191
  title="Undo (Cmd+Z)"
589
192
  >
590
193
  <Undo className="h-4 w-4" />
@@ -594,7 +197,7 @@ export function EmailEditor({
594
197
  size="icon"
595
198
  className="h-8 w-8"
596
199
  onClick={handleRedo}
597
- disabled={!canRedo(historyRef.current)}
200
+ disabled={!canRedoVal}
598
201
  title="Redo (Cmd+Shift+Z)"
599
202
  >
600
203
  <Redo className="h-4 w-4" />
@@ -714,7 +317,7 @@ export function EmailEditor({
714
317
  {col.length === 0 && (
715
318
  <div className="flex items-center justify-center h-full p-2">
716
319
  <AddBlockMenu
717
- onAdd={(type) => addBlockToColumn(si, ri, ci, type)}
320
+ onAdd={(type) => addBlock(si, ri, ci, type)}
718
321
  variant="inline"
719
322
  />
720
323
  </div>
@@ -771,13 +374,13 @@ export function EmailEditor({
771
374
  {/* Block toolbar (on hover when selected) */}
772
375
  {isSelected && (
773
376
  <BlockToolbar
774
- onDelete={() => removeBlock(blockSel)}
775
- onDuplicate={() => duplicateBlock(blockSel)}
377
+ onDelete={() => removeBlock(si, ri, ci, bi)}
378
+ onDuplicate={() => duplicateBlock(si, ri, ci, bi)}
776
379
  onMoveUp={() =>
777
- moveBlockInColumn(blockSel, 'up')
380
+ moveBlock(si, ri, ci, bi, 'up')
778
381
  }
779
382
  onMoveDown={() =>
780
- moveBlockInColumn(blockSel, 'down')
383
+ moveBlock(si, ri, ci, bi, 'down')
781
384
  }
782
385
  canMoveUp={bi > 0}
783
386
  canMoveDown={bi < col.length - 1}
@@ -786,7 +389,15 @@ export function EmailEditor({
786
389
 
787
390
  {/* Block content */}
788
391
  <div className="p-1">
789
- {renderBlock(block, blockSel)}
392
+ <BlockRenderer
393
+ block={block}
394
+ editing={!readOnly}
395
+ onChange={(updates) =>
396
+ handleBlockUpdate(blockSel, updates)
397
+ }
398
+ mergeFields={mergeFields ?? []}
399
+ renderTextEditor={renderTextEditor}
400
+ />
790
401
  </div>
791
402
  </div>
792
403
 
@@ -795,7 +406,7 @@ export function EmailEditor({
795
406
  <div className="flex justify-center py-1 opacity-0 group-hover:opacity-100 hover:!opacity-100 transition-opacity">
796
407
  <AddBlockMenu
797
408
  onAdd={(type) =>
798
- addBlockToColumn(si, ri, ci, type, bi)
409
+ addBlock(si, ri, ci, type, bi)
799
410
  }
800
411
  variant="small"
801
412
  />
@@ -810,7 +421,7 @@ export function EmailEditor({
810
421
  <div className="flex justify-center py-1 opacity-0 hover:opacity-100 transition-opacity">
811
422
  <AddBlockMenu
812
423
  onAdd={(type) =>
813
- addBlockToColumn(
424
+ addBlock(
814
425
  si,
815
426
  ri,
816
427
  ci,
@@ -874,7 +485,7 @@ export function EmailEditor({
874
485
  <EditorSidebar
875
486
  selectedBlock={selectedBlock}
876
487
  onBlockChange={(block) => {
877
- if (selection) updateBlock(selection, block)
488
+ if (selection) updateBlock(selection.sectionIndex, selection.rowIndex, selection.columnIndex, selection.blockIndex, block)
878
489
  }}
879
490
  globalStyles={globalStyles}
880
491
  onGlobalStylesChange={onGlobalStylesChange || (() => {})}