@prenta/admin 0.98.0 → 1.0.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 (270) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +17 -28
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/seo-score-bands.test.js +0 -10
  6. package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
  7. package/dist/__tests__/layout/sidebar-posts.test.js +6 -11
  8. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  9. package/dist/__tests__/layout/sidebar-submenu.render.test.js +3 -2
  10. package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
  11. package/dist/__tests__/lib/page-audit-input.test.js +77 -1
  12. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
  13. package/dist/__tests__/lib/page-editor-service-api.test.js +26 -23
  14. package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
  15. package/dist/__tests__/views/page-section-editor.test.js +0 -1
  16. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  17. package/dist/__tests__/views/page-tab.render.test.js +0 -1
  18. package/dist/__tests__/views/page-tab.render.test.js.map +1 -1
  19. package/dist/index.d.ts +1 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/layout/Sidebar.d.ts +3 -4
  24. package/dist/layout/Sidebar.d.ts.map +1 -1
  25. package/dist/layout/Sidebar.js +7 -38
  26. package/dist/layout/Sidebar.js.map +1 -1
  27. package/dist/lib/page-audit-input.d.ts +14 -1
  28. package/dist/lib/page-audit-input.d.ts.map +1 -1
  29. package/dist/lib/page-audit-input.js +19 -0
  30. package/dist/lib/page-audit-input.js.map +1 -1
  31. package/dist/lib/page-editor-service.d.ts +0 -7
  32. package/dist/lib/page-editor-service.d.ts.map +1 -1
  33. package/dist/lib/page-editor-service.js +9 -9
  34. package/dist/lib/page-editor-service.js.map +1 -1
  35. package/dist/prenta-admin.css +1 -1
  36. package/dist/views/PageEditor.d.ts +2 -2
  37. package/dist/views/PageEditor.js +2 -2
  38. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  39. package/dist/views/page-editor/PageSectionEditor.js +9 -15
  40. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  41. package/package.json +4 -4
  42. package/src/AdminRoot.tsx +18 -68
  43. package/src/__tests__/components/seo-score-bands.test.ts +0 -10
  44. package/src/__tests__/layout/sidebar-posts.test.ts +6 -11
  45. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +3 -2
  46. package/src/__tests__/lib/page-audit-input.test.ts +91 -1
  47. package/src/__tests__/lib/page-editor-service-api.test.ts +27 -25
  48. package/src/__tests__/views/page-section-editor.test.tsx +0 -1
  49. package/src/__tests__/views/page-tab.render.test.tsx +0 -1
  50. package/src/index.ts +1 -11
  51. package/src/layout/Sidebar.tsx +6 -41
  52. package/src/lib/page-audit-input.ts +36 -1
  53. package/src/lib/page-editor-service.ts +9 -16
  54. package/src/views/PageEditor.tsx +2 -2
  55. package/src/views/page-editor/PageSectionEditor.tsx +8 -37
  56. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts +0 -2
  57. package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts.map +0 -1
  58. package/dist/__tests__/views/page-builder-block-renderer.render.test.js +0 -48
  59. package/dist/__tests__/views/page-builder-block-renderer.render.test.js.map +0 -1
  60. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts +0 -2
  61. package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts.map +0 -1
  62. package/dist/__tests__/views/page-builder-form-picker.render.test.js +0 -44
  63. package/dist/__tests__/views/page-builder-form-picker.render.test.js.map +0 -1
  64. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +0 -2
  65. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +0 -1
  66. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +0 -121
  67. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +0 -1
  68. package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts +0 -2
  69. package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts.map +0 -1
  70. package/dist/__tests__/views/page-builder-template-seed.render.test.js +0 -48
  71. package/dist/__tests__/views/page-builder-template-seed.render.test.js.map +0 -1
  72. package/dist/__tests__/views/page-templates.render.test.d.ts +0 -2
  73. package/dist/__tests__/views/page-templates.render.test.d.ts.map +0 -1
  74. package/dist/__tests__/views/page-templates.render.test.js +0 -126
  75. package/dist/__tests__/views/page-templates.render.test.js.map +0 -1
  76. package/dist/hooks/useBuilderState.d.ts +0 -49
  77. package/dist/hooks/useBuilderState.d.ts.map +0 -1
  78. package/dist/hooks/useBuilderState.js +0 -238
  79. package/dist/hooks/useBuilderState.js.map +0 -1
  80. package/dist/views/page-builder/AIBlockAssist.d.ts +0 -9
  81. package/dist/views/page-builder/AIBlockAssist.d.ts.map +0 -1
  82. package/dist/views/page-builder/AIBlockAssist.js +0 -40
  83. package/dist/views/page-builder/AIBlockAssist.js.map +0 -1
  84. package/dist/views/page-builder/AIGenerateDialog.d.ts +0 -8
  85. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +0 -1
  86. package/dist/views/page-builder/AIGenerateDialog.js +0 -173
  87. package/dist/views/page-builder/AIGenerateDialog.js.map +0 -1
  88. package/dist/views/page-builder/BlockEditor.d.ts +0 -11
  89. package/dist/views/page-builder/BlockEditor.d.ts.map +0 -1
  90. package/dist/views/page-builder/BlockEditor.js +0 -159
  91. package/dist/views/page-builder/BlockEditor.js.map +0 -1
  92. package/dist/views/page-builder/BlockPicker.d.ts +0 -7
  93. package/dist/views/page-builder/BlockPicker.d.ts.map +0 -1
  94. package/dist/views/page-builder/BlockPicker.js +0 -103
  95. package/dist/views/page-builder/BlockPicker.js.map +0 -1
  96. package/dist/views/page-builder/BottomBar.d.ts +0 -9
  97. package/dist/views/page-builder/BottomBar.d.ts.map +0 -1
  98. package/dist/views/page-builder/BottomBar.js +0 -13
  99. package/dist/views/page-builder/BottomBar.js.map +0 -1
  100. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +0 -20
  101. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +0 -1
  102. package/dist/views/page-builder/BuilderSectionsPanel.js +0 -105
  103. package/dist/views/page-builder/BuilderSectionsPanel.js.map +0 -1
  104. package/dist/views/page-builder/BuilderToolbar.d.ts +0 -21
  105. package/dist/views/page-builder/BuilderToolbar.d.ts.map +0 -1
  106. package/dist/views/page-builder/BuilderToolbar.js +0 -18
  107. package/dist/views/page-builder/BuilderToolbar.js.map +0 -1
  108. package/dist/views/page-builder/ContextPanel.d.ts +0 -20
  109. package/dist/views/page-builder/ContextPanel.d.ts.map +0 -1
  110. package/dist/views/page-builder/ContextPanel.js +0 -43
  111. package/dist/views/page-builder/ContextPanel.js.map +0 -1
  112. package/dist/views/page-builder/DesignScore.d.ts +0 -6
  113. package/dist/views/page-builder/DesignScore.d.ts.map +0 -1
  114. package/dist/views/page-builder/DesignScore.js +0 -93
  115. package/dist/views/page-builder/DesignScore.js.map +0 -1
  116. package/dist/views/page-builder/NodeSettings.d.ts +0 -12
  117. package/dist/views/page-builder/NodeSettings.d.ts.map +0 -1
  118. package/dist/views/page-builder/NodeSettings.js +0 -80
  119. package/dist/views/page-builder/NodeSettings.js.map +0 -1
  120. package/dist/views/page-builder/PageBuilder.d.ts +0 -10
  121. package/dist/views/page-builder/PageBuilder.d.ts.map +0 -1
  122. package/dist/views/page-builder/PageBuilder.js +0 -222
  123. package/dist/views/page-builder/PageBuilder.js.map +0 -1
  124. package/dist/views/page-builder/PageSettings.d.ts +0 -7
  125. package/dist/views/page-builder/PageSettings.d.ts.map +0 -1
  126. package/dist/views/page-builder/PageSettings.js +0 -27
  127. package/dist/views/page-builder/PageSettings.js.map +0 -1
  128. package/dist/views/page-builder/PageTemplates.d.ts +0 -5
  129. package/dist/views/page-builder/PageTemplates.d.ts.map +0 -1
  130. package/dist/views/page-builder/PageTemplates.js +0 -146
  131. package/dist/views/page-builder/PageTemplates.js.map +0 -1
  132. package/dist/views/page-builder/RelationshipField.d.ts +0 -15
  133. package/dist/views/page-builder/RelationshipField.d.ts.map +0 -1
  134. package/dist/views/page-builder/RelationshipField.js +0 -65
  135. package/dist/views/page-builder/RelationshipField.js.map +0 -1
  136. package/dist/views/page-builder/SEOPanel.d.ts +0 -10
  137. package/dist/views/page-builder/SEOPanel.d.ts.map +0 -1
  138. package/dist/views/page-builder/SEOPanel.js +0 -103
  139. package/dist/views/page-builder/SEOPanel.js.map +0 -1
  140. package/dist/views/page-builder/SavedSections.d.ts +0 -6
  141. package/dist/views/page-builder/SavedSections.d.ts.map +0 -1
  142. package/dist/views/page-builder/SavedSections.js +0 -141
  143. package/dist/views/page-builder/SavedSections.js.map +0 -1
  144. package/dist/views/page-builder/TemplatePicker.d.ts +0 -7
  145. package/dist/views/page-builder/TemplatePicker.d.ts.map +0 -1
  146. package/dist/views/page-builder/TemplatePicker.js +0 -68
  147. package/dist/views/page-builder/TemplatePicker.js.map +0 -1
  148. package/dist/views/page-builder/Widgets.d.ts +0 -13
  149. package/dist/views/page-builder/Widgets.d.ts.map +0 -1
  150. package/dist/views/page-builder/Widgets.js +0 -15
  151. package/dist/views/page-builder/Widgets.js.map +0 -1
  152. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +0 -3
  153. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +0 -1
  154. package/dist/views/page-builder/block-renderers/CTAPreview.js +0 -19
  155. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +0 -1
  156. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +0 -3
  157. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +0 -1
  158. package/dist/views/page-builder/block-renderers/CardsPreview.js +0 -22
  159. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +0 -1
  160. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +0 -3
  161. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +0 -1
  162. package/dist/views/page-builder/block-renderers/CodePreview.js +0 -12
  163. package/dist/views/page-builder/block-renderers/CodePreview.js.map +0 -1
  164. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +0 -3
  165. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +0 -1
  166. package/dist/views/page-builder/block-renderers/FAQPreview.js +0 -27
  167. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +0 -1
  168. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +0 -6
  169. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +0 -1
  170. package/dist/views/page-builder/block-renderers/FallbackPreview.js +0 -7
  171. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +0 -1
  172. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +0 -9
  173. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +0 -1
  174. package/dist/views/page-builder/block-renderers/FormPreview.js +0 -52
  175. package/dist/views/page-builder/block-renderers/FormPreview.js.map +0 -1
  176. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +0 -3
  177. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +0 -1
  178. package/dist/views/page-builder/block-renderers/GalleryPreview.js +0 -19
  179. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +0 -1
  180. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +0 -3
  181. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +0 -1
  182. package/dist/views/page-builder/block-renderers/HeroPreview.js +0 -19
  183. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +0 -1
  184. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +0 -3
  185. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +0 -1
  186. package/dist/views/page-builder/block-renderers/ImagePreview.js +0 -17
  187. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +0 -1
  188. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +0 -3
  189. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +0 -1
  190. package/dist/views/page-builder/block-renderers/TextPreview.js +0 -22
  191. package/dist/views/page-builder/block-renderers/TextPreview.js.map +0 -1
  192. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +0 -3
  193. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +0 -1
  194. package/dist/views/page-builder/block-renderers/VideoPreview.js +0 -18
  195. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +0 -1
  196. package/dist/views/page-builder/block-renderers/index.d.ts +0 -9
  197. package/dist/views/page-builder/block-renderers/index.d.ts.map +0 -1
  198. package/dist/views/page-builder/block-renderers/index.js +0 -25
  199. package/dist/views/page-builder/block-renderers/index.js.map +0 -1
  200. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +0 -8
  201. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +0 -1
  202. package/dist/views/page-builder/canvas/BlockRenderer.js +0 -20
  203. package/dist/views/page-builder/canvas/BlockRenderer.js.map +0 -1
  204. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +0 -10
  205. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +0 -1
  206. package/dist/views/page-builder/canvas/BuilderCanvas.js +0 -26
  207. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +0 -1
  208. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +0 -8
  209. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +0 -1
  210. package/dist/views/page-builder/canvas/ColumnRenderer.js +0 -32
  211. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +0 -1
  212. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +0 -8
  213. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +0 -1
  214. package/dist/views/page-builder/canvas/ContainerRenderer.js +0 -29
  215. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +0 -1
  216. package/dist/views/page-builder/canvas/RowRenderer.d.ts +0 -8
  217. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +0 -1
  218. package/dist/views/page-builder/canvas/RowRenderer.js +0 -28
  219. package/dist/views/page-builder/canvas/RowRenderer.js.map +0 -1
  220. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +0 -8
  221. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +0 -1
  222. package/dist/views/page-builder/canvas/SectionRenderer.js +0 -50
  223. package/dist/views/page-builder/canvas/SectionRenderer.js.map +0 -1
  224. package/dist/views/page-builder/canvas/index.d.ts +0 -3
  225. package/dist/views/page-builder/canvas/index.d.ts.map +0 -1
  226. package/dist/views/page-builder/canvas/index.js +0 -2
  227. package/dist/views/page-builder/canvas/index.js.map +0 -1
  228. package/src/__tests__/views/page-builder-block-renderer.render.test.tsx +0 -66
  229. package/src/__tests__/views/page-builder-form-picker.render.test.tsx +0 -54
  230. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +0 -138
  231. package/src/__tests__/views/page-builder-template-seed.render.test.tsx +0 -59
  232. package/src/__tests__/views/page-templates.render.test.tsx +0 -182
  233. package/src/hooks/useBuilderState.ts +0 -361
  234. package/src/views/page-builder/AIBlockAssist.tsx +0 -64
  235. package/src/views/page-builder/AIGenerateDialog.tsx +0 -593
  236. package/src/views/page-builder/BlockEditor.tsx +0 -523
  237. package/src/views/page-builder/BlockPicker.tsx +0 -324
  238. package/src/views/page-builder/BottomBar.tsx +0 -65
  239. package/src/views/page-builder/BuilderSectionsPanel.tsx +0 -304
  240. package/src/views/page-builder/BuilderToolbar.tsx +0 -221
  241. package/src/views/page-builder/ContextPanel.tsx +0 -145
  242. package/src/views/page-builder/DesignScore.tsx +0 -251
  243. package/src/views/page-builder/NodeSettings.tsx +0 -500
  244. package/src/views/page-builder/PageBuilder.tsx +0 -405
  245. package/src/views/page-builder/PageSettings.tsx +0 -162
  246. package/src/views/page-builder/PageTemplates.tsx +0 -435
  247. package/src/views/page-builder/RelationshipField.tsx +0 -113
  248. package/src/views/page-builder/SEOPanel.tsx +0 -464
  249. package/src/views/page-builder/SavedSections.tsx +0 -480
  250. package/src/views/page-builder/TemplatePicker.tsx +0 -197
  251. package/src/views/page-builder/Widgets.tsx +0 -44
  252. package/src/views/page-builder/block-renderers/CTAPreview.tsx +0 -79
  253. package/src/views/page-builder/block-renderers/CardsPreview.tsx +0 -69
  254. package/src/views/page-builder/block-renderers/CodePreview.tsx +0 -46
  255. package/src/views/page-builder/block-renderers/FAQPreview.tsx +0 -87
  256. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +0 -18
  257. package/src/views/page-builder/block-renderers/FormPreview.tsx +0 -117
  258. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +0 -87
  259. package/src/views/page-builder/block-renderers/HeroPreview.tsx +0 -90
  260. package/src/views/page-builder/block-renderers/ImagePreview.tsx +0 -54
  261. package/src/views/page-builder/block-renderers/TextPreview.tsx +0 -81
  262. package/src/views/page-builder/block-renderers/VideoPreview.tsx +0 -73
  263. package/src/views/page-builder/block-renderers/index.ts +0 -34
  264. package/src/views/page-builder/canvas/BlockRenderer.tsx +0 -56
  265. package/src/views/page-builder/canvas/BuilderCanvas.tsx +0 -87
  266. package/src/views/page-builder/canvas/ColumnRenderer.tsx +0 -82
  267. package/src/views/page-builder/canvas/ContainerRenderer.tsx +0 -67
  268. package/src/views/page-builder/canvas/RowRenderer.tsx +0 -68
  269. package/src/views/page-builder/canvas/SectionRenderer.tsx +0 -93
  270. package/src/views/page-builder/canvas/index.ts +0 -2
