@actuate-media/cms-admin 0.6.0 → 0.7.0

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 (188) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +13 -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/page-builder/AIBlockAssist.d.ts +9 -0
  19. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  20. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  21. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  22. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  23. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  24. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  25. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  26. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  27. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  28. package/dist/views/page-builder/BlockEditor.js +67 -0
  29. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  30. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  31. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  32. package/dist/views/page-builder/BlockPicker.js +102 -0
  33. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  34. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  35. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  36. package/dist/views/page-builder/BottomBar.js +13 -0
  37. package/dist/views/page-builder/BottomBar.js.map +1 -0
  38. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  39. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  40. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  41. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  42. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  43. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  44. package/dist/views/page-builder/ContextPanel.js +40 -0
  45. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  46. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  47. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  48. package/dist/views/page-builder/DesignScore.js +93 -0
  49. package/dist/views/page-builder/DesignScore.js.map +1 -0
  50. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  51. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  52. package/dist/views/page-builder/NodeSettings.js +80 -0
  53. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  54. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  55. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  56. package/dist/views/page-builder/PageBuilder.js +126 -0
  57. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  58. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  59. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  60. package/dist/views/page-builder/PageSettings.js +27 -0
  61. package/dist/views/page-builder/PageSettings.js.map +1 -0
  62. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  63. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  64. package/dist/views/page-builder/SEOPanel.js +105 -0
  65. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  66. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  67. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  68. package/dist/views/page-builder/SavedSections.js +145 -0
  69. package/dist/views/page-builder/SavedSections.js.map +1 -0
  70. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  71. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  72. package/dist/views/page-builder/TemplatePicker.js +68 -0
  73. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  74. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  75. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  76. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  77. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  78. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  79. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  80. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  81. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  82. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  83. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  84. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  85. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  86. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  87. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  88. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  89. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  90. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  91. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  92. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  93. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  94. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  95. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  96. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  97. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  98. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  99. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  100. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  101. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  102. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  103. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  104. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  105. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  106. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  107. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  108. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  109. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  110. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  111. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  112. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  113. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  114. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  115. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  116. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  117. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  118. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  119. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  120. package/dist/views/page-builder/block-renderers/index.js +25 -0
  121. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  122. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  123. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  124. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  125. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  126. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  127. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  128. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  129. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  130. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  131. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  132. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  133. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  134. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  135. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  136. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  137. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  138. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  139. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  140. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  141. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  142. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  143. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  144. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  145. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  146. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  147. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  148. package/dist/views/page-builder/canvas/index.js +2 -0
  149. package/dist/views/page-builder/canvas/index.js.map +1 -0
  150. package/package.json +3 -2
  151. package/src/AdminRoot.tsx +16 -0
  152. package/src/components/ErrorBoundary.tsx +3 -3
  153. package/src/hooks/useBuilderState.ts +328 -0
  154. package/src/index.ts +4 -0
  155. package/src/layout/Sidebar.tsx +5 -0
  156. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  157. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  158. package/src/views/page-builder/BlockEditor.tsx +352 -0
  159. package/src/views/page-builder/BlockPicker.tsx +338 -0
  160. package/src/views/page-builder/BottomBar.tsx +64 -0
  161. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  162. package/src/views/page-builder/ContextPanel.tsx +145 -0
  163. package/src/views/page-builder/DesignScore.tsx +258 -0
  164. package/src/views/page-builder/NodeSettings.tsx +515 -0
  165. package/src/views/page-builder/PageBuilder.tsx +288 -0
  166. package/src/views/page-builder/PageSettings.tsx +161 -0
  167. package/src/views/page-builder/SEOPanel.tsx +485 -0
  168. package/src/views/page-builder/SavedSections.tsx +486 -0
  169. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  170. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  171. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  172. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  173. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  174. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  175. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  176. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  177. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  178. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  179. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  180. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  181. package/src/views/page-builder/block-renderers/index.ts +34 -0
  182. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  183. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  184. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  185. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  186. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  187. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  188. 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
+ }