@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,497 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState, useCallback } from 'react'
5
+ import {
6
+ Section,
7
+ Block,
8
+ ColumnLayout,
9
+ GlobalStyles,
10
+ MergeFieldDefinition,
11
+ EditorSelection,
12
+ DEFAULT_GLOBAL_STYLES,
13
+ getColumnWidths,
14
+ } from './types'
15
+ import { useDragDrop } from './hooks/useDragDrop'
16
+ import { useEmailEditorState } from './hooks/useEmailEditorState'
17
+ import { BlockRenderer } from './BlockRenderer'
18
+ import { BlockToolbar } from './block-toolbar'
19
+ import { AddBlockMenu } from './add-block-menu'
20
+ import { EditorSidebar } from './editor-sidebar'
21
+ import { Button } from '../ui/button'
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '../ui/select'
29
+ import {
30
+ Undo,
31
+ Redo,
32
+ Plus,
33
+ Settings,
34
+ Columns2,
35
+ Columns3,
36
+ RectangleHorizontal,
37
+ Trash2,
38
+ } from 'lucide-react'
39
+ import { cn } from '../../lib/utils'
40
+
41
+ interface EmailEditorProps {
42
+ sections: Section[]
43
+ onChange: (sections: Section[]) => void
44
+ globalStyles?: GlobalStyles
45
+ onGlobalStylesChange?: (styles: GlobalStyles) => void
46
+ mergeFields?: MergeFieldDefinition[]
47
+ readOnly?: boolean
48
+ /** Optional custom rich text editor for the text block. */
49
+ renderTextEditor?: (props: {
50
+ content: string
51
+ onChange: (html: string) => void
52
+ placeholder?: string
53
+ className?: string
54
+ }) => React.ReactNode
55
+ }
56
+
57
+ // --- Main Editor ---
58
+ export function EmailEditor({
59
+ sections: sectionsProp,
60
+ onChange,
61
+ globalStyles = DEFAULT_GLOBAL_STYLES,
62
+ onGlobalStylesChange,
63
+ mergeFields,
64
+ readOnly = false,
65
+ renderTextEditor,
66
+ }: EmailEditorProps) {
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 })
87
+
88
+ const [showSidebar, setShowSidebar] = useState(false)
89
+
90
+ const {
91
+ dragState,
92
+ dragOverTarget,
93
+ setDragOverTarget,
94
+ handleDragStart,
95
+ handleDragOver,
96
+ handleDrop,
97
+ handleDragEnd,
98
+ } = useDragDrop({ sections, commitChange, setSelection })
99
+
100
+ // Get selected block
101
+ const selectedBlock = selection
102
+ ? sections[selection.sectionIndex]?.rows[selection.rowIndex]?.columns[selection.columnIndex]?.[
103
+ selection.blockIndex
104
+ ] ?? null
105
+ : null
106
+
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)
117
+ },
118
+ [sections, updateBlock]
119
+ )
120
+
121
+ // --- Read-only mode ---
122
+ if (readOnly) {
123
+ return (
124
+ <div
125
+ className="mx-auto"
126
+ style={{
127
+ maxWidth: globalStyles.contentWidth,
128
+ backgroundColor: globalStyles.backgroundColor,
129
+ fontFamily: globalStyles.fontFamily,
130
+ }}
131
+ >
132
+ {sections.map((section, si) => (
133
+ <div
134
+ key={section.id}
135
+ style={{
136
+ backgroundColor: section.backgroundColor || '#ffffff',
137
+ paddingTop: section.paddingTop,
138
+ paddingBottom: section.paddingBottom,
139
+ paddingLeft: section.paddingLeft,
140
+ paddingRight: section.paddingRight,
141
+ borderRadius: 8,
142
+ marginBottom: 4,
143
+ }}
144
+ >
145
+ {section.rows.map((row, ri) => {
146
+ const widths = getColumnWidths(row.layout)
147
+ return (
148
+ <div key={row.id} className="flex gap-2">
149
+ {row.columns.map((col, ci) => (
150
+ <div key={ci} style={{ width: `${widths[ci]}%` }}>
151
+ {col.map((block, bi) => (
152
+ <div key={block.id}>
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
+ />
165
+ </div>
166
+ ))}
167
+ </div>
168
+ ))}
169
+ </div>
170
+ )
171
+ })}
172
+ </div>
173
+ ))}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ // --- Editor mode ---
179
+ return (
180
+ <div className="flex h-full">
181
+ {/* Main canvas */}
182
+ <div className="flex-1 overflow-y-auto">
183
+ {/* Toolbar */}
184
+ <div className="sticky top-0 z-30 bg-background border-b px-4 py-2 flex items-center gap-2">
185
+ <Button
186
+ variant="ghost"
187
+ size="icon"
188
+ className="h-8 w-8"
189
+ onClick={handleUndo}
190
+ disabled={!canUndoVal}
191
+ title="Undo (Cmd+Z)"
192
+ >
193
+ <Undo className="h-4 w-4" />
194
+ </Button>
195
+ <Button
196
+ variant="ghost"
197
+ size="icon"
198
+ className="h-8 w-8"
199
+ onClick={handleRedo}
200
+ disabled={!canRedoVal}
201
+ title="Redo (Cmd+Shift+Z)"
202
+ >
203
+ <Redo className="h-4 w-4" />
204
+ </Button>
205
+ <div className="w-px h-6 bg-border mx-1" />
206
+ <Button
207
+ variant={showSidebar ? 'secondary' : 'ghost'}
208
+ size="sm"
209
+ className="h-8 gap-1 text-xs"
210
+ onClick={() => setShowSidebar(!showSidebar)}
211
+ >
212
+ <Settings className="h-3.5 w-3.5" />
213
+ Settings
214
+ </Button>
215
+ </div>
216
+
217
+ {/* Canvas area */}
218
+ <div
219
+ className="p-6"
220
+ style={{ backgroundColor: globalStyles.backgroundColor }}
221
+ onClick={() => setSelection(null)}
222
+ >
223
+ <div
224
+ className="mx-auto"
225
+ style={{ maxWidth: globalStyles.contentWidth }}
226
+ >
227
+ {sections.map((section, si) => (
228
+ <div key={section.id} className="mb-2">
229
+ {/* Section wrapper */}
230
+ <div
231
+ className="relative group/section rounded-lg"
232
+ style={{
233
+ backgroundColor: section.backgroundColor || '#ffffff',
234
+ paddingTop: section.paddingTop,
235
+ paddingBottom: section.paddingBottom,
236
+ paddingLeft: section.paddingLeft,
237
+ paddingRight: section.paddingRight,
238
+ }}
239
+ >
240
+ {/* Section controls */}
241
+ <div className="absolute -right-10 top-2 flex flex-col gap-1 opacity-0 group-hover/section:opacity-100 transition-opacity z-20">
242
+ {sections.length > 1 && (
243
+ <Button
244
+ variant="ghost"
245
+ size="icon"
246
+ className="h-6 w-6 text-destructive hover:text-destructive"
247
+ onClick={(e) => {
248
+ e.stopPropagation()
249
+ removeSection(si)
250
+ }}
251
+ title="Remove section"
252
+ >
253
+ <Trash2 className="h-3 w-3" />
254
+ </Button>
255
+ )}
256
+ </div>
257
+
258
+ {/* Rows */}
259
+ {section.rows.map((row, ri) => {
260
+ const widths = getColumnWidths(row.layout)
261
+ return (
262
+ <div key={row.id} className="relative group/row mb-1">
263
+ {/* Row layout controls */}
264
+ <div className="absolute -right-10 top-0 opacity-0 group-hover/row:opacity-100 transition-opacity z-10">
265
+ <Select
266
+ value={row.layout}
267
+ onValueChange={(v) =>
268
+ changeRowLayout(si, ri, v as ColumnLayout)
269
+ }
270
+ >
271
+ <SelectTrigger className="h-6 w-6 p-0 border-0 bg-transparent [&>svg]:hidden">
272
+ {row.layout === '1' ? (
273
+ <RectangleHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
274
+ ) : row.layout === '3' ? (
275
+ <Columns3 className="h-3.5 w-3.5 text-muted-foreground" />
276
+ ) : (
277
+ <Columns2 className="h-3.5 w-3.5 text-muted-foreground" />
278
+ )}
279
+ </SelectTrigger>
280
+ <SelectContent>
281
+ <SelectItem value="1">1 Column</SelectItem>
282
+ <SelectItem value="2">2 Equal</SelectItem>
283
+ <SelectItem value="3">3 Equal</SelectItem>
284
+ <SelectItem value="2-1">2/3 + 1/3</SelectItem>
285
+ <SelectItem value="1-2">1/3 + 2/3</SelectItem>
286
+ </SelectContent>
287
+ </Select>
288
+ </div>
289
+
290
+ {/* Columns */}
291
+ <div className="flex gap-2">
292
+ {row.columns.map((col, ci) => (
293
+ <div
294
+ key={ci}
295
+ style={{ width: `${widths[ci]}%` }}
296
+ className={cn(
297
+ 'min-h-[40px] rounded transition-colors',
298
+ col.length === 0 && 'border-2 border-dashed border-gray-200'
299
+ )}
300
+ onDragOver={(e) => {
301
+ e.preventDefault()
302
+ if (col.length === 0) {
303
+ setDragOverTarget({
304
+ sectionIndex: si,
305
+ rowIndex: ri,
306
+ colIndex: ci,
307
+ blockIndex: 0,
308
+ })
309
+ }
310
+ }}
311
+ onDrop={(e) => {
312
+ if (col.length === 0) {
313
+ handleDrop(e, si, ri, ci, 0)
314
+ }
315
+ }}
316
+ >
317
+ {col.length === 0 && (
318
+ <div className="flex items-center justify-center h-full p-2">
319
+ <AddBlockMenu
320
+ onAdd={(type) => addBlock(si, ri, ci, type)}
321
+ variant="inline"
322
+ />
323
+ </div>
324
+ )}
325
+
326
+ {col.map((block, bi) => {
327
+ const blockSel: EditorSelection = {
328
+ sectionIndex: si,
329
+ rowIndex: ri,
330
+ columnIndex: ci,
331
+ blockIndex: bi,
332
+ }
333
+ const isSelected =
334
+ selection?.sectionIndex === si &&
335
+ selection?.rowIndex === ri &&
336
+ selection?.columnIndex === ci &&
337
+ selection?.blockIndex === bi
338
+ const isDragOver =
339
+ dragOverTarget?.sectionIndex === si &&
340
+ dragOverTarget?.rowIndex === ri &&
341
+ dragOverTarget?.colIndex === ci &&
342
+ dragOverTarget?.blockIndex === bi
343
+
344
+ return (
345
+ <div key={block.id}>
346
+ {/* Drop zone indicator */}
347
+ {isDragOver && dragState && (
348
+ <div className="h-0.5 bg-primary rounded-full mx-2 my-1" />
349
+ )}
350
+ <div
351
+ className={cn(
352
+ 'group relative rounded transition-all cursor-pointer',
353
+ isSelected
354
+ ? 'ring-2 ring-foreground ring-offset-1'
355
+ : 'hover:ring-1 hover:ring-muted-foreground/30'
356
+ )}
357
+ onClick={(e) => {
358
+ e.stopPropagation()
359
+ setSelection(blockSel)
360
+ }}
361
+ draggable
362
+ onDragStart={(e) => {
363
+ e.stopPropagation()
364
+ handleDragStart(si, ri, ci, bi)
365
+ }}
366
+ onDragOver={(e) =>
367
+ handleDragOver(e, si, ri, ci, bi)
368
+ }
369
+ onDrop={(e) =>
370
+ handleDrop(e, si, ri, ci, bi)
371
+ }
372
+ onDragEnd={handleDragEnd}
373
+ >
374
+ {/* Block toolbar (on hover when selected) */}
375
+ {isSelected && (
376
+ <BlockToolbar
377
+ onDelete={() => removeBlock(si, ri, ci, bi)}
378
+ onDuplicate={() => duplicateBlock(si, ri, ci, bi)}
379
+ onMoveUp={() =>
380
+ moveBlock(si, ri, ci, bi, 'up')
381
+ }
382
+ onMoveDown={() =>
383
+ moveBlock(si, ri, ci, bi, 'down')
384
+ }
385
+ canMoveUp={bi > 0}
386
+ canMoveDown={bi < col.length - 1}
387
+ />
388
+ )}
389
+
390
+ {/* Block content */}
391
+ <div className="p-1">
392
+ <BlockRenderer
393
+ block={block}
394
+ editing={!readOnly}
395
+ onChange={(updates) =>
396
+ handleBlockUpdate(blockSel, updates)
397
+ }
398
+ mergeFields={mergeFields ?? []}
399
+ renderTextEditor={renderTextEditor}
400
+ />
401
+ </div>
402
+ </div>
403
+
404
+ {/* Add block between items */}
405
+ {isSelected && (
406
+ <div className="flex justify-center py-1 opacity-0 group-hover:opacity-100 hover:!opacity-100 transition-opacity">
407
+ <AddBlockMenu
408
+ onAdd={(type) =>
409
+ addBlock(si, ri, ci, type, bi)
410
+ }
411
+ variant="small"
412
+ />
413
+ </div>
414
+ )}
415
+ </div>
416
+ )
417
+ })}
418
+
419
+ {/* Add block at end of column */}
420
+ {col.length > 0 && (
421
+ <div className="flex justify-center py-1 opacity-0 hover:opacity-100 transition-opacity">
422
+ <AddBlockMenu
423
+ onAdd={(type) =>
424
+ addBlock(
425
+ si,
426
+ ri,
427
+ ci,
428
+ type,
429
+ col.length - 1
430
+ )
431
+ }
432
+ variant="small"
433
+ />
434
+ </div>
435
+ )}
436
+ </div>
437
+ ))}
438
+ </div>
439
+
440
+ {/* Add row button */}
441
+ {ri === section.rows.length - 1 && (
442
+ <div className="flex justify-center py-2 opacity-0 group-hover/section:opacity-100 transition-opacity">
443
+ <Button
444
+ variant="ghost"
445
+ size="sm"
446
+ className="h-6 text-xs gap-1 text-muted-foreground"
447
+ onClick={(e) => {
448
+ e.stopPropagation()
449
+ addRow(si)
450
+ }}
451
+ >
452
+ <Plus className="h-3 w-3" />
453
+ Add Row
454
+ </Button>
455
+ </div>
456
+ )}
457
+ </div>
458
+ )
459
+ })}
460
+ </div>
461
+
462
+ {/* Add section between */}
463
+ <div className="flex justify-center py-2">
464
+ <Button
465
+ variant="ghost"
466
+ size="sm"
467
+ className="h-6 text-xs gap-1 text-muted-foreground opacity-0 hover:opacity-100 transition-opacity"
468
+ onClick={(e) => {
469
+ e.stopPropagation()
470
+ addSection(si)
471
+ }}
472
+ >
473
+ <Plus className="h-3 w-3" />
474
+ Add Section
475
+ </Button>
476
+ </div>
477
+ </div>
478
+ ))}
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ {/* Sidebar */}
484
+ {showSidebar && (
485
+ <EditorSidebar
486
+ selectedBlock={selectedBlock}
487
+ onBlockChange={(block) => {
488
+ if (selection) updateBlock(selection.sectionIndex, selection.rowIndex, selection.columnIndex, selection.blockIndex, block)
489
+ }}
490
+ globalStyles={globalStyles}
491
+ onGlobalStylesChange={onGlobalStylesChange || (() => {})}
492
+ onClose={() => setShowSidebar(false)}
493
+ />
494
+ )}
495
+ </div>
496
+ )
497
+ }