@@ -1,405 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect, useCallback, useRef } from 'react'
4
- import { Loader2, AlertTriangle } from 'lucide-react'
5
- import { toast } from 'sonner'
6
- import { cmsApi } from '../../lib/api.js'
7
- import { useNavigationBlocker } from '../../router/index.js'
8
- import { ErrorBoundary } from '../../components/ErrorBoundary.js'
9
- import {
10
- useBuilderState,
11
- type BuilderState,
12
- type BuilderActions,
13
- type PageSettings,
14
- } from '../../hooks/useBuilderState.js'
15
- import type { PageNode } from '@prenta/core'
16
- import { BuilderToolbar } from './BuilderToolbar.js'
17
- import { BottomBar } from './BottomBar.js'
18
- import { BuilderCanvas } from './canvas/index.js'
19
- import { BuilderSectionsPanel } from './BuilderSectionsPanel.js'
20
- import { ContextPanel } from './ContextPanel.js'
21
- import { AIGenerateDialog } from './AIGenerateDialog.js'
22
-
23
- export interface PageBuilderProps {
24
- documentId?: string
25
- /** Seed a NEW page from a saved template (`/page-builder/new?template=<id>`). */
26
- templateId?: string
27
- collectionSlug: string
28
- config: any
29
- onNavigate: (path: string) => void
30
- }
31
-
32
- type DocumentStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED'
33
-
34
- const DEVICE_WIDTHS = {
35
- desktop: '100%',
36
- tablet: '768px',
37
- mobile: '375px',
38
- } as const
39
-
40
- const SECTIONS_PANEL_KEY = 'prenta-builder-sections-panel'
41
-
42
- /**
43
- * Left sections panel default: open on wide viewports (≥1280px), collapsed
44
- * below so the canvas keeps room. An explicit user choice, persisted in
45
- * localStorage, always wins over the breakpoint default.
46
- */
47
- function initialSectionsCollapsed(): boolean {
48
- try {
49
- const stored = localStorage.getItem(SECTIONS_PANEL_KEY)
50
- if (stored === 'open') return false
51
- if (stored === 'collapsed') return true
52
- } catch {
53
- /* localStorage unavailable — fall through to the breakpoint default */
54
- }
55
- if (typeof window !== 'undefined') {
56
- return window.innerWidth < 1280
57
- }
58
- return false
59
- }
60
-
61
- export function PageBuilder({
62
- documentId,
63
- templateId,
64
- collectionSlug,
65
- config,
66
- onNavigate,
67
- }: PageBuilderProps) {
68
- const [loading, setLoading] = useState(!!documentId)
69
- const [loadError, setLoadError] = useState<string | null>(null)
70
- const [saving, setSaving] = useState(false)
71
- const [status, setStatus] = useState<DocumentStatus>('DRAFT')
72
- const [aiDialogOpen, setAiDialogOpen] = useState(false)
73
- const [sectionsCollapsed, setSectionsCollapsed] = useState(initialSectionsCollapsed)
74
- const canvasRef = useRef<HTMLDivElement>(null)
75
-
76
- const builder = useBuilderState()
77
- const {
78
- tree,
79
- pageSettings,
80
- deviceMode,
81
- dirty,
82
- canUndo,
83
- canRedo,
84
- selectedNodeId,
85
- selectedNode,
86
- activeTab,
87
- showGridOverlay,
88
- selectNode,
89
- setDeviceMode,
90
- setActiveTab,
91
- setPageSettings,
92
- setShowGridOverlay,
93
- addSection,
94
- addRowToSection,
95
- addBlockToColumn,
96
- addNodeAtId,
97
- removeNodeById,
98
- moveNodeById,
99
- updateSettings,
100
- updateBlock,
101
- duplicateNode,
102
- moveNodeUp,
103
- moveNodeDown,
104
- undo,
105
- redo,
106
- markClean,
107
- replaceTree,
108
- } = builder
109
-
110
- useEffect(() => {
111
- if (!documentId) {
112
- // A new page can be seeded from a saved template ("Use template" on
113
- // the Templates grid). Load the template tree, then hand the canvas
114
- // over as a dirty, unsaved draft.
115
- if (templateId) {
116
- let cancelledNew = false
117
- setLoading(true)
118
- cmsApi<{ name?: string; tree?: PageNode }>(`/page-templates/${templateId}`).then((res) => {
119
- if (cancelledNew) return
120
- if (res.error) {
121
- toast.error(`Couldn't load template: ${res.error}`)
122
- } else if (res.data?.tree) {
123
- replaceTree(res.data.tree)
124
- }
125
- setLoading(false)
126
- })
127
- return () => {
128
- cancelledNew = true
129
- }
130
- }
131
- setLoading(false)
132
- return
133
- }
134
-
135
- let cancelled = false
136
-
137
- async function loadDocument() {
138
- const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`)
139
-
140
- if (cancelled) return
141
-
142
- if (res.error) {
143
- setLoadError(res.error)
144
- setLoading(false)
145
- return
146
- }
147
-
148
- const doc = res.data
149
- if (doc?.layout) {
150
- replaceTree(doc.layout as PageNode)
151
- markClean()
152
- }
153
- if (doc?.pageSettings) {
154
- setPageSettings(doc.pageSettings)
155
- }
156
- if (doc?.status) {
157
- setStatus(doc.status as DocumentStatus)
158
- }
159
- if (doc?.title) {
160
- setPageSettings({ title: doc.title })
161
- }
162
-
163
- setLoading(false)
164
- }
165
-
166
- loadDocument()
167
- return () => {
168
- cancelled = true
169
- }
170
- // eslint-disable-next-line react-hooks/exhaustive-deps -- replaceTree/setPageSettings are stable builder actions
171
- }, [documentId, templateId, collectionSlug])
172
-
173
- // Warn before discarding unsaved builder changes on navigation / unload.
174
- useNavigationBlocker(dirty)
175
-
176
- const handleSave = useCallback(async () => {
177
- setSaving(true)
178
- const body = JSON.stringify({
179
- layout: tree,
180
- pageSettings,
181
- title: pageSettings.title,
182
- slug: pageSettings.slug,
183
- })
184
-
185
- const endpoint = documentId
186
- ? `/collections/${collectionSlug}/${documentId}`
187
- : `/collections/${collectionSlug}`
188
- const method = documentId ? 'PUT' : 'POST'
189
-
190
- const res = await cmsApi(endpoint, { method, body })
191
- setSaving(false)
192
-
193
- if (res.error) {
194
- toast.error(res.error)
195
- } else {
196
- markClean()
197
- toast.success('Page saved')
198
- if (!documentId && (res.data as any)?.id) {
199
- onNavigate(`/pages/${(res.data as any).id}/builder`)
200
- }
201
- }
202
- }, [tree, pageSettings, documentId, collectionSlug, markClean, onNavigate])
203
-
204
- const handlePublish = useCallback(async () => {
205
- setSaving(true)
206
- const body = JSON.stringify({
207
- layout: tree,
208
- pageSettings,
209
- title: pageSettings.title,
210
- slug: pageSettings.slug,
211
- status: 'PUBLISHED',
212
- })
213
-
214
- const endpoint = documentId
215
- ? `/collections/${collectionSlug}/${documentId}`
216
- : `/collections/${collectionSlug}`
217
- const method = documentId ? 'PUT' : 'POST'
218
-
219
- const res = await cmsApi(endpoint, { method, body })
220
- setSaving(false)
221
-
222
- if (res.error) {
223
- toast.error(res.error)
224
- } else {
225
- markClean()
226
- setStatus('PUBLISHED')
227
- toast.success('Page published')
228
- if (!documentId && (res.data as any)?.id) {
229
- onNavigate(`/pages/${(res.data as any).id}/builder`)
230
- }
231
- }
232
- }, [tree, pageSettings, documentId, collectionSlug, markClean, onNavigate])
233
-
234
- const handleAIAccept = useCallback(
235
- (generatedTree: PageNode) => {
236
- replaceTree(generatedTree)
237
- },
238
- [replaceTree],
239
- )
240
-
241
- const toggleSectionsPanel = useCallback(() => {
242
- setSectionsCollapsed((prev) => {
243
- const next = !prev
244
- try {
245
- localStorage.setItem(SECTIONS_PANEL_KEY, next ? 'collapsed' : 'open')
246
- } catch {
247
- /* localStorage unavailable — the choice just won't persist */
248
- }
249
- return next
250
- })
251
- }, [])
252
-
253
- // Selecting from the left panel highlights the section on the canvas (the
254
- // canvas already outlines the selected node) and opens its settings in the
255
- // right-hand context panel, then scrolls the section into view.
256
- const handlePanelSelect = useCallback(
257
- (id: string) => {
258
- selectNode(id)
259
- canvasRef.current
260
- ?.querySelector(`[data-node-id="${CSS.escape(id)}"]`)
261
- ?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
262
- },
263
- [selectNode],
264
- )
265
-
266
- const handlePanelReorder = useCallback(
267
- (id: string, toIndex: number) => {
268
- moveNodeById(id, tree.id, toIndex)
269
- },
270
- [moveNodeById, tree.id],
271
- )
272
-
273
- if (loading) {
274
- return (
275
- <div className="bg-background flex h-full items-center justify-center">
276
- <Loader2 className="text-muted-foreground animate-spin" size={24} />
277
- </div>
278
- )
279
- }
280
-
281
- if (loadError) {
282
- return (
283
- <div className="bg-background flex h-full items-center justify-center">
284
- <div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
285
- <AlertTriangle className="text-destructive shrink-0" size={20} />
286
- <div>
287
- <p className="text-foreground text-sm font-medium">Failed to load page</p>
288
- <p className="text-muted-foreground mt-1 text-sm">{loadError}</p>
289
- </div>
290
- </div>
291
- </div>
292
- )
293
- }
294
-
295
- const canvasWidth = DEVICE_WIDTHS[deviceMode]
296
- const isConstrained = deviceMode !== 'desktop'
297
-
298
- // `pageBuilder.previewTheme` lets consumers preview blocks with their
299
- // brand fonts/colors/radii without baking them into the admin's tokens.
300
- // We scope the CSS variables to the canvas wrapper so they never leak
301
- // into the rest of the admin chrome.
302
- const previewTheme = config?.pageBuilder?.previewTheme ?? {}
303
- const previewStyle: Record<string, string> = {}
304
- if (previewTheme.fontFamily) previewStyle['--preview-font-family'] = previewTheme.fontFamily
305
- if (previewTheme.headingFontFamily)
306
- previewStyle['--preview-heading-font-family'] = previewTheme.headingFontFamily
307
- if (previewTheme.primaryColor) previewStyle['--preview-primary'] = previewTheme.primaryColor
308
- if (previewTheme.secondaryColor) previewStyle['--preview-secondary'] = previewTheme.secondaryColor
309
- if (previewTheme.borderRadius) previewStyle['--preview-radius'] = previewTheme.borderRadius
310
- const canvasShellStyle: React.CSSProperties = {
311
- ...previewStyle,
312
- fontFamily: previewTheme.fontFamily ?? undefined,
313
- }
314
-
315
- return (
316
- <ErrorBoundary>
317
- <div className="bg-background flex h-full flex-col overflow-hidden">
318
- <BuilderToolbar
319
- collectionSlug={collectionSlug}
320
- pageSettings={pageSettings}
321
- status={status}
322
- dirty={dirty}
323
- saving={saving}
324
- canUndo={canUndo}
325
- canRedo={canRedo}
326
- deviceMode={deviceMode}
327
- onNavigate={onNavigate}
328
- onTitleChange={(title) => setPageSettings({ title })}
329
- onUndo={undo}
330
- onRedo={redo}
331
- onDeviceMode={setDeviceMode}
332
- onSave={handleSave}
333
- onPublish={handlePublish}
334
- onOpenAI={() => setAiDialogOpen(true)}
335
- />
336
-
337
- <div className="flex flex-1 overflow-hidden">
338
- {/* Sections panel (design parity: sections list left of the canvas) */}
339
- <BuilderSectionsPanel
340
- tree={tree}
341
- selectedNodeId={selectedNodeId}
342
- collapsed={sectionsCollapsed}
343
- onToggleCollapsed={toggleSectionsPanel}
344
- onSelect={handlePanelSelect}
345
- onReorder={handlePanelReorder}
346
- onAddSection={addSection}
347
- />
348
-
349
- {/* Canvas area */}
350
- <div ref={canvasRef} className="bg-muted flex-1 overflow-auto">
351
- <div
352
- className="mx-auto h-full max-w-full transition-all duration-200"
353
- style={{ width: canvasWidth }}
354
- >
355
- <div
356
- className={`bg-background page-builder-canvas-shell h-full ${isConstrained ? 'border-border border-x shadow-lg' : ''}`}
357
- style={canvasShellStyle}
358
- >
359
- <BuilderCanvas
360
- tree={tree}
361
- selectedNodeId={selectedNodeId}
362
- showGridOverlay={showGridOverlay}
363
- deviceMode={deviceMode}
364
- onSelectNode={selectNode}
365
- />
366
- </div>
367
- </div>
368
- </div>
369
-
370
- {/* Context panel */}
371
- <div className="border-border bg-card w-[35%] max-w-[420px] min-w-[280px] overflow-y-auto border-l">
372
- <ContextPanel
373
- activeTab={activeTab}
374
- onTabChange={setActiveTab}
375
- selectedNode={selectedNode}
376
- tree={tree}
377
- pageSettings={pageSettings}
378
- onUpdateSettings={updateSettings}
379
- onUpdateBlock={updateBlock}
380
- onRemoveNode={removeNodeById}
381
- onDuplicateNode={duplicateNode}
382
- onMoveNodeUp={moveNodeUp}
383
- onMoveNodeDown={moveNodeDown}
384
- onPageSettingsChange={setPageSettings}
385
- onAddRow={addRowToSection}
386
- config={config}
387
- />
388
- </div>
389
- </div>
390
-
391
- <BottomBar
392
- deviceMode={deviceMode}
393
- showGridOverlay={showGridOverlay}
394
- onAddSection={addSection}
395
- onToggleGrid={setShowGridOverlay}
396
- />
397
- </div>
398
- <AIGenerateDialog
399
- open={aiDialogOpen}
400
- onClose={() => setAiDialogOpen(false)}
401
- onAccept={handleAIAccept}
402
- />
403
- </ErrorBoundary>
404
- )
405
- }
@@ -1,162 +0,0 @@
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 =
15
- 'text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2'
16
-
17
- const SCHEMA_TYPES = [
18
- 'Article',
19
- 'LocalBusiness',
20
- 'WebPage',
21
- 'Product',
22
- 'FAQPage',
23
- 'Organization',
24
- 'Event',
25
- 'Recipe',
26
- 'HowTo',
27
- 'BreadcrumbList',
28
- ]
29
-
30
- export function PageSettingsEditor({ settings, onChange }: PageSettingsProps) {
31
- const update = useCallback(
32
- (key: keyof PageSettings, value: string) => {
33
- onChange({ [key]: value })
34
- },
35
- [onChange],
36
- )
37
-
38
- const metaTitleLength = (settings.metaTitle ?? '').length
39
- const metaDescLength = (settings.metaDescription ?? '').length
40
-
41
- return (
42
- <div className="space-y-4 p-4">
43
- <p className={SECTION_HEADING_CLASS}>General</p>
44
-
45
- <div>
46
- <label className={LABEL_CLASS}>Title</label>
47
- <input
48
- type="text"
49
- value={settings.title}
50
- onChange={(e) => update('title', e.target.value)}
51
- placeholder="Page title"
52
- className={INPUT_CLASS}
53
- />
54
- </div>
55
-
56
- <div>
57
- <label className={LABEL_CLASS}>Slug</label>
58
- <input
59
- type="text"
60
- value={settings.slug}
61
- onChange={(e) => update('slug', e.target.value)}
62
- placeholder="/page-slug"
63
- className={INPUT_CLASS}
64
- />
65
- </div>
66
-
67
- <div>
68
- <label className={LABEL_CLASS}>Template</label>
69
- <input
70
- type="text"
71
- value={settings.template ?? ''}
72
- onChange={(e) => update('template', e.target.value)}
73
- placeholder="default"
74
- className={INPUT_CLASS}
75
- readOnly
76
- />
77
- </div>
78
-
79
- <p className={SECTION_HEADING_CLASS}>SEO</p>
80
-
81
- <div>
82
- <div className="mb-1 flex items-center justify-between">
83
- <label className="text-foreground text-sm font-medium">Meta Title</label>
84
- <span
85
- className={`text-xs ${
86
- metaTitleLength > 60 ? 'text-destructive' : 'text-muted-foreground'
87
- }`}
88
- >
89
- {metaTitleLength}/60
90
- </span>
91
- </div>
92
- <input
93
- type="text"
94
- value={settings.metaTitle ?? ''}
95
- onChange={(e) => update('metaTitle', e.target.value)}
96
- placeholder="SEO title"
97
- maxLength={100}
98
- className={INPUT_CLASS}
99
- />
100
- </div>
101
-
102
- <div>
103
- <div className="mb-1 flex items-center justify-between">
104
- <label className="text-foreground text-sm font-medium">Meta Description</label>
105
- <span
106
- className={`text-xs ${
107
- metaDescLength > 160 ? 'text-destructive' : 'text-muted-foreground'
108
- }`}
109
- >
110
- {metaDescLength}/160
111
- </span>
112
- </div>
113
- <textarea
114
- value={settings.metaDescription ?? ''}
115
- onChange={(e) => update('metaDescription', e.target.value)}
116
- placeholder="Brief description for search engines"
117
- rows={3}
118
- maxLength={250}
119
- className={`${INPUT_CLASS} resize-y`}
120
- />
121
- </div>
122
-
123
- <div>
124
- <label className={LABEL_CLASS}>OG Image</label>
125
- <input
126
- type="text"
127
- value={settings.ogImage ?? ''}
128
- onChange={(e) => update('ogImage', e.target.value)}
129
- placeholder="https://example.com/image.jpg"
130
- className={INPUT_CLASS}
131
- />
132
- </div>
133
-
134
- <div>
135
- <label className={LABEL_CLASS}>Focus Keyphrase</label>
136
- <input
137
- type="text"
138
- value={settings.focusKeyphrase ?? ''}
139
- onChange={(e) => update('focusKeyphrase', e.target.value)}
140
- placeholder="primary keyword"
141
- className={INPUT_CLASS}
142
- />
143
- </div>
144
-
145
- <div>
146
- <label className={LABEL_CLASS}>Schema Type</label>
147
- <select
148
- value={settings.schemaType ?? ''}
149
- onChange={(e) => update('schemaType', e.target.value)}
150
- className={INPUT_CLASS}
151
- >
152
- <option value="">Select schema type...</option>
153
- {SCHEMA_TYPES.map((type) => (
154
- <option key={type} value={type}>
155
- {type}
156
- </option>
157
- ))}
158
- </select>
159
- </div>
160
- </div>
161
- )
162
- }