@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,64 @@
1
+ 'use client';
2
+
3
+ import { Plus, Grid3X3 } from 'lucide-react';
4
+ import * as Switch from '@radix-ui/react-switch';
5
+ import type { DeviceMode } from '../../hooks/useBuilderState.js';
6
+
7
+ export interface BottomBarProps {
8
+ deviceMode: DeviceMode;
9
+ showGridOverlay: boolean;
10
+ onAddSection: () => void;
11
+ onToggleGrid: (show: boolean) => void;
12
+ }
13
+
14
+ const DEVICE_LABELS: Record<DeviceMode, string> = {
15
+ desktop: '100%',
16
+ tablet: '768px',
17
+ mobile: '375px',
18
+ };
19
+
20
+ export function BottomBar({
21
+ deviceMode,
22
+ showGridOverlay,
23
+ onAddSection,
24
+ onToggleGrid,
25
+ }: BottomBarProps) {
26
+ return (
27
+ <div className="h-10 bg-card border-t border-border flex items-center px-4 gap-4 shrink-0" role="toolbar" aria-label="Builder actions">
28
+ {/* Add Section */}
29
+ <button
30
+ onClick={onAddSection}
31
+ className="flex items-center gap-1.5 px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
32
+ aria-label="Add a new section"
33
+ >
34
+ <Plus size={14} />
35
+ <span>Add Section</span>
36
+ </button>
37
+
38
+ {/* Divider */}
39
+ <div className="w-px h-5 bg-border" />
40
+
41
+ {/* Grid overlay toggle */}
42
+ <label className="flex items-center gap-2 cursor-pointer">
43
+ <Grid3X3 size={14} className="text-muted-foreground" />
44
+ <Switch.Root
45
+ checked={showGridOverlay}
46
+ onCheckedChange={onToggleGrid}
47
+ className="w-8 h-[18px] bg-input-background rounded-full relative data-[state=checked]:bg-primary transition-colors"
48
+ aria-label="Toggle grid overlay"
49
+ >
50
+ <Switch.Thumb className="block w-3.5 h-3.5 bg-background rounded-full shadow-sm transition-transform translate-x-0.5 data-[state=checked]:translate-x-[14px]" />
51
+ </Switch.Root>
52
+ <span className="text-xs text-muted-foreground">Grid</span>
53
+ </label>
54
+
55
+ {/* Spacer */}
56
+ <div className="flex-1" />
57
+
58
+ {/* Zoom / device width display */}
59
+ <span className="text-xs text-muted-foreground tabular-nums">
60
+ {DEVICE_LABELS[deviceMode]}
61
+ </span>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,218 @@
1
+ 'use client';
2
+
3
+ import {
4
+ ChevronLeft,
5
+ Undo2,
6
+ Redo2,
7
+ Monitor,
8
+ Tablet,
9
+ Smartphone,
10
+ Loader2,
11
+ Sparkles,
12
+ } from 'lucide-react';
13
+ import type { DeviceMode, PageSettings } from '../../hooks/useBuilderState.js';
14
+
15
+ export interface BuilderToolbarProps {
16
+ collectionSlug: string;
17
+ pageSettings: PageSettings;
18
+ status: 'DRAFT' | 'PUBLISHED' | 'SCHEDULED';
19
+ dirty: boolean;
20
+ saving: boolean;
21
+ canUndo: boolean;
22
+ canRedo: boolean;
23
+ deviceMode: DeviceMode;
24
+ onNavigate: (path: string) => void;
25
+ onTitleChange: (title: string) => void;
26
+ onUndo: () => void;
27
+ onRedo: () => void;
28
+ onDeviceMode: (mode: DeviceMode) => void;
29
+ onSave: () => void;
30
+ onPublish: () => void;
31
+ onOpenAI?: () => void;
32
+ }
33
+
34
+ const STATUS_STYLES: Record<string, string> = {
35
+ DRAFT: 'bg-muted text-muted-foreground',
36
+ PUBLISHED: 'bg-primary/10 text-primary',
37
+ SCHEDULED: 'bg-accent text-accent-foreground',
38
+ };
39
+
40
+ export function BuilderToolbar({
41
+ collectionSlug,
42
+ pageSettings,
43
+ status,
44
+ dirty,
45
+ saving,
46
+ canUndo,
47
+ canRedo,
48
+ deviceMode,
49
+ onNavigate,
50
+ onTitleChange,
51
+ onUndo,
52
+ onRedo,
53
+ onDeviceMode,
54
+ onSave,
55
+ onPublish,
56
+ onOpenAI,
57
+ }: BuilderToolbarProps) {
58
+ const collectionLabel =
59
+ collectionSlug.charAt(0).toUpperCase() + collectionSlug.slice(1);
60
+
61
+ return (
62
+ <div className="h-14 bg-card border-b border-border flex items-center px-4 gap-3 shrink-0" role="toolbar" aria-label="Page builder toolbar">
63
+ {/* Back button */}
64
+ <button
65
+ onClick={() => onNavigate(`/collections/${collectionSlug}`)}
66
+ className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
67
+ aria-label={`Back to ${collectionLabel}`}
68
+ >
69
+ <ChevronLeft size={16} />
70
+ <span className="hidden sm:inline">{collectionLabel}</span>
71
+ </button>
72
+
73
+ {/* Divider */}
74
+ <div className="w-px h-6 bg-border" />
75
+
76
+ {/* Page title */}
77
+ <input
78
+ type="text"
79
+ value={pageSettings.title}
80
+ onChange={(e) => onTitleChange(e.target.value)}
81
+ placeholder="Untitled Page"
82
+ className="flex-1 min-w-0 text-sm font-medium text-foreground bg-transparent border-none outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm placeholder:text-muted-foreground"
83
+ aria-label="Page title"
84
+ />
85
+
86
+ {/* Status badge */}
87
+ <span
88
+ className={`text-xs font-medium px-2 py-0.5 rounded-md whitespace-nowrap ${STATUS_STYLES[status] ?? STATUS_STYLES.DRAFT}`}
89
+ >
90
+ {status}
91
+ </span>
92
+
93
+ {/* Unsaved indicator */}
94
+ {dirty && !saving && (
95
+ <span
96
+ className="w-2 h-2 rounded-full bg-destructive shrink-0"
97
+ title="Unsaved changes"
98
+ aria-label="Unsaved changes"
99
+ />
100
+ )}
101
+
102
+ {/* Divider */}
103
+ <div className="w-px h-6 bg-border" />
104
+
105
+ {/* Undo / Redo */}
106
+ <div className="flex items-center gap-1">
107
+ <button
108
+ onClick={onUndo}
109
+ disabled={!canUndo}
110
+ className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 disabled:pointer-events-none transition-colors"
111
+ aria-label="Undo"
112
+ >
113
+ <Undo2 size={16} />
114
+ </button>
115
+ <button
116
+ onClick={onRedo}
117
+ disabled={!canRedo}
118
+ className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 disabled:pointer-events-none transition-colors"
119
+ aria-label="Redo"
120
+ >
121
+ <Redo2 size={16} />
122
+ </button>
123
+ </div>
124
+
125
+ {/* Divider */}
126
+ <div className="w-px h-6 bg-border" />
127
+
128
+ {/* Device toggle */}
129
+ <div className="flex items-center gap-0.5 bg-muted rounded-md p-0.5">
130
+ <DeviceButton
131
+ active={deviceMode === 'desktop'}
132
+ onClick={() => onDeviceMode('desktop')}
133
+ label="Desktop view"
134
+ >
135
+ <Monitor size={16} />
136
+ </DeviceButton>
137
+ <DeviceButton
138
+ active={deviceMode === 'tablet'}
139
+ onClick={() => onDeviceMode('tablet')}
140
+ label="Tablet view"
141
+ >
142
+ <Tablet size={16} />
143
+ </DeviceButton>
144
+ <DeviceButton
145
+ active={deviceMode === 'mobile'}
146
+ onClick={() => onDeviceMode('mobile')}
147
+ label="Mobile view"
148
+ >
149
+ <Smartphone size={16} />
150
+ </DeviceButton>
151
+ </div>
152
+
153
+ {/* Divider */}
154
+ <div className="w-px h-6 bg-border" />
155
+
156
+ {/* AI Generate */}
157
+ {onOpenAI && (
158
+ <>
159
+ <button
160
+ onClick={onOpenAI}
161
+ className="flex items-center gap-1.5 px-2.5 py-1.5 text-sm font-medium text-primary bg-primary/10 rounded-md hover:bg-primary/20 transition-colors"
162
+ aria-label="Generate page with AI"
163
+ >
164
+ <Sparkles size={14} />
165
+ <span className="hidden sm:inline">AI</span>
166
+ </button>
167
+ <div className="w-px h-6 bg-border" />
168
+ </>
169
+ )}
170
+
171
+ {/* Save / Publish */}
172
+ <div className="flex items-center gap-2">
173
+ <button
174
+ onClick={onSave}
175
+ disabled={saving || !dirty}
176
+ className="bg-muted text-muted-foreground rounded-md px-3 py-1.5 text-sm font-medium hover:bg-muted/80 disabled:opacity-50 disabled:pointer-events-none transition-colors flex items-center gap-1.5"
177
+ >
178
+ {saving && <Loader2 size={14} className="animate-spin" />}
179
+ {saving ? 'Saving...' : 'Save'}
180
+ </button>
181
+ <button
182
+ onClick={onPublish}
183
+ disabled={saving}
184
+ className="bg-primary text-primary-foreground rounded-md px-3 py-1.5 text-sm font-medium hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none transition-colors"
185
+ >
186
+ Publish
187
+ </button>
188
+ </div>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ function DeviceButton({
194
+ active,
195
+ onClick,
196
+ label,
197
+ children,
198
+ }: {
199
+ active: boolean;
200
+ onClick: () => void;
201
+ label: string;
202
+ children: React.ReactNode;
203
+ }) {
204
+ return (
205
+ <button
206
+ onClick={onClick}
207
+ className={`p-1.5 rounded-md transition-colors ${
208
+ active
209
+ ? 'bg-background text-foreground shadow-sm'
210
+ : 'text-muted-foreground hover:text-foreground'
211
+ }`}
212
+ aria-label={label}
213
+ aria-pressed={active}
214
+ >
215
+ {children}
216
+ </button>
217
+ );
218
+ }
@@ -0,0 +1,145 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import * as Tabs from '@radix-ui/react-tabs';
5
+ import { Blocks, Settings2, FileText, Search, Palette } from 'lucide-react';
6
+ import type { BuilderNode, PageNode } from '@actuate-media/cms-core';
7
+ import { BlockEditor } from './BlockEditor.js';
8
+ import { NodeSettings } from './NodeSettings.js';
9
+ import { PageSettingsEditor } from './PageSettings.js';
10
+ import { BuilderSEOPanel } from './SEOPanel.js';
11
+ import { DesignScorePanel } from './DesignScore.js';
12
+ import type { PageSettings } from '../../hooks/useBuilderState.js';
13
+
14
+ export interface ContextPanelProps {
15
+ activeTab: 'block' | 'node' | 'page' | 'seo' | 'design';
16
+ onTabChange: (tab: 'block' | 'node' | 'page' | 'seo' | 'design') => void;
17
+ selectedNode: BuilderNode | null;
18
+ tree: PageNode;
19
+ pageSettings: PageSettings;
20
+ onUpdateSettings: (id: string, settings: Record<string, unknown>) => void;
21
+ onUpdateBlock: (id: string, data: Record<string, unknown>) => void;
22
+ onRemoveNode: (id: string) => void;
23
+ onDuplicateNode: (id: string) => void;
24
+ onMoveNodeUp: (id: string) => void;
25
+ onMoveNodeDown: (id: string) => void;
26
+ onPageSettingsChange: (settings: Partial<PageSettings>) => void;
27
+ onAddRow: (sectionId: string) => void;
28
+ config: any;
29
+ }
30
+
31
+ type TabDef = {
32
+ value: 'block' | 'node' | 'page' | 'seo' | 'design';
33
+ label: string;
34
+ icon: typeof Blocks;
35
+ };
36
+
37
+ const TABS: TabDef[] = [
38
+ { value: 'block', label: 'Block', icon: Blocks },
39
+ { value: 'node', label: 'Node', icon: Settings2 },
40
+ { value: 'page', label: 'Page', icon: FileText },
41
+ { value: 'seo', label: 'SEO', icon: Search },
42
+ { value: 'design', label: 'Design', icon: Palette },
43
+ ];
44
+
45
+ export function ContextPanel({
46
+ activeTab,
47
+ onTabChange,
48
+ selectedNode,
49
+ tree,
50
+ pageSettings,
51
+ onUpdateSettings,
52
+ onUpdateBlock,
53
+ onRemoveNode,
54
+ onDuplicateNode,
55
+ onMoveNodeUp,
56
+ onMoveNodeDown,
57
+ onPageSettingsChange,
58
+ onAddRow,
59
+ config,
60
+ }: ContextPanelProps) {
61
+ const availableTabs = useMemo(() => {
62
+ if (!selectedNode) return TABS.filter((t) => t.value !== 'block' && t.value !== 'node');
63
+ if (selectedNode.type === 'block') return TABS.filter((t) => t.value !== 'node');
64
+ return TABS.filter((t) => t.value !== 'block');
65
+ }, [selectedNode]);
66
+
67
+ const effectiveTab = useMemo(() => {
68
+ if (availableTabs.some((t) => t.value === activeTab)) return activeTab;
69
+ if (selectedNode?.type === 'block') return 'block';
70
+ if (selectedNode) return 'node';
71
+ return 'page';
72
+ }, [activeTab, availableTabs, selectedNode]);
73
+
74
+ return (
75
+ <div className="w-full h-full bg-card overflow-y-auto">
76
+ <Tabs.Root
77
+ value={effectiveTab}
78
+ onValueChange={(value) => onTabChange(value as ContextPanelProps['activeTab'])}
79
+ >
80
+ <Tabs.List className="flex border-b border-border bg-muted/30">
81
+ {availableTabs.map((tab) => {
82
+ const Icon = tab.icon;
83
+ return (
84
+ <Tabs.Trigger
85
+ key={tab.value}
86
+ value={tab.value}
87
+ className="flex-1 flex items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[state=active]:text-foreground data-[state=active]:border-b-2 data-[state=active]:border-primary data-[state=active]:bg-background"
88
+ >
89
+ <Icon size={14} />
90
+ <span>{tab.label}</span>
91
+ </Tabs.Trigger>
92
+ );
93
+ })}
94
+ </Tabs.List>
95
+
96
+ <Tabs.Content value="block">
97
+ {selectedNode?.type === 'block' && (
98
+ <BlockEditor
99
+ node={selectedNode}
100
+ onUpdateBlock={onUpdateBlock}
101
+ onUpdateSettings={onUpdateSettings}
102
+ onRemoveNode={onRemoveNode}
103
+ onDuplicateNode={onDuplicateNode}
104
+ config={config}
105
+ />
106
+ )}
107
+ </Tabs.Content>
108
+
109
+ <Tabs.Content value="node">
110
+ {selectedNode && selectedNode.type !== 'block' && selectedNode.type !== 'page' && selectedNode.type !== 'savedSectionRef' && (
111
+ <NodeSettings
112
+ node={selectedNode}
113
+ onUpdateSettings={onUpdateSettings}
114
+ onRemoveNode={onRemoveNode}
115
+ onDuplicateNode={onDuplicateNode}
116
+ onMoveNodeUp={onMoveNodeUp}
117
+ onMoveNodeDown={onMoveNodeDown}
118
+ onAddRow={selectedNode.type === 'section' ? onAddRow : undefined}
119
+ />
120
+ )}
121
+ </Tabs.Content>
122
+
123
+ <Tabs.Content value="page">
124
+ <PageSettingsEditor
125
+ settings={pageSettings}
126
+ onChange={onPageSettingsChange}
127
+ />
128
+ </Tabs.Content>
129
+
130
+ <Tabs.Content value="seo">
131
+ <BuilderSEOPanel
132
+ tree={tree}
133
+ pageSettings={pageSettings}
134
+ onPageSettingsChange={onPageSettingsChange}
135
+ selectedNodeId={selectedNode?.id ?? null}
136
+ />
137
+ </Tabs.Content>
138
+
139
+ <Tabs.Content value="design">
140
+ <DesignScorePanel tree={tree} />
141
+ </Tabs.Content>
142
+ </Tabs.Root>
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { analyzeDesign } from '@actuate-media/cms-core';
5
+ import type {
6
+ PageNode,
7
+ DesignAnalysis,
8
+ DesignCategory,
9
+ BuilderDesignCheck,
10
+ DesignSuggestion,
11
+ } from '@actuate-media/cms-core';
12
+ import {
13
+ CheckCircle2,
14
+ AlertCircle,
15
+ XCircle,
16
+ ChevronDown,
17
+ ChevronUp,
18
+ Lightbulb,
19
+ Palette,
20
+ } from 'lucide-react';
21
+
22
+ export interface DesignScorePanelProps {
23
+ tree: PageNode;
24
+ }
25
+
26
+ function getScoreColor(score: number, max: number): string {
27
+ const ratio = max > 0 ? score / max : 0;
28
+ if (ratio >= 0.8) return 'text-green-500';
29
+ if (ratio >= 0.5) return 'text-amber-500';
30
+ return 'text-red-500';
31
+ }
32
+
33
+ function getBarColor(score: number, max: number): string {
34
+ const ratio = max > 0 ? score / max : 0;
35
+ if (ratio >= 0.8) return 'bg-green-500';
36
+ if (ratio >= 0.5) return 'bg-amber-500';
37
+ return 'bg-red-500';
38
+ }
39
+
40
+ function getScoreLabel(score: number): string {
41
+ if (score >= 80) return 'Excellent';
42
+ if (score >= 65) return 'Good';
43
+ if (score >= 50) return 'Needs Work';
44
+ return 'Poor';
45
+ }
46
+
47
+ function getRingStrokeColor(score: number): string {
48
+ if (score >= 80) return 'rgb(34, 197, 94)';
49
+ if (score >= 50) return 'rgb(245, 158, 11)';
50
+ return 'rgb(239, 68, 68)';
51
+ }
52
+
53
+ function StatusIcon({ status }: { status: BuilderDesignCheck['status'] }) {
54
+ switch (status) {
55
+ case 'good':
56
+ return <CheckCircle2 size={14} className="text-green-500 shrink-0" />;
57
+ case 'warning':
58
+ return <AlertCircle size={14} className="text-amber-500 shrink-0" />;
59
+ case 'error':
60
+ return <XCircle size={14} className="text-red-500 shrink-0" />;
61
+ }
62
+ }
63
+
64
+ function SeverityBadge({ severity }: { severity: DesignSuggestion['severity'] }) {
65
+ const classes: Record<DesignSuggestion['severity'], string> = {
66
+ important: 'bg-red-100 text-red-700',
67
+ suggestion: 'bg-amber-100 text-amber-700',
68
+ info: 'bg-blue-100 text-blue-700',
69
+ };
70
+
71
+ return (
72
+ <span className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${classes[severity]}`}>
73
+ {severity}
74
+ </span>
75
+ );
76
+ }
77
+
78
+ function ScoreRing({ score }: { score: number }) {
79
+ const radius = 32;
80
+ const strokeWidth = 6;
81
+ const circumference = 2 * Math.PI * radius;
82
+ const progress = Math.min(score, 100) / 100;
83
+ const dashOffset = circumference * (1 - progress);
84
+ const size = (radius + strokeWidth) * 2;
85
+
86
+ return (
87
+ <div className="flex flex-col items-center gap-2">
88
+ <div className="relative">
89
+ <svg
90
+ width={size}
91
+ height={size}
92
+ viewBox={`0 0 ${size} ${size}`}
93
+ className="-rotate-90"
94
+ >
95
+ <circle
96
+ cx={size / 2}
97
+ cy={size / 2}
98
+ r={radius}
99
+ fill="none"
100
+ stroke="var(--border)"
101
+ strokeWidth={strokeWidth}
102
+ />
103
+ <circle
104
+ cx={size / 2}
105
+ cy={size / 2}
106
+ r={radius}
107
+ fill="none"
108
+ stroke={getRingStrokeColor(score)}
109
+ strokeWidth={strokeWidth}
110
+ strokeDasharray={circumference}
111
+ strokeDashoffset={dashOffset}
112
+ strokeLinecap="round"
113
+ className="transition-all duration-700 ease-out"
114
+ />
115
+ </svg>
116
+ <div className="absolute inset-0 flex items-center justify-center">
117
+ <span className={`text-lg font-medium ${getScoreColor(score, 100)}`}>
118
+ {score}
119
+ </span>
120
+ </div>
121
+ </div>
122
+ <span className="text-xs text-muted-foreground">{getScoreLabel(score)}</span>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ function CategoryRow({
128
+ category,
129
+ expanded,
130
+ onToggle,
131
+ }: {
132
+ category: DesignCategory;
133
+ expanded: boolean;
134
+ onToggle: () => void;
135
+ }) {
136
+ const percentage = category.maxScore > 0 ? (category.score / category.maxScore) * 100 : 0;
137
+
138
+ return (
139
+ <div className="border-b border-border last:border-b-0">
140
+ <button
141
+ type="button"
142
+ className="w-full text-left px-4 py-3 flex flex-col gap-2 hover:bg-muted/50 transition-colors"
143
+ onClick={onToggle}
144
+ aria-expanded={expanded}
145
+ >
146
+ <div className="flex items-center justify-between">
147
+ <span className="text-xs font-medium text-foreground">{category.label}</span>
148
+ <div className="flex items-center gap-2">
149
+ <span className="text-xs text-muted-foreground">
150
+ {category.score}/{category.maxScore}
151
+ </span>
152
+ {expanded ? (
153
+ <ChevronUp size={14} className="text-muted-foreground" />
154
+ ) : (
155
+ <ChevronDown size={14} className="text-muted-foreground" />
156
+ )}
157
+ </div>
158
+ </div>
159
+ <div className="w-full h-1.5 rounded-full bg-muted overflow-hidden">
160
+ <div
161
+ className={`h-full rounded-full transition-all duration-500 ease-out ${getBarColor(category.score, category.maxScore)}`}
162
+ style={{ width: `${percentage}%` }}
163
+ />
164
+ </div>
165
+ </button>
166
+
167
+ {expanded && category.checks.length > 0 && (
168
+ <div className="px-4 pb-3 flex flex-col gap-2">
169
+ {category.checks.map((check) => (
170
+ <div key={check.id} className="flex items-start gap-2 pl-1">
171
+ <StatusIcon status={check.status} />
172
+ <div className="flex flex-col">
173
+ <span className="text-xs font-medium text-foreground">{check.label}</span>
174
+ <span className="text-xs text-muted-foreground">{check.detail}</span>
175
+ </div>
176
+ </div>
177
+ ))}
178
+ </div>
179
+ )}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function SuggestionsList({ suggestions }: { suggestions: DesignSuggestion[] }) {
185
+ const sorted = useMemo(() => {
186
+ const order: Record<DesignSuggestion['severity'], number> = {
187
+ important: 0,
188
+ suggestion: 1,
189
+ info: 2,
190
+ };
191
+ return [...suggestions].sort((a, b) => order[a.severity] - order[b.severity]);
192
+ }, [suggestions]);
193
+
194
+ if (sorted.length === 0) return null;
195
+
196
+ return (
197
+ <div className="px-4 py-3 border-t border-border">
198
+ <div className="flex items-center gap-2 mb-3">
199
+ <Lightbulb size={14} className="text-muted-foreground" />
200
+ <span className="text-xs font-medium text-foreground">Suggestions</span>
201
+ </div>
202
+ <div className="flex flex-col gap-2.5">
203
+ {sorted.map((suggestion) => (
204
+ <div key={suggestion.id} className="flex items-start gap-2">
205
+ <SeverityBadge severity={suggestion.severity} />
206
+ <span className="text-xs text-muted-foreground leading-relaxed">
207
+ {suggestion.message}
208
+ </span>
209
+ </div>
210
+ ))}
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ export function DesignScorePanel({ tree }: DesignScorePanelProps) {
217
+ const analysis = useMemo(() => analyzeDesign(tree), [tree]);
218
+ const [expandedCategories, setExpandedCategories] = useState<string[]>([]);
219
+
220
+ const toggleCategory = (name: string) => {
221
+ setExpandedCategories((prev) =>
222
+ prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
223
+ );
224
+ };
225
+
226
+ if (!tree.children || tree.children.length === 0) {
227
+ return (
228
+ <div className="p-6 flex flex-col items-center justify-center text-center min-h-[200px]">
229
+ <Palette size={32} className="text-muted-foreground mb-3" />
230
+ <p className="text-sm font-medium text-foreground mb-1">Design Score</p>
231
+ <p className="text-xs text-muted-foreground">
232
+ Add sections to your page to see the design analysis
233
+ </p>
234
+ </div>
235
+ );
236
+ }
237
+
238
+ return (
239
+ <div className="flex flex-col">
240
+ <div className="flex flex-col items-center py-5 border-b border-border">
241
+ <ScoreRing score={analysis.score} />
242
+ </div>
243
+
244
+ <div className="flex flex-col">
245
+ {analysis.categories.map((category) => (
246
+ <CategoryRow
247
+ key={category.name}
248
+ category={category}
249
+ expanded={expandedCategories.includes(category.name)}
250
+ onToggle={() => toggleCategory(category.name)}
251
+ />
252
+ ))}
253
+ </div>
254
+
255
+ <SuggestionsList suggestions={analysis.suggestions} />
256
+ </div>
257
+ );
258
+ }