@actuate-media/cms-admin 0.4.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.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +35 -0
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +1 -0
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts +49 -0
- package/dist/hooks/useBuilderState.d.ts.map +1 -0
- package/dist/hooks/useBuilderState.js +238 -0
- package/dist/hooks/useBuilderState.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +2 -2
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/ForgotPassword.d.ts +5 -0
- package/dist/views/ForgotPassword.d.ts.map +1 -0
- package/dist/views/ForgotPassword.js +41 -0
- package/dist/views/ForgotPassword.js.map +1 -0
- package/dist/views/ResetPassword.d.ts +6 -0
- package/dist/views/ResetPassword.d.ts.map +1 -0
- package/dist/views/ResetPassword.js +46 -0
- package/dist/views/ResetPassword.js.map +1 -0
- package/dist/views/ScriptTagEditor.d.ts +6 -0
- package/dist/views/ScriptTagEditor.d.ts.map +1 -0
- package/dist/views/ScriptTagEditor.js +109 -0
- package/dist/views/ScriptTagEditor.js.map +1 -0
- package/dist/views/ScriptTags.d.ts +5 -0
- package/dist/views/ScriptTags.d.ts.map +1 -0
- package/dist/views/ScriptTags.js +54 -0
- package/dist/views/ScriptTags.js.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.js +40 -0
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.js +170 -0
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
- package/dist/views/page-builder/BlockEditor.d.ts +11 -0
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
- package/dist/views/page-builder/BlockEditor.js +67 -0
- package/dist/views/page-builder/BlockEditor.js.map +1 -0
- package/dist/views/page-builder/BlockPicker.d.ts +7 -0
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
- package/dist/views/page-builder/BlockPicker.js +102 -0
- package/dist/views/page-builder/BlockPicker.js.map +1 -0
- package/dist/views/page-builder/BottomBar.d.ts +9 -0
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
- package/dist/views/page-builder/BottomBar.js +13 -0
- package/dist/views/page-builder/BottomBar.js.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.js +18 -0
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
- package/dist/views/page-builder/ContextPanel.d.ts +20 -0
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
- package/dist/views/page-builder/ContextPanel.js +40 -0
- package/dist/views/page-builder/ContextPanel.js.map +1 -0
- package/dist/views/page-builder/DesignScore.d.ts +6 -0
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
- package/dist/views/page-builder/DesignScore.js +93 -0
- package/dist/views/page-builder/DesignScore.js.map +1 -0
- package/dist/views/page-builder/NodeSettings.d.ts +12 -0
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
- package/dist/views/page-builder/NodeSettings.js +80 -0
- package/dist/views/page-builder/NodeSettings.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts +8 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
- package/dist/views/page-builder/PageBuilder.js +126 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -0
- package/dist/views/page-builder/PageSettings.d.ts +7 -0
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
- package/dist/views/page-builder/PageSettings.js +27 -0
- package/dist/views/page-builder/PageSettings.js.map +1 -0
- package/dist/views/page-builder/SEOPanel.d.ts +10 -0
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
- package/dist/views/page-builder/SEOPanel.js +105 -0
- package/dist/views/page-builder/SEOPanel.js.map +1 -0
- package/dist/views/page-builder/SavedSections.d.ts +6 -0
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
- package/dist/views/page-builder/SavedSections.js +145 -0
- package/dist/views/page-builder/SavedSections.js.map +1 -0
- package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
- package/dist/views/page-builder/TemplatePicker.js +68 -0
- package/dist/views/page-builder/TemplatePicker.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/index.js +25 -0
- package/dist/views/page-builder/block-renderers/index.js.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/index.d.ts +3 -0
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/index.js +2 -0
- package/dist/views/page-builder/canvas/index.js.map +1 -0
- package/package.json +7 -4
- package/src/AdminRoot.tsx +41 -0
- package/src/components/Breadcrumbs.tsx +1 -0
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/hooks/useBuilderState.ts +328 -0
- package/src/index.ts +8 -0
- package/src/layout/Sidebar.tsx +7 -0
- package/src/views/ForgotPassword.tsx +136 -0
- package/src/views/ResetPassword.tsx +192 -0
- package/src/views/ScriptTagEditor.tsx +361 -0
- package/src/views/ScriptTags.tsx +174 -0
- package/src/views/page-builder/AIBlockAssist.tsx +68 -0
- package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
- package/src/views/page-builder/BlockEditor.tsx +352 -0
- package/src/views/page-builder/BlockPicker.tsx +338 -0
- package/src/views/page-builder/BottomBar.tsx +64 -0
- package/src/views/page-builder/BuilderToolbar.tsx +218 -0
- package/src/views/page-builder/ContextPanel.tsx +145 -0
- package/src/views/page-builder/DesignScore.tsx +258 -0
- package/src/views/page-builder/NodeSettings.tsx +515 -0
- package/src/views/page-builder/PageBuilder.tsx +288 -0
- package/src/views/page-builder/PageSettings.tsx +161 -0
- package/src/views/page-builder/SEOPanel.tsx +485 -0
- package/src/views/page-builder/SavedSections.tsx +486 -0
- package/src/views/page-builder/TemplatePicker.tsx +201 -0
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
- package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
- package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
- package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
- package/src/views/page-builder/block-renderers/index.ts +34 -0
- package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
- package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
- package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
- 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
|
+
}
|