@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,81 @@
1
+ 'use client';
2
+
3
+ import type { BlockPreviewProps } from './index.js';
4
+
5
+ export function TextPreview({ data, variant = 'prose' }: BlockPreviewProps) {
6
+ const body = (data.body as string) || '';
7
+ const heading = (data.heading as string) || '';
8
+ const headingLevel = (data.headingLevel as string) || 'h2';
9
+
10
+ const truncatedBody = body.length > 300 ? body.slice(0, 300) + '…' : body;
11
+
12
+ const validLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
13
+ const HeadingTag = (validLevels.includes(headingLevel as any) ? headingLevel : 'h2') as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
14
+ const headingSizeClass =
15
+ HeadingTag === 'h1'
16
+ ? 'text-2xl'
17
+ : HeadingTag === 'h2'
18
+ ? 'text-xl'
19
+ : 'text-lg';
20
+
21
+ if (variant === 'two-column') {
22
+ const midpoint = Math.ceil(truncatedBody.length / 2);
23
+ const col1 = truncatedBody.slice(0, midpoint);
24
+ const col2 = truncatedBody.slice(midpoint);
25
+
26
+ return (
27
+ <div className="rounded-md border border-border p-5">
28
+ {heading && (
29
+ <HeadingTag className={`${headingSizeClass} mb-3 font-medium text-foreground`}>
30
+ {heading}
31
+ </HeadingTag>
32
+ )}
33
+ <div className="grid grid-cols-2 gap-4">
34
+ <p className="text-sm leading-relaxed text-muted-foreground">
35
+ {col1 || 'Column one text content…'}
36
+ </p>
37
+ <p className="text-sm leading-relaxed text-muted-foreground">
38
+ {col2 || 'Column two text content…'}
39
+ </p>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ if (variant === 'with-sidebar') {
46
+ return (
47
+ <div className="grid grid-cols-[1fr_200px] gap-4 rounded-md border border-border p-5">
48
+ <div>
49
+ {heading && (
50
+ <HeadingTag className={`${headingSizeClass} mb-3 font-medium text-foreground`}>
51
+ {heading}
52
+ </HeadingTag>
53
+ )}
54
+ <p className="text-sm leading-relaxed text-muted-foreground">
55
+ {truncatedBody || 'Body text content goes here…'}
56
+ </p>
57
+ </div>
58
+ <aside className="rounded-md bg-muted p-3">
59
+ <div className="h-3 w-3/4 rounded bg-border" />
60
+ <div className="mt-2 h-2 w-full rounded bg-border" />
61
+ <div className="mt-1 h-2 w-2/3 rounded bg-border" />
62
+ </aside>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <div className="rounded-md border border-border p-5">
69
+ {heading && (
70
+ <HeadingTag className={`${headingSizeClass} mb-3 font-medium text-foreground`}>
71
+ {heading}
72
+ </HeadingTag>
73
+ )}
74
+ <p className="text-sm leading-relaxed text-muted-foreground">
75
+ {truncatedBody || (
76
+ <span className="italic">Body text content goes here…</span>
77
+ )}
78
+ </p>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { Play } from 'lucide-react';
4
+ import type { BlockPreviewProps } from './index.js';
5
+
6
+ export function VideoPreview({ data, variant = 'inline' }: BlockPreviewProps) {
7
+ const poster = data.poster as string | undefined;
8
+ const autoplay = data.autoplay as boolean | undefined;
9
+ const loop = data.loop as boolean | undefined;
10
+
11
+ const badges: string[] = [
12
+ autoplay ? 'Autoplay' : '',
13
+ loop ? 'Loop' : '',
14
+ ].filter((b): b is string => b !== '');
15
+
16
+ const content = (
17
+ <div className="relative flex aspect-video items-center justify-center overflow-hidden rounded-md bg-muted">
18
+ {poster ? (
19
+ <img src={poster} alt="" className="absolute inset-0 h-full w-full object-cover" />
20
+ ) : (
21
+ <div className="absolute inset-0 bg-card" />
22
+ )}
23
+ <div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-primary/90 shadow-sm">
24
+ <Play size={20} className="text-primary-foreground" />
25
+ </div>
26
+ {badges.length > 0 && (
27
+ <div className="absolute bottom-2 right-2 z-10 flex gap-1">
28
+ {badges.map((badge) => (
29
+ <span
30
+ key={badge}
31
+ className="rounded bg-background/80 px-1.5 py-0.5 text-xs text-muted-foreground"
32
+ >
33
+ {badge}
34
+ </span>
35
+ ))}
36
+ </div>
37
+ )}
38
+ </div>
39
+ );
40
+
41
+ if (variant === 'background') {
42
+ return (
43
+ <div className="overflow-hidden rounded-md border border-border">
44
+ <div className="relative aspect-[21/9]">
45
+ {poster ? (
46
+ <img src={poster} alt="" className="absolute inset-0 h-full w-full object-cover" />
47
+ ) : (
48
+ <div className="absolute inset-0 bg-card" />
49
+ )}
50
+ <div className="absolute inset-0 flex items-center justify-center bg-background/30">
51
+ <div className="flex h-14 w-14 items-center justify-center rounded-full bg-primary/90 shadow-sm">
52
+ <Play size={24} className="text-primary-foreground" />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ if (variant === 'lightbox') {
61
+ return (
62
+ <div className="rounded-md border border-border p-4">
63
+ <div className="mx-auto max-w-sm">
64
+ {content}
65
+ <p className="mt-2 text-center text-xs text-muted-foreground">
66
+ Click to play in lightbox
67
+ </p>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div className="rounded-md border border-border p-4">
75
+ {content}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import type { ComponentType } from 'react';
4
+ import { HeroPreview } from './HeroPreview.js';
5
+ import { TextPreview } from './TextPreview.js';
6
+ import { ImagePreview } from './ImagePreview.js';
7
+ import { CardsPreview } from './CardsPreview.js';
8
+ import { CTAPreview } from './CTAPreview.js';
9
+ import { VideoPreview } from './VideoPreview.js';
10
+ import { GalleryPreview } from './GalleryPreview.js';
11
+ import { FAQPreview } from './FAQPreview.js';
12
+ import { FormPreview } from './FormPreview.js';
13
+ import { CodePreview } from './CodePreview.js';
14
+
15
+ export interface BlockPreviewProps {
16
+ data: Record<string, unknown>;
17
+ variant?: string;
18
+ }
19
+
20
+ export const blockRenderers: Record<string, ComponentType<BlockPreviewProps>> = {
21
+ hero: HeroPreview,
22
+ text: TextPreview,
23
+ image: ImagePreview,
24
+ cards: CardsPreview,
25
+ cta: CTAPreview,
26
+ video: VideoPreview,
27
+ gallery: GalleryPreview,
28
+ faq: FAQPreview,
29
+ form: FormPreview,
30
+ code: CodePreview,
31
+ };
32
+
33
+ export { FallbackPreview } from './FallbackPreview.js';
34
+ export type { FallbackPreviewProps } from './FallbackPreview.js';
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Box, Type, Image, Layout, Star } from 'lucide-react';
5
+ import type { BlockNode } from '@actuate-media/cms-core';
6
+
7
+ export interface BlockRendererProps {
8
+ node: BlockNode;
9
+ selectedNodeId: string | null;
10
+ onSelectNode: (id: string | null) => void;
11
+ }
12
+
13
+ const BLOCK_ICONS: Record<string, typeof Box> = {
14
+ hero: Star,
15
+ text: Type,
16
+ image: Image,
17
+ layout: Layout,
18
+ };
19
+
20
+ function getBlockLabel(blockType: string): string {
21
+ return blockType.charAt(0).toUpperCase() + blockType.slice(1);
22
+ }
23
+
24
+ export function BlockRenderer({ node, selectedNodeId, onSelectNode }: BlockRendererProps) {
25
+ const [hovered, setHovered] = useState(false);
26
+ const isSelected = selectedNodeId === node.id;
27
+ const blockType = node.settings.blockType;
28
+ const label = getBlockLabel(blockType);
29
+ const Icon = BLOCK_ICONS[blockType] ?? Box;
30
+
31
+ const handleClick = (e: React.MouseEvent) => {
32
+ e.stopPropagation();
33
+ onSelectNode(node.id);
34
+ };
35
+
36
+ return (
37
+ <div
38
+ data-node-id={node.id}
39
+ className={`relative min-h-[48px] transition-shadow ${
40
+ isSelected
41
+ ? 'ring-2 ring-primary ring-offset-2'
42
+ : hovered
43
+ ? 'ring-1 ring-primary/50'
44
+ : ''
45
+ }`}
46
+ onClick={handleClick}
47
+ onMouseEnter={() => setHovered(true)}
48
+ onMouseLeave={() => setHovered(false)}
49
+ >
50
+ {(hovered || isSelected) && (
51
+ <span className="absolute -top-2 -left-1 text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded font-medium z-10">
52
+ {label}
53
+ </span>
54
+ )}
55
+
56
+ <div className="flex items-center justify-center gap-2 p-4 bg-muted/30 border border-dashed border-border rounded-md">
57
+ <Icon size={16} className="text-muted-foreground" />
58
+ <span className="text-sm text-muted-foreground">{label}</span>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { LayoutGrid } from 'lucide-react';
4
+ import type { PageNode } from '@actuate-media/cms-core';
5
+ import { SectionRenderer } from './SectionRenderer.js';
6
+
7
+ export interface BuilderCanvasProps {
8
+ tree: PageNode;
9
+ selectedNodeId: string | null;
10
+ showGridOverlay: boolean;
11
+ deviceMode: 'desktop' | 'tablet' | 'mobile';
12
+ onSelectNode: (id: string | null) => void;
13
+ }
14
+
15
+ const DEVICE_CLASSES: Record<BuilderCanvasProps['deviceMode'], string> = {
16
+ desktop: 'w-full',
17
+ tablet: 'max-w-[768px] mx-auto',
18
+ mobile: 'max-w-[375px] mx-auto',
19
+ };
20
+
21
+ export function BuilderCanvas({
22
+ tree,
23
+ selectedNodeId,
24
+ showGridOverlay,
25
+ deviceMode,
26
+ onSelectNode,
27
+ }: BuilderCanvasProps) {
28
+ const handleCanvasClick = (e: React.MouseEvent) => {
29
+ if (e.target === e.currentTarget) {
30
+ onSelectNode(null);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div
36
+ className="relative flex-1 overflow-auto bg-muted p-6"
37
+ onClick={handleCanvasClick}
38
+ >
39
+ {showGridOverlay && <GridOverlay deviceMode={deviceMode} />}
40
+
41
+ <div
42
+ className={`relative bg-background shadow-sm min-h-full ${DEVICE_CLASSES[deviceMode]}`}
43
+ onClick={handleCanvasClick}
44
+ >
45
+ {tree.children.map((child) => {
46
+ if (child.type === 'section') {
47
+ return (
48
+ <SectionRenderer
49
+ key={child.id}
50
+ node={child}
51
+ selectedNodeId={selectedNodeId}
52
+ onSelectNode={onSelectNode}
53
+ />
54
+ );
55
+ }
56
+ return null;
57
+ })}
58
+
59
+ {tree.children.length === 0 && (
60
+ <div className="flex flex-col items-center justify-center min-h-[400px] text-center">
61
+ <LayoutGrid size={32} className="text-muted-foreground mb-3" />
62
+ <p className="text-sm font-medium text-foreground mb-1">No sections yet</p>
63
+ <p className="text-xs text-muted-foreground">
64
+ Click "Add Section" below to start building your page
65
+ </p>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function GridOverlay({ deviceMode }: { deviceMode: BuilderCanvasProps['deviceMode'] }) {
74
+ return (
75
+ <div
76
+ className={`pointer-events-none absolute inset-0 z-50 p-6 ${
77
+ deviceMode !== 'desktop' ? 'flex justify-center' : ''
78
+ }`}
79
+ aria-hidden="true"
80
+ >
81
+ <div
82
+ className={`grid grid-cols-12 gap-4 h-full opacity-[0.08] ${DEVICE_CLASSES[deviceMode]}`}
83
+ >
84
+ {Array.from({ length: 12 }).map((_, i) => (
85
+ <div key={i} className="bg-primary h-full rounded-sm" />
86
+ ))}
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { ColumnNode } from '@actuate-media/cms-core';
5
+ import { BlockRenderer } from './BlockRenderer.js';
6
+ import { ContainerRenderer } from './ContainerRenderer.js';
7
+
8
+ export interface ColumnRendererProps {
9
+ node: ColumnNode;
10
+ selectedNodeId: string | null;
11
+ onSelectNode: (id: string | null) => void;
12
+ }
13
+
14
+ export function ColumnRenderer({ node, selectedNodeId, onSelectNode }: ColumnRendererProps) {
15
+ const [hovered, setHovered] = useState(false);
16
+ const isSelected = selectedNodeId === node.id;
17
+ const width = node.settings.width;
18
+ const isEmpty = node.children.length === 0;
19
+
20
+ const handleClick = (e: React.MouseEvent) => {
21
+ e.stopPropagation();
22
+ onSelectNode(node.id);
23
+ };
24
+
25
+ return (
26
+ <div
27
+ data-node-id={node.id}
28
+ className={`relative transition-shadow ${
29
+ isSelected
30
+ ? 'ring-2 ring-primary ring-offset-2'
31
+ : hovered
32
+ ? 'ring-1 ring-primary/50'
33
+ : ''
34
+ }`}
35
+ style={{
36
+ gridColumn: `span ${width}`,
37
+ padding: node.settings.padding,
38
+ background: node.settings.background,
39
+ }}
40
+ onClick={handleClick}
41
+ onMouseEnter={(e) => {
42
+ e.stopPropagation();
43
+ setHovered(true);
44
+ }}
45
+ onMouseLeave={() => setHovered(false)}
46
+ >
47
+ {(hovered || isSelected) && (
48
+ <span className="absolute -top-2 -left-1 text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded font-medium z-10">
49
+ Col {width}
50
+ </span>
51
+ )}
52
+
53
+ {isEmpty ? (
54
+ <div className="flex items-center justify-center min-h-[64px] border-2 border-dashed border-border rounded-md text-sm text-muted-foreground">
55
+ Empty column
56
+ </div>
57
+ ) : (
58
+ <div className="flex flex-col gap-2">
59
+ {node.children.map((child) => {
60
+ if (child.type === 'block') {
61
+ return (
62
+ <BlockRenderer
63
+ key={child.id}
64
+ node={child}
65
+ selectedNodeId={selectedNodeId}
66
+ onSelectNode={onSelectNode}
67
+ />
68
+ );
69
+ }
70
+ if (child.type === 'container') {
71
+ return (
72
+ <ContainerRenderer
73
+ key={child.id}
74
+ node={child}
75
+ selectedNodeId={selectedNodeId}
76
+ onSelectNode={onSelectNode}
77
+ />
78
+ );
79
+ }
80
+ return null;
81
+ })}
82
+ </div>
83
+ )}
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { ContainerNode } from '@actuate-media/cms-core';
5
+ import { RowRenderer } from './RowRenderer.js';
6
+
7
+ export interface ContainerRendererProps {
8
+ node: ContainerNode;
9
+ selectedNodeId: string | null;
10
+ onSelectNode: (id: string | null) => void;
11
+ }
12
+
13
+ export function ContainerRenderer({ node, selectedNodeId, onSelectNode }: ContainerRendererProps) {
14
+ const [hovered, setHovered] = useState(false);
15
+ const isSelected = selectedNodeId === node.id;
16
+ const maxWidth = node.settings.maxWidth ?? '1200px';
17
+ const alignment = node.settings.alignment ?? 'center';
18
+ const padding = node.settings.padding;
19
+
20
+ const marginMap: Record<string, string> = {
21
+ left: '0 auto 0 0',
22
+ center: '0 auto',
23
+ right: '0 0 0 auto',
24
+ };
25
+
26
+ const handleClick = (e: React.MouseEvent) => {
27
+ e.stopPropagation();
28
+ onSelectNode(node.id);
29
+ };
30
+
31
+ return (
32
+ <div
33
+ data-node-id={node.id}
34
+ className={`relative transition-shadow ${
35
+ isSelected
36
+ ? 'ring-2 ring-primary ring-offset-2'
37
+ : hovered
38
+ ? 'ring-1 ring-primary/50'
39
+ : ''
40
+ }`}
41
+ style={{
42
+ maxWidth,
43
+ margin: marginMap[alignment] ?? '0 auto',
44
+ padding,
45
+ }}
46
+ onClick={handleClick}
47
+ onMouseEnter={(e) => {
48
+ e.stopPropagation();
49
+ setHovered(true);
50
+ }}
51
+ onMouseLeave={() => setHovered(false)}
52
+ >
53
+ {(hovered || isSelected) && (
54
+ <span className="absolute -top-2 -left-1 text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded font-medium z-10">
55
+ Container
56
+ </span>
57
+ )}
58
+
59
+ <div className="flex flex-col gap-4">
60
+ {node.children.map((row) => (
61
+ <RowRenderer
62
+ key={row.id}
63
+ node={row}
64
+ selectedNodeId={selectedNodeId}
65
+ onSelectNode={onSelectNode}
66
+ />
67
+ ))}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { RowNode } from '@actuate-media/cms-core';
5
+ import { ColumnRenderer } from './ColumnRenderer.js';
6
+
7
+ export interface RowRendererProps {
8
+ node: RowNode;
9
+ selectedNodeId: string | null;
10
+ onSelectNode: (id: string | null) => void;
11
+ }
12
+
13
+ export function RowRenderer({ node, selectedNodeId, onSelectNode }: RowRendererProps) {
14
+ const [hovered, setHovered] = useState(false);
15
+ const isSelected = selectedNodeId === node.id;
16
+ const gap = node.settings.gap ?? '16px';
17
+ const verticalAlign = node.settings.verticalAlign ?? 'stretch';
18
+
19
+ const alignMap: Record<string, string> = {
20
+ top: 'start',
21
+ center: 'center',
22
+ bottom: 'end',
23
+ stretch: 'stretch',
24
+ };
25
+
26
+ const handleClick = (e: React.MouseEvent) => {
27
+ e.stopPropagation();
28
+ onSelectNode(node.id);
29
+ };
30
+
31
+ return (
32
+ <div
33
+ data-node-id={node.id}
34
+ className={`relative transition-shadow ${
35
+ isSelected
36
+ ? 'ring-2 ring-primary ring-offset-2'
37
+ : hovered
38
+ ? 'ring-1 ring-primary/50'
39
+ : ''
40
+ }`}
41
+ onClick={handleClick}
42
+ onMouseEnter={(e) => {
43
+ e.stopPropagation();
44
+ setHovered(true);
45
+ }}
46
+ onMouseLeave={() => setHovered(false)}
47
+ >
48
+ {(hovered || isSelected) && (
49
+ <span className="absolute -top-2 -left-1 text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded font-medium z-10">
50
+ Row
51
+ </span>
52
+ )}
53
+
54
+ <div
55
+ className="grid grid-cols-12"
56
+ style={{
57
+ gap,
58
+ alignItems: alignMap[verticalAlign] ?? 'stretch',
59
+ }}
60
+ >
61
+ {node.children.map((col) => (
62
+ <ColumnRenderer
63
+ key={col.id}
64
+ node={col}
65
+ selectedNodeId={selectedNodeId}
66
+ onSelectNode={onSelectNode}
67
+ />
68
+ ))}
69
+ </div>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { SectionNode } from '@actuate-media/cms-core';
5
+ import { ContainerRenderer } from './ContainerRenderer.js';
6
+ import { RowRenderer } from './RowRenderer.js';
7
+
8
+ export interface SectionRendererProps {
9
+ node: SectionNode;
10
+ selectedNodeId: string | null;
11
+ onSelectNode: (id: string | null) => void;
12
+ }
13
+
14
+ export function SectionRenderer({ node, selectedNodeId, onSelectNode }: SectionRendererProps) {
15
+ const [hovered, setHovered] = useState(false);
16
+ const isSelected = selectedNodeId === node.id;
17
+
18
+ const handleClick = (e: React.MouseEvent) => {
19
+ e.stopPropagation();
20
+ onSelectNode(node.id);
21
+ };
22
+
23
+ const backgroundStyle: React.CSSProperties = {};
24
+ if (node.settings.background) {
25
+ backgroundStyle.backgroundColor = node.settings.background;
26
+ }
27
+ if (node.settings.backgroundImage) {
28
+ backgroundStyle.backgroundImage = `url(${node.settings.backgroundImage})`;
29
+ backgroundStyle.backgroundSize = 'cover';
30
+ backgroundStyle.backgroundPosition = 'center';
31
+ }
32
+ if (node.settings.backgroundGradient) {
33
+ backgroundStyle.backgroundImage = node.settings.backgroundGradient;
34
+ }
35
+ if (node.settings.paddingTop) {
36
+ backgroundStyle.paddingTop = node.settings.paddingTop;
37
+ }
38
+ if (node.settings.paddingBottom) {
39
+ backgroundStyle.paddingBottom = node.settings.paddingBottom;
40
+ }
41
+ if (node.settings.marginTop) {
42
+ backgroundStyle.marginTop = node.settings.marginTop;
43
+ }
44
+ if (node.settings.marginBottom) {
45
+ backgroundStyle.marginBottom = node.settings.marginBottom;
46
+ }
47
+
48
+ return (
49
+ <div
50
+ data-node-id={node.id}
51
+ className={`relative w-full transition-shadow ${
52
+ isSelected
53
+ ? 'ring-2 ring-primary ring-offset-2'
54
+ : hovered
55
+ ? 'ring-1 ring-primary/50'
56
+ : ''
57
+ }`}
58
+ style={backgroundStyle}
59
+ onClick={handleClick}
60
+ onMouseEnter={(e) => {
61
+ e.stopPropagation();
62
+ setHovered(true);
63
+ }}
64
+ onMouseLeave={() => setHovered(false)}
65
+ >
66
+ {(hovered || isSelected) && (
67
+ <span className="absolute -top-2 -left-1 text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded font-medium z-10">
68
+ Section
69
+ </span>
70
+ )}
71
+
72
+ {node.children.map((child) => {
73
+ if (child.type === 'container') {
74
+ return (
75
+ <ContainerRenderer
76
+ key={child.id}
77
+ node={child}
78
+ selectedNodeId={selectedNodeId}
79
+ onSelectNode={onSelectNode}
80
+ />
81
+ );
82
+ }
83
+ if (child.type === 'row') {
84
+ return (
85
+ <RowRenderer
86
+ key={child.id}
87
+ node={child}
88
+ selectedNodeId={selectedNodeId}
89
+ onSelectNode={onSelectNode}
90
+ />
91
+ );
92
+ }
93
+ return null;
94
+ })}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,2 @@
1
+ export { BuilderCanvas } from './BuilderCanvas.js';
2
+ export type { BuilderCanvasProps } from './BuilderCanvas.js';