@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,288 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Loader2, AlertTriangle } from 'lucide-react';
5
+ import { toast } from 'sonner';
6
+ import { cmsApi } from '../../lib/api.js';
7
+ import { ErrorBoundary } from '../../components/ErrorBoundary.js';
8
+ import {
9
+ useBuilderState,
10
+ type BuilderState,
11
+ type BuilderActions,
12
+ type PageSettings,
13
+ } from '../../hooks/useBuilderState.js';
14
+ import type { PageNode } from '@actuate-media/cms-core';
15
+ import { BuilderToolbar } from './BuilderToolbar.js';
16
+ import { BottomBar } from './BottomBar.js';
17
+ import { BuilderCanvas } from './canvas/index.js';
18
+ import { ContextPanel } from './ContextPanel.js';
19
+ import { AIGenerateDialog } from './AIGenerateDialog.js';
20
+
21
+ export interface PageBuilderProps {
22
+ documentId?: string;
23
+ collectionSlug: string;
24
+ config: any;
25
+ onNavigate: (path: string) => void;
26
+ }
27
+
28
+ type DocumentStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED';
29
+
30
+ const DEVICE_WIDTHS = {
31
+ desktop: '100%',
32
+ tablet: '768px',
33
+ mobile: '375px',
34
+ } as const;
35
+
36
+ export function PageBuilder({
37
+ documentId,
38
+ collectionSlug,
39
+ config,
40
+ onNavigate,
41
+ }: PageBuilderProps) {
42
+ const [loading, setLoading] = useState(!!documentId);
43
+ const [loadError, setLoadError] = useState<string | null>(null);
44
+ const [saving, setSaving] = useState(false);
45
+ const [status, setStatus] = useState<DocumentStatus>('DRAFT');
46
+ const [aiDialogOpen, setAiDialogOpen] = useState(false);
47
+
48
+ const builder = useBuilderState();
49
+ const {
50
+ tree,
51
+ pageSettings,
52
+ deviceMode,
53
+ dirty,
54
+ canUndo,
55
+ canRedo,
56
+ selectedNodeId,
57
+ selectedNode,
58
+ activeTab,
59
+ showGridOverlay,
60
+ selectNode,
61
+ setDeviceMode,
62
+ setActiveTab,
63
+ setPageSettings,
64
+ setShowGridOverlay,
65
+ addSection,
66
+ addRowToSection,
67
+ addBlockToColumn,
68
+ addNodeAtId,
69
+ removeNodeById,
70
+ moveNodeById,
71
+ updateSettings,
72
+ updateBlock,
73
+ duplicateNode,
74
+ moveNodeUp,
75
+ moveNodeDown,
76
+ undo,
77
+ redo,
78
+ markClean,
79
+ replaceTree,
80
+ } = builder;
81
+
82
+ useEffect(() => {
83
+ if (!documentId) {
84
+ setLoading(false);
85
+ return;
86
+ }
87
+
88
+ let cancelled = false;
89
+
90
+ async function loadDocument() {
91
+ const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`);
92
+
93
+ if (cancelled) return;
94
+
95
+ if (res.error) {
96
+ setLoadError(res.error);
97
+ setLoading(false);
98
+ return;
99
+ }
100
+
101
+ const doc = res.data;
102
+ if (doc?.layout) {
103
+ replaceTree(doc.layout as PageNode);
104
+ markClean();
105
+ }
106
+ if (doc?.pageSettings) {
107
+ setPageSettings(doc.pageSettings);
108
+ }
109
+ if (doc?.status) {
110
+ setStatus(doc.status as DocumentStatus);
111
+ }
112
+ if (doc?.title) {
113
+ setPageSettings({ title: doc.title });
114
+ }
115
+
116
+ setLoading(false);
117
+ }
118
+
119
+ loadDocument();
120
+ return () => { cancelled = true; };
121
+ }, [documentId, collectionSlug]);
122
+
123
+ const handleSave = useCallback(async () => {
124
+ setSaving(true);
125
+ const body = JSON.stringify({
126
+ layout: tree,
127
+ pageSettings,
128
+ title: pageSettings.title,
129
+ slug: pageSettings.slug,
130
+ });
131
+
132
+ const endpoint = documentId
133
+ ? `/collections/${collectionSlug}/${documentId}`
134
+ : `/collections/${collectionSlug}`;
135
+ const method = documentId ? 'PUT' : 'POST';
136
+
137
+ const res = await cmsApi(endpoint, { method, body });
138
+ setSaving(false);
139
+
140
+ if (res.error) {
141
+ toast.error(res.error);
142
+ } else {
143
+ markClean();
144
+ toast.success('Page saved');
145
+ if (!documentId && (res.data as any)?.id) {
146
+ onNavigate(`/pages/${(res.data as any).id}/builder`);
147
+ }
148
+ }
149
+ }, [tree, pageSettings, documentId, collectionSlug, markClean, onNavigate]);
150
+
151
+ const handlePublish = useCallback(async () => {
152
+ setSaving(true);
153
+ const body = JSON.stringify({
154
+ layout: tree,
155
+ pageSettings,
156
+ title: pageSettings.title,
157
+ slug: pageSettings.slug,
158
+ status: 'PUBLISHED',
159
+ });
160
+
161
+ const endpoint = documentId
162
+ ? `/collections/${collectionSlug}/${documentId}`
163
+ : `/collections/${collectionSlug}`;
164
+ const method = documentId ? 'PUT' : 'POST';
165
+
166
+ const res = await cmsApi(endpoint, { method, body });
167
+ setSaving(false);
168
+
169
+ if (res.error) {
170
+ toast.error(res.error);
171
+ } else {
172
+ markClean();
173
+ setStatus('PUBLISHED');
174
+ toast.success('Page published');
175
+ if (!documentId && (res.data as any)?.id) {
176
+ onNavigate(`/pages/${(res.data as any).id}/builder`);
177
+ }
178
+ }
179
+ }, [tree, pageSettings, documentId, collectionSlug, markClean, onNavigate]);
180
+
181
+ const handleAIAccept = useCallback((generatedTree: PageNode) => {
182
+ replaceTree(generatedTree);
183
+ }, [replaceTree]);
184
+
185
+ if (loading) {
186
+ return (
187
+ <div className="h-full flex items-center justify-center bg-background">
188
+ <Loader2 className="animate-spin text-muted-foreground" size={24} />
189
+ </div>
190
+ );
191
+ }
192
+
193
+ if (loadError) {
194
+ return (
195
+ <div className="h-full flex items-center justify-center bg-background">
196
+ <div className="flex items-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4 max-w-md">
197
+ <AlertTriangle className="text-destructive shrink-0" size={20} />
198
+ <div>
199
+ <p className="text-sm font-medium text-foreground">Failed to load page</p>
200
+ <p className="text-sm text-muted-foreground mt-1">{loadError}</p>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ const canvasWidth = DEVICE_WIDTHS[deviceMode];
208
+ const isConstrained = deviceMode !== 'desktop';
209
+
210
+ return (
211
+ <ErrorBoundary>
212
+ <div className="h-full flex flex-col bg-background overflow-hidden">
213
+ <BuilderToolbar
214
+ collectionSlug={collectionSlug}
215
+ pageSettings={pageSettings}
216
+ status={status}
217
+ dirty={dirty}
218
+ saving={saving}
219
+ canUndo={canUndo}
220
+ canRedo={canRedo}
221
+ deviceMode={deviceMode}
222
+ onNavigate={onNavigate}
223
+ onTitleChange={(title) => setPageSettings({ title })}
224
+ onUndo={undo}
225
+ onRedo={redo}
226
+ onDeviceMode={setDeviceMode}
227
+ onSave={handleSave}
228
+ onPublish={handlePublish}
229
+ onOpenAI={() => setAiDialogOpen(true)}
230
+ />
231
+
232
+ <div className="flex-1 flex overflow-hidden">
233
+ {/* Canvas area */}
234
+ <div className="flex-1 overflow-auto bg-muted">
235
+ <div
236
+ className="mx-auto h-full max-w-full transition-all duration-200"
237
+ style={{ width: canvasWidth }}
238
+ >
239
+ <div
240
+ className={`h-full bg-background ${isConstrained ? 'shadow-lg border-x border-border' : ''}`}
241
+ >
242
+ <BuilderCanvas
243
+ tree={tree}
244
+ selectedNodeId={selectedNodeId}
245
+ showGridOverlay={showGridOverlay}
246
+ deviceMode={deviceMode}
247
+ onSelectNode={selectNode}
248
+ />
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Context panel */}
254
+ <div className="w-[35%] min-w-[280px] max-w-[420px] border-l border-border bg-card overflow-y-auto">
255
+ <ContextPanel
256
+ activeTab={activeTab}
257
+ onTabChange={setActiveTab}
258
+ selectedNode={selectedNode}
259
+ tree={tree}
260
+ pageSettings={pageSettings}
261
+ onUpdateSettings={updateSettings}
262
+ onUpdateBlock={updateBlock}
263
+ onRemoveNode={removeNodeById}
264
+ onDuplicateNode={duplicateNode}
265
+ onMoveNodeUp={moveNodeUp}
266
+ onMoveNodeDown={moveNodeDown}
267
+ onPageSettingsChange={setPageSettings}
268
+ onAddRow={addRowToSection}
269
+ config={config}
270
+ />
271
+ </div>
272
+ </div>
273
+
274
+ <BottomBar
275
+ deviceMode={deviceMode}
276
+ showGridOverlay={showGridOverlay}
277
+ onAddSection={addSection}
278
+ onToggleGrid={setShowGridOverlay}
279
+ />
280
+ </div>
281
+ <AIGenerateDialog
282
+ open={aiDialogOpen}
283
+ onClose={() => setAiDialogOpen(false)}
284
+ onAccept={handleAIAccept}
285
+ />
286
+ </ErrorBoundary>
287
+ );
288
+ }
@@ -0,0 +1,161 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import type { PageSettings } from '../../hooks/useBuilderState.js';
5
+
6
+ export interface PageSettingsProps {
7
+ settings: PageSettings;
8
+ onChange: (settings: Partial<PageSettings>) => void;
9
+ }
10
+
11
+ const INPUT_CLASS =
12
+ 'w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring';
13
+ const LABEL_CLASS = 'text-sm font-medium text-foreground mb-1 block';
14
+ const SECTION_HEADING_CLASS = 'text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2';
15
+
16
+ const SCHEMA_TYPES = [
17
+ 'Article',
18
+ 'LocalBusiness',
19
+ 'WebPage',
20
+ 'Product',
21
+ 'FAQPage',
22
+ 'Organization',
23
+ 'Event',
24
+ 'Recipe',
25
+ 'HowTo',
26
+ 'BreadcrumbList',
27
+ ];
28
+
29
+ export function PageSettingsEditor({ settings, onChange }: PageSettingsProps) {
30
+ const update = useCallback(
31
+ (key: keyof PageSettings, value: string) => {
32
+ onChange({ [key]: value });
33
+ },
34
+ [onChange]
35
+ );
36
+
37
+ const metaTitleLength = (settings.metaTitle ?? '').length;
38
+ const metaDescLength = (settings.metaDescription ?? '').length;
39
+
40
+ return (
41
+ <div className="space-y-4 p-4">
42
+ <p className={SECTION_HEADING_CLASS}>General</p>
43
+
44
+ <div>
45
+ <label className={LABEL_CLASS}>Title</label>
46
+ <input
47
+ type="text"
48
+ value={settings.title}
49
+ onChange={(e) => update('title', e.target.value)}
50
+ placeholder="Page title"
51
+ className={INPUT_CLASS}
52
+ />
53
+ </div>
54
+
55
+ <div>
56
+ <label className={LABEL_CLASS}>Slug</label>
57
+ <input
58
+ type="text"
59
+ value={settings.slug}
60
+ onChange={(e) => update('slug', e.target.value)}
61
+ placeholder="/page-slug"
62
+ className={INPUT_CLASS}
63
+ />
64
+ </div>
65
+
66
+ <div>
67
+ <label className={LABEL_CLASS}>Template</label>
68
+ <input
69
+ type="text"
70
+ value={settings.template ?? ''}
71
+ onChange={(e) => update('template', e.target.value)}
72
+ placeholder="default"
73
+ className={INPUT_CLASS}
74
+ readOnly
75
+ />
76
+ </div>
77
+
78
+ <p className={SECTION_HEADING_CLASS}>SEO</p>
79
+
80
+ <div>
81
+ <div className="flex items-center justify-between mb-1">
82
+ <label className="text-sm font-medium text-foreground">Meta Title</label>
83
+ <span
84
+ className={`text-xs ${
85
+ metaTitleLength > 60 ? 'text-destructive' : 'text-muted-foreground'
86
+ }`}
87
+ >
88
+ {metaTitleLength}/60
89
+ </span>
90
+ </div>
91
+ <input
92
+ type="text"
93
+ value={settings.metaTitle ?? ''}
94
+ onChange={(e) => update('metaTitle', e.target.value)}
95
+ placeholder="SEO title"
96
+ maxLength={100}
97
+ className={INPUT_CLASS}
98
+ />
99
+ </div>
100
+
101
+ <div>
102
+ <div className="flex items-center justify-between mb-1">
103
+ <label className="text-sm font-medium text-foreground">Meta Description</label>
104
+ <span
105
+ className={`text-xs ${
106
+ metaDescLength > 160 ? 'text-destructive' : 'text-muted-foreground'
107
+ }`}
108
+ >
109
+ {metaDescLength}/160
110
+ </span>
111
+ </div>
112
+ <textarea
113
+ value={settings.metaDescription ?? ''}
114
+ onChange={(e) => update('metaDescription', e.target.value)}
115
+ placeholder="Brief description for search engines"
116
+ rows={3}
117
+ maxLength={250}
118
+ className={`${INPUT_CLASS} resize-y`}
119
+ />
120
+ </div>
121
+
122
+ <div>
123
+ <label className={LABEL_CLASS}>OG Image</label>
124
+ <input
125
+ type="text"
126
+ value={settings.ogImage ?? ''}
127
+ onChange={(e) => update('ogImage', e.target.value)}
128
+ placeholder="https://example.com/image.jpg"
129
+ className={INPUT_CLASS}
130
+ />
131
+ </div>
132
+
133
+ <div>
134
+ <label className={LABEL_CLASS}>Focus Keyphrase</label>
135
+ <input
136
+ type="text"
137
+ value={settings.focusKeyphrase ?? ''}
138
+ onChange={(e) => update('focusKeyphrase', e.target.value)}
139
+ placeholder="primary keyword"
140
+ className={INPUT_CLASS}
141
+ />
142
+ </div>
143
+
144
+ <div>
145
+ <label className={LABEL_CLASS}>Schema Type</label>
146
+ <select
147
+ value={settings.schemaType ?? ''}
148
+ onChange={(e) => update('schemaType', e.target.value)}
149
+ className={INPUT_CLASS}
150
+ >
151
+ <option value="">Select schema type...</option>
152
+ {SCHEMA_TYPES.map((type) => (
153
+ <option key={type} value={type}>
154
+ {type}
155
+ </option>
156
+ ))}
157
+ </select>
158
+ </div>
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { AlertTriangle, Layers, Loader2, RefreshCw } from 'lucide-react';
4
+ import { useApiData } from '../../lib/useApiData.js';
5
+
6
+ interface PageTemplate {
7
+ id: string;
8
+ name?: string;
9
+ description?: string | null;
10
+ category?: string;
11
+ builtIn?: boolean;
12
+ updatedAt?: string;
13
+ }
14
+
15
+ export interface PageTemplatesProps {
16
+ onNavigate?: (path: string) => void;
17
+ }
18
+
19
+ export function PageTemplates({ onNavigate }: PageTemplatesProps) {
20
+ const { data, loading, error, refetch } = useApiData<PageTemplate[]>('/page-templates');
21
+ const templates = data ?? [];
22
+
23
+ if (loading) {
24
+ return (
25
+ <div className="flex h-64 items-center justify-center p-4" role="status" aria-live="polite">
26
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
27
+ <span className="sr-only">Loading page templates</span>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <div className="p-4 pr-8">
34
+ {error && (
35
+ <div className="mb-4 flex items-center gap-3 rounded-lg border border-border bg-card p-3">
36
+ <AlertTriangle className="h-5 w-5 shrink-0 text-muted-foreground" />
37
+ <span className="flex-1 text-sm text-foreground">{error}</span>
38
+ <button
39
+ type="button"
40
+ onClick={refetch}
41
+ className="rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-accent"
42
+ >
43
+ Retry
44
+ </button>
45
+ </div>
46
+ )}
47
+
48
+ <div className="mb-4 flex items-center justify-between">
49
+ <div>
50
+ <h1 className="mb-1 text-2xl font-medium text-foreground">Page Templates</h1>
51
+ <p className="text-sm text-muted-foreground">
52
+ {templates.length} saved template{templates.length === 1 ? '' : 's'}
53
+ </p>
54
+ </div>
55
+ <button
56
+ type="button"
57
+ onClick={refetch}
58
+ className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
59
+ >
60
+ <RefreshCw className="h-4 w-4" />
61
+ Refresh
62
+ </button>
63
+ </div>
64
+
65
+ {templates.length === 0 ? (
66
+ <div className="rounded-lg border border-border bg-card p-8 text-center">
67
+ <Layers className="mx-auto mb-3 h-8 w-8 text-muted-foreground" />
68
+ <h2 className="mb-1 text-lg font-medium text-foreground">No page templates yet</h2>
69
+ <p className="mb-4 text-sm text-muted-foreground">
70
+ Built-in templates are seeded by the CMS when the templates API is available.
71
+ </p>
72
+ <button
73
+ type="button"
74
+ onClick={() => onNavigate?.('/saved-sections')}
75
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
76
+ >
77
+ View Saved Sections
78
+ </button>
79
+ </div>
80
+ ) : (
81
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
82
+ {templates.map((template) => (
83
+ <div key={template.id} className="rounded-lg border border-border bg-card p-4">
84
+ <div className="mb-3 flex items-start justify-between gap-3">
85
+ <div>
86
+ <h2 className="text-base font-medium text-foreground">{template.name ?? 'Untitled template'}</h2>
87
+ <p className="mt-1 text-sm text-muted-foreground">{template.description ?? 'No description provided.'}</p>
88
+ </div>
89
+ {template.builtIn && (
90
+ <span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
91
+ Built-in
92
+ </span>
93
+ )}
94
+ </div>
95
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
96
+ <span>{template.category ?? 'content'}</span>
97
+ <span>{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : ''}</span>
98
+ </div>
99
+ </div>
100
+ ))}
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }