@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,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
|
+
}
|