@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.
- package/package.json +1 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +183 -200
- 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/index.ts +6 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -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 +6 -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-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/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/editor-sidebar.tsx +6 -731
- package/src/components/email-editor/email-editor.tsx +78 -467
- 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 +1 -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/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +134 -881
- 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/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -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/__tests__/pipeline.test.tsx +169 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- 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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
// ---
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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,
|
|
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
|
-
|
|
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={!
|
|
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={!
|
|
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) =>
|
|
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(
|
|
775
|
-
onDuplicate={() => duplicateBlock(
|
|
377
|
+
onDelete={() => removeBlock(si, ri, ci, bi)}
|
|
378
|
+
onDuplicate={() => duplicateBlock(si, ri, ci, bi)}
|
|
776
379
|
onMoveUp={() =>
|
|
777
|
-
|
|
380
|
+
moveBlock(si, ri, ci, bi, 'up')
|
|
778
381
|
}
|
|
779
382
|
onMoveDown={() =>
|
|
780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || (() => {})}
|