@actuate-media/cms-admin 0.6.0 → 0.7.1

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 (215) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +17 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/ErrorBoundary.js +1 -1
  6. package/dist/components/ErrorBoundary.js.map +1 -1
  7. package/dist/hooks/useBuilderState.d.ts +49 -0
  8. package/dist/hooks/useBuilderState.d.ts.map +1 -0
  9. package/dist/hooks/useBuilderState.js +238 -0
  10. package/dist/hooks/useBuilderState.js.map +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/layout/Sidebar.d.ts.map +1 -1
  16. package/dist/layout/Sidebar.js +2 -2
  17. package/dist/layout/Sidebar.js.map +1 -1
  18. package/dist/views/FormSubmissions.js +11 -11
  19. package/dist/views/FormSubmissions.js.map +1 -1
  20. package/dist/views/Forms.js +1 -1
  21. package/dist/views/Forms.js.map +1 -1
  22. package/dist/views/MediaBrowser.d.ts.map +1 -1
  23. package/dist/views/MediaBrowser.js +28 -8
  24. package/dist/views/MediaBrowser.js.map +1 -1
  25. package/dist/views/Posts.js +1 -1
  26. package/dist/views/Posts.js.map +1 -1
  27. package/dist/views/Redirects.js +2 -2
  28. package/dist/views/Redirects.js.map +1 -1
  29. package/dist/views/SEO.js +3 -3
  30. package/dist/views/SEO.js.map +1 -1
  31. package/dist/views/Users.js +3 -3
  32. package/dist/views/Users.js.map +1 -1
  33. package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
  34. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  35. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  36. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  37. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  38. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  39. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  40. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  41. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  42. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  43. package/dist/views/page-builder/BlockEditor.js +67 -0
  44. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  45. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  46. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  47. package/dist/views/page-builder/BlockPicker.js +102 -0
  48. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  49. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  50. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  51. package/dist/views/page-builder/BottomBar.js +13 -0
  52. package/dist/views/page-builder/BottomBar.js.map +1 -0
  53. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  54. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  55. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  56. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  57. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  58. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  59. package/dist/views/page-builder/ContextPanel.js +40 -0
  60. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  61. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  62. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  63. package/dist/views/page-builder/DesignScore.js +93 -0
  64. package/dist/views/page-builder/DesignScore.js.map +1 -0
  65. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  66. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  67. package/dist/views/page-builder/NodeSettings.js +80 -0
  68. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  69. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  70. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  71. package/dist/views/page-builder/PageBuilder.js +126 -0
  72. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  73. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  74. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  75. package/dist/views/page-builder/PageSettings.js +27 -0
  76. package/dist/views/page-builder/PageSettings.js.map +1 -0
  77. package/dist/views/page-builder/PageTemplates.d.ts +5 -0
  78. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -0
  79. package/dist/views/page-builder/PageTemplates.js +13 -0
  80. package/dist/views/page-builder/PageTemplates.js.map +1 -0
  81. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  82. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  83. package/dist/views/page-builder/SEOPanel.js +105 -0
  84. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  85. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  86. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  87. package/dist/views/page-builder/SavedSections.js +145 -0
  88. package/dist/views/page-builder/SavedSections.js.map +1 -0
  89. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  90. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  91. package/dist/views/page-builder/TemplatePicker.js +68 -0
  92. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  93. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  94. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  95. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  96. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  97. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  98. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  99. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  100. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  101. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  102. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  103. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  104. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  105. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  106. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  107. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  108. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  109. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  110. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  111. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  112. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  113. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  114. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  115. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  116. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  117. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  118. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  119. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  120. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  121. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  122. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  123. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  124. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  125. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  126. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  127. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  128. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  129. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  130. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  131. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  132. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  133. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  134. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  135. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  136. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  137. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  138. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  139. package/dist/views/page-builder/block-renderers/index.js +25 -0
  140. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  141. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  142. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  143. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  144. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  145. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  146. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  147. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  148. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  149. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  150. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  151. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  152. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  153. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  154. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  155. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  156. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  157. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  158. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  159. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  160. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  161. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  162. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  163. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  164. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  165. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  166. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  167. package/dist/views/page-builder/canvas/index.js +2 -0
  168. package/dist/views/page-builder/canvas/index.js.map +1 -0
  169. package/package.json +3 -2
  170. package/src/AdminRoot.tsx +21 -0
  171. package/src/components/ErrorBoundary.tsx +3 -3
  172. package/src/hooks/useBuilderState.ts +328 -0
  173. package/src/index.ts +4 -0
  174. package/src/layout/Sidebar.tsx +5 -0
  175. package/src/views/FormSubmissions.tsx +12 -12
  176. package/src/views/Forms.tsx +1 -1
  177. package/src/views/MediaBrowser.tsx +46 -15
  178. package/src/views/Posts.tsx +1 -1
  179. package/src/views/Redirects.tsx +2 -2
  180. package/src/views/SEO.tsx +3 -3
  181. package/src/views/Users.tsx +3 -3
  182. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  183. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  184. package/src/views/page-builder/BlockEditor.tsx +352 -0
  185. package/src/views/page-builder/BlockPicker.tsx +338 -0
  186. package/src/views/page-builder/BottomBar.tsx +64 -0
  187. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  188. package/src/views/page-builder/ContextPanel.tsx +145 -0
  189. package/src/views/page-builder/DesignScore.tsx +258 -0
  190. package/src/views/page-builder/NodeSettings.tsx +515 -0
  191. package/src/views/page-builder/PageBuilder.tsx +288 -0
  192. package/src/views/page-builder/PageSettings.tsx +161 -0
  193. package/src/views/page-builder/PageTemplates.tsx +105 -0
  194. package/src/views/page-builder/SEOPanel.tsx +485 -0
  195. package/src/views/page-builder/SavedSections.tsx +486 -0
  196. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  197. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  198. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  199. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  200. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  201. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  202. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  203. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  204. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  205. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  206. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  207. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  208. package/src/views/page-builder/block-renderers/index.ts +34 -0
  209. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  210. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  211. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  212. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  213. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  214. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  215. package/src/views/page-builder/canvas/index.ts +2 -0
@@ -0,0 +1,515 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import {
5
+ ArrowUp,
6
+ ArrowDown,
7
+ Copy,
8
+ Trash2,
9
+ Plus,
10
+ AlignLeft,
11
+ AlignCenter,
12
+ AlignRight,
13
+ ArrowUpFromLine,
14
+ ArrowDownToLine,
15
+ Minus,
16
+ } from 'lucide-react';
17
+ import * as SwitchPrimitive from '@radix-ui/react-switch';
18
+ import type {
19
+ BuilderNode,
20
+ SectionNode,
21
+ ContainerNode,
22
+ RowNode,
23
+ ColumnNode,
24
+ } from '@actuate-media/cms-core';
25
+
26
+ export interface NodeSettingsProps {
27
+ node: SectionNode | ContainerNode | RowNode | ColumnNode;
28
+ onUpdateSettings: (id: string, settings: Record<string, unknown>) => void;
29
+ onRemoveNode: (id: string) => void;
30
+ onDuplicateNode: (id: string) => void;
31
+ onMoveNodeUp: (id: string) => void;
32
+ onMoveNodeDown: (id: string) => void;
33
+ onAddRow?: (sectionId: string) => void;
34
+ }
35
+
36
+ const INPUT_CLASS =
37
+ 'w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring';
38
+ const LABEL_CLASS = 'text-sm font-medium text-foreground mb-1 block';
39
+ const SECTION_HEADING_CLASS = 'text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2';
40
+
41
+ const COLUMN_PRESETS: { label: string; widths: number[] }[] = [
42
+ { label: '12', widths: [12] },
43
+ { label: '6 | 6', widths: [6, 6] },
44
+ { label: '4 | 4 | 4', widths: [4, 4, 4] },
45
+ { label: '3 | 3 | 3 | 3', widths: [3, 3, 3, 3] },
46
+ { label: '8 | 4', widths: [8, 4] },
47
+ { label: '4 | 8', widths: [4, 8] },
48
+ { label: '3 | 6 | 3', widths: [3, 6, 3] },
49
+ ];
50
+
51
+ export function NodeSettings({
52
+ node,
53
+ onUpdateSettings,
54
+ onRemoveNode,
55
+ onDuplicateNode,
56
+ onMoveNodeUp,
57
+ onMoveNodeDown,
58
+ onAddRow,
59
+ }: NodeSettingsProps) {
60
+ const [confirmDelete, setConfirmDelete] = useState(false);
61
+
62
+ const updateSetting = useCallback(
63
+ (key: string, value: unknown) => {
64
+ onUpdateSettings(node.id, { [key]: value });
65
+ },
66
+ [node.id, onUpdateSettings]
67
+ );
68
+
69
+ const handleDelete = useCallback(() => {
70
+ if (confirmDelete) {
71
+ onRemoveNode(node.id);
72
+ setConfirmDelete(false);
73
+ } else {
74
+ setConfirmDelete(true);
75
+ }
76
+ }, [confirmDelete, node.id, onRemoveNode]);
77
+
78
+ const nodeLabel =
79
+ node.type === 'section'
80
+ ? 'Section'
81
+ : node.type === 'container'
82
+ ? 'Container'
83
+ : node.type === 'row'
84
+ ? 'Row'
85
+ : 'Column';
86
+
87
+ return (
88
+ <div className="flex flex-col h-full">
89
+ <div className="p-4 border-b border-border">
90
+ <p className="text-sm font-medium text-foreground">{nodeLabel} Settings</p>
91
+ </div>
92
+
93
+ <div className="flex-1 overflow-y-auto">
94
+ <div className="space-y-4 p-4">
95
+ {node.type === 'section' && <SectionFields node={node} updateSetting={updateSetting} onAddRow={onAddRow} />}
96
+ {node.type === 'container' && <ContainerFields node={node} updateSetting={updateSetting} />}
97
+ {node.type === 'row' && <RowFields node={node} updateSetting={updateSetting} />}
98
+ {node.type === 'column' && <ColumnFields node={node} updateSetting={updateSetting} />}
99
+ </div>
100
+ </div>
101
+
102
+ <div className="p-4 border-t border-border space-y-2">
103
+ <p className={SECTION_HEADING_CLASS}>Actions</p>
104
+ <div className="flex gap-2">
105
+ <button
106
+ type="button"
107
+ onClick={() => onMoveNodeUp(node.id)}
108
+ aria-label="Move up"
109
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-sm bg-background border border-input rounded-md hover:bg-accent transition-colors"
110
+ >
111
+ <ArrowUp size={14} />
112
+ Up
113
+ </button>
114
+ <button
115
+ type="button"
116
+ onClick={() => onMoveNodeDown(node.id)}
117
+ aria-label="Move down"
118
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-sm bg-background border border-input rounded-md hover:bg-accent transition-colors"
119
+ >
120
+ <ArrowDown size={14} />
121
+ Down
122
+ </button>
123
+ </div>
124
+ <button
125
+ type="button"
126
+ onClick={() => onDuplicateNode(node.id)}
127
+ className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium bg-background border border-input rounded-md hover:bg-accent transition-colors"
128
+ >
129
+ <Copy size={14} />
130
+ Duplicate
131
+ </button>
132
+ <button
133
+ type="button"
134
+ onClick={handleDelete}
135
+ onBlur={() => setConfirmDelete(false)}
136
+ className={`w-full flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors ${
137
+ confirmDelete
138
+ ? 'bg-destructive text-destructive-foreground'
139
+ : 'bg-background border border-destructive text-destructive hover:bg-destructive/10'
140
+ }`}
141
+ >
142
+ <Trash2 size={14} />
143
+ {confirmDelete ? 'Click again to confirm' : 'Delete'}
144
+ </button>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function SectionFields({
151
+ node,
152
+ updateSetting,
153
+ onAddRow,
154
+ }: {
155
+ node: SectionNode;
156
+ updateSetting: (key: string, value: unknown) => void;
157
+ onAddRow?: (sectionId: string) => void;
158
+ }) {
159
+ const s = node.settings;
160
+ return (
161
+ <>
162
+ <p className={SECTION_HEADING_CLASS}>Background</p>
163
+ <div>
164
+ <label className={LABEL_CLASS}>Background Color</label>
165
+ <div className="flex items-center gap-2">
166
+ <input
167
+ type="color"
168
+ value={s.background ?? '#ffffff'}
169
+ onChange={(e) => updateSetting('background', e.target.value)}
170
+ className="h-9 w-9 rounded-md border border-input cursor-pointer"
171
+ />
172
+ <input
173
+ type="text"
174
+ value={s.background ?? ''}
175
+ onChange={(e) => updateSetting('background', e.target.value)}
176
+ placeholder="transparent"
177
+ className={`${INPUT_CLASS} flex-1`}
178
+ />
179
+ </div>
180
+ </div>
181
+
182
+ <p className={SECTION_HEADING_CLASS}>Spacing</p>
183
+ <div className="grid grid-cols-2 gap-3">
184
+ <div>
185
+ <label className={LABEL_CLASS}>Padding Top</label>
186
+ <input
187
+ type="text"
188
+ value={s.paddingTop ?? ''}
189
+ onChange={(e) => updateSetting('paddingTop', e.target.value)}
190
+ placeholder="0px"
191
+ className={INPUT_CLASS}
192
+ />
193
+ </div>
194
+ <div>
195
+ <label className={LABEL_CLASS}>Padding Bottom</label>
196
+ <input
197
+ type="text"
198
+ value={s.paddingBottom ?? ''}
199
+ onChange={(e) => updateSetting('paddingBottom', e.target.value)}
200
+ placeholder="0px"
201
+ className={INPUT_CLASS}
202
+ />
203
+ </div>
204
+ <div>
205
+ <label className={LABEL_CLASS}>Margin Top</label>
206
+ <input
207
+ type="text"
208
+ value={s.marginTop ?? ''}
209
+ onChange={(e) => updateSetting('marginTop', e.target.value)}
210
+ placeholder="0px"
211
+ className={INPUT_CLASS}
212
+ />
213
+ </div>
214
+ <div>
215
+ <label className={LABEL_CLASS}>Margin Bottom</label>
216
+ <input
217
+ type="text"
218
+ value={s.marginBottom ?? ''}
219
+ onChange={(e) => updateSetting('marginBottom', e.target.value)}
220
+ placeholder="0px"
221
+ className={INPUT_CLASS}
222
+ />
223
+ </div>
224
+ </div>
225
+
226
+ <p className={SECTION_HEADING_CLASS}>Attributes</p>
227
+ <div className="flex items-center justify-between">
228
+ <label className="text-sm font-medium text-foreground">Visibility</label>
229
+ <SwitchPrimitive.Root
230
+ checked={s.visibility !== 'hidden'}
231
+ onCheckedChange={(checked) =>
232
+ updateSetting('visibility', checked ? 'visible' : 'hidden')
233
+ }
234
+ className="w-9 h-5 bg-input rounded-full relative data-[state=checked]:bg-primary transition-colors"
235
+ aria-label="Section visibility"
236
+ >
237
+ <SwitchPrimitive.Thumb className="block h-3.5 w-3.5 rounded-full bg-background shadow-sm transition-transform translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
238
+ </SwitchPrimitive.Root>
239
+ </div>
240
+ <div>
241
+ <label className={LABEL_CLASS}>HTML ID</label>
242
+ <input
243
+ type="text"
244
+ value={s.htmlId ?? ''}
245
+ onChange={(e) => updateSetting('htmlId', e.target.value)}
246
+ placeholder="section-id"
247
+ className={INPUT_CLASS}
248
+ />
249
+ </div>
250
+ <div>
251
+ <label className={LABEL_CLASS}>HTML Class</label>
252
+ <input
253
+ type="text"
254
+ value={s.htmlClass ?? ''}
255
+ onChange={(e) => updateSetting('htmlClass', e.target.value)}
256
+ placeholder="my-class"
257
+ className={INPUT_CLASS}
258
+ />
259
+ </div>
260
+
261
+ {onAddRow && (
262
+ <button
263
+ type="button"
264
+ onClick={() => onAddRow(node.id)}
265
+ className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
266
+ >
267
+ <Plus size={14} />
268
+ Add Row
269
+ </button>
270
+ )}
271
+ </>
272
+ );
273
+ }
274
+
275
+ function ContainerFields({
276
+ node,
277
+ updateSetting,
278
+ }: {
279
+ node: ContainerNode;
280
+ updateSetting: (key: string, value: unknown) => void;
281
+ }) {
282
+ const s = node.settings;
283
+ return (
284
+ <>
285
+ <p className={SECTION_HEADING_CLASS}>Layout</p>
286
+ <div>
287
+ <label className={LABEL_CLASS}>Max Width</label>
288
+ <input
289
+ type="text"
290
+ value={s.maxWidth ?? ''}
291
+ onChange={(e) => updateSetting('maxWidth', e.target.value)}
292
+ placeholder="1200px"
293
+ className={INPUT_CLASS}
294
+ />
295
+ </div>
296
+ <div>
297
+ <label className={LABEL_CLASS}>Alignment</label>
298
+ <div className="flex gap-1">
299
+ {(['left', 'center', 'right'] as const).map((align) => {
300
+ const Icon = align === 'left' ? AlignLeft : align === 'center' ? AlignCenter : AlignRight;
301
+ return (
302
+ <button
303
+ key={align}
304
+ type="button"
305
+ onClick={() => updateSetting('alignment', align)}
306
+ aria-label={`Align ${align}`}
307
+ className={`flex-1 flex items-center justify-center py-2 rounded-md border transition-colors ${
308
+ s.alignment === align
309
+ ? 'bg-primary text-primary-foreground border-primary'
310
+ : 'bg-background border-input hover:bg-accent'
311
+ }`}
312
+ >
313
+ <Icon size={14} />
314
+ </button>
315
+ );
316
+ })}
317
+ </div>
318
+ </div>
319
+ <div>
320
+ <label className={LABEL_CLASS}>Padding</label>
321
+ <input
322
+ type="text"
323
+ value={s.padding ?? ''}
324
+ onChange={(e) => updateSetting('padding', e.target.value)}
325
+ placeholder="0px"
326
+ className={INPUT_CLASS}
327
+ />
328
+ </div>
329
+ </>
330
+ );
331
+ }
332
+
333
+ function RowFields({
334
+ node,
335
+ updateSetting,
336
+ }: {
337
+ node: RowNode;
338
+ updateSetting: (key: string, value: unknown) => void;
339
+ }) {
340
+ const s = node.settings;
341
+ return (
342
+ <>
343
+ <p className={SECTION_HEADING_CLASS}>Layout</p>
344
+ <div>
345
+ <label className={LABEL_CLASS}>Gap</label>
346
+ <input
347
+ type="text"
348
+ value={s.gap ?? ''}
349
+ onChange={(e) => updateSetting('gap', e.target.value)}
350
+ placeholder="16px"
351
+ className={INPUT_CLASS}
352
+ />
353
+ </div>
354
+ <div>
355
+ <label className={LABEL_CLASS}>Vertical Align</label>
356
+ <div className="flex gap-1">
357
+ {(['top', 'center', 'bottom', 'stretch'] as const).map((align) => {
358
+ const Icon =
359
+ align === 'top'
360
+ ? ArrowUpFromLine
361
+ : align === 'bottom'
362
+ ? ArrowDownToLine
363
+ : align === 'center'
364
+ ? Minus
365
+ : AlignCenter;
366
+ return (
367
+ <button
368
+ key={align}
369
+ type="button"
370
+ onClick={() => updateSetting('verticalAlign', align)}
371
+ aria-label={`Align ${align}`}
372
+ className={`flex-1 flex items-center justify-center py-2 rounded-md border text-xs transition-colors ${
373
+ s.verticalAlign === align
374
+ ? 'bg-primary text-primary-foreground border-primary'
375
+ : 'bg-background border-input hover:bg-accent'
376
+ }`}
377
+ >
378
+ <Icon size={14} />
379
+ </button>
380
+ );
381
+ })}
382
+ </div>
383
+ </div>
384
+
385
+ <p className={SECTION_HEADING_CLASS}>Mobile</p>
386
+ <div className="flex items-center justify-between">
387
+ <label className="text-sm font-medium text-foreground">Reverse on Mobile</label>
388
+ <SwitchPrimitive.Root
389
+ checked={!!s.reverseOnMobile}
390
+ onCheckedChange={(checked) => updateSetting('reverseOnMobile', checked)}
391
+ className="w-9 h-5 bg-input rounded-full relative data-[state=checked]:bg-primary transition-colors"
392
+ aria-label="Reverse column order on mobile"
393
+ >
394
+ <SwitchPrimitive.Thumb className="block h-3.5 w-3.5 rounded-full bg-background shadow-sm transition-transform translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
395
+ </SwitchPrimitive.Root>
396
+ </div>
397
+ <div className="flex items-center justify-between">
398
+ <label className="text-sm font-medium text-foreground">Wrap on Mobile</label>
399
+ <SwitchPrimitive.Root
400
+ checked={!!s.wrapOnMobile}
401
+ onCheckedChange={(checked) => updateSetting('wrapOnMobile', checked)}
402
+ className="w-9 h-5 bg-input rounded-full relative data-[state=checked]:bg-primary transition-colors"
403
+ aria-label="Wrap columns on mobile"
404
+ >
405
+ <SwitchPrimitive.Thumb className="block h-3.5 w-3.5 rounded-full bg-background shadow-sm transition-transform translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
406
+ </SwitchPrimitive.Root>
407
+ </div>
408
+
409
+ <p className={SECTION_HEADING_CLASS}>Column Presets</p>
410
+ <div className="grid grid-cols-2 gap-1.5">
411
+ {COLUMN_PRESETS.map((preset) => (
412
+ <button
413
+ key={preset.label}
414
+ type="button"
415
+ onClick={() => updateSetting('__columnPreset', preset.widths)}
416
+ className="px-2 py-2 rounded-md border border-input bg-background hover:bg-accent transition-colors"
417
+ >
418
+ <div className="flex gap-0.5 h-4">
419
+ {preset.widths.map((w, i) => (
420
+ <div
421
+ key={i}
422
+ className="bg-muted-foreground/30 rounded-sm"
423
+ style={{ flex: w }}
424
+ />
425
+ ))}
426
+ </div>
427
+ <p className="text-xs text-muted-foreground mt-1 text-center">
428
+ {preset.widths.join(' | ')}
429
+ </p>
430
+ </button>
431
+ ))}
432
+ </div>
433
+ </>
434
+ );
435
+ }
436
+
437
+ function ColumnFields({
438
+ node,
439
+ updateSetting,
440
+ }: {
441
+ node: ColumnNode;
442
+ updateSetting: (key: string, value: unknown) => void;
443
+ }) {
444
+ const s = node.settings;
445
+ return (
446
+ <>
447
+ <p className={SECTION_HEADING_CLASS}>Size</p>
448
+ <div>
449
+ <label className={LABEL_CLASS}>Width (1-12)</label>
450
+ <input
451
+ type="number"
452
+ min={1}
453
+ max={12}
454
+ value={s.width}
455
+ onChange={(e) => updateSetting('width', Number(e.target.value))}
456
+ className={INPUT_CLASS}
457
+ />
458
+ </div>
459
+
460
+ <p className={SECTION_HEADING_CLASS}>Layout</p>
461
+ <div>
462
+ <label className={LABEL_CLASS}>Vertical Align</label>
463
+ <div className="flex gap-1">
464
+ {(['top', 'center', 'bottom'] as const).map((align) => {
465
+ const Icon =
466
+ align === 'top' ? ArrowUpFromLine : align === 'bottom' ? ArrowDownToLine : Minus;
467
+ return (
468
+ <button
469
+ key={align}
470
+ type="button"
471
+ onClick={() => updateSetting('verticalAlign', align)}
472
+ aria-label={`Align ${align}`}
473
+ className={`flex-1 flex items-center justify-center py-2 rounded-md border transition-colors ${
474
+ s.verticalAlign === align
475
+ ? 'bg-primary text-primary-foreground border-primary'
476
+ : 'bg-background border-input hover:bg-accent'
477
+ }`}
478
+ >
479
+ <Icon size={14} />
480
+ </button>
481
+ );
482
+ })}
483
+ </div>
484
+ </div>
485
+ <div>
486
+ <label className={LABEL_CLASS}>Padding</label>
487
+ <input
488
+ type="text"
489
+ value={s.padding ?? ''}
490
+ onChange={(e) => updateSetting('padding', e.target.value)}
491
+ placeholder="0px"
492
+ className={INPUT_CLASS}
493
+ />
494
+ </div>
495
+ <div>
496
+ <label className={LABEL_CLASS}>Background Color</label>
497
+ <div className="flex items-center gap-2">
498
+ <input
499
+ type="color"
500
+ value={s.background ?? '#ffffff'}
501
+ onChange={(e) => updateSetting('background', e.target.value)}
502
+ className="h-9 w-9 rounded-md border border-input cursor-pointer"
503
+ />
504
+ <input
505
+ type="text"
506
+ value={s.background ?? ''}
507
+ onChange={(e) => updateSetting('background', e.target.value)}
508
+ placeholder="transparent"
509
+ className={`${INPUT_CLASS} flex-1`}
510
+ />
511
+ </div>
512
+ </div>
513
+ </>
514
+ );
515
+ }