@prenta/admin 0.98.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +17 -28
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/seo-score-bands.test.js +0 -3
- package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
- package/dist/__tests__/layout/sidebar-posts.test.js +6 -11
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
- package/dist/__tests__/layout/sidebar-submenu.render.test.js +3 -2
- package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
- package/dist/__tests__/lib/page-editor-service-api.test.js +26 -23
- package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.js +0 -1
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.js +0 -1
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +3 -4
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +7 -38
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +0 -7
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +9 -9
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/PageEditor.d.ts +2 -2
- package/dist/views/PageEditor.js +2 -2
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +9 -15
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/package.json +4 -4
- package/src/AdminRoot.tsx +18 -68
- package/src/__tests__/components/seo-score-bands.test.ts +0 -3
- package/src/__tests__/layout/sidebar-posts.test.ts +6 -11
- package/src/__tests__/layout/sidebar-submenu.render.test.tsx +3 -2
- package/src/__tests__/lib/page-editor-service-api.test.ts +27 -25
- package/src/__tests__/views/page-section-editor.test.tsx +0 -1
- package/src/__tests__/views/page-tab.render.test.tsx +0 -1
- package/src/index.ts +1 -11
- package/src/layout/Sidebar.tsx +6 -41
- package/src/lib/page-editor-service.ts +9 -16
- package/src/views/PageEditor.tsx +2 -2
- package/src/views/page-editor/PageSectionEditor.tsx +8 -37
- package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-builder-block-renderer.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-builder-block-renderer.render.test.js +0 -48
- package/dist/__tests__/views/page-builder-block-renderer.render.test.js.map +0 -1
- package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-builder-form-picker.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-builder-form-picker.render.test.js +0 -44
- package/dist/__tests__/views/page-builder-form-picker.render.test.js.map +0 -1
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +0 -121
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +0 -1
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js +0 -65
- package/dist/__tests__/views/page-builder-seo-panel.render.test.js.map +0 -1
- package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-builder-template-seed.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-builder-template-seed.render.test.js +0 -48
- package/dist/__tests__/views/page-builder-template-seed.render.test.js.map +0 -1
- package/dist/__tests__/views/page-templates.render.test.d.ts +0 -2
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +0 -1
- package/dist/__tests__/views/page-templates.render.test.js +0 -126
- package/dist/__tests__/views/page-templates.render.test.js.map +0 -1
- package/dist/hooks/useBuilderState.d.ts +0 -49
- package/dist/hooks/useBuilderState.d.ts.map +0 -1
- package/dist/hooks/useBuilderState.js +0 -238
- package/dist/hooks/useBuilderState.js.map +0 -1
- package/dist/views/page-builder/AIBlockAssist.d.ts +0 -9
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +0 -1
- package/dist/views/page-builder/AIBlockAssist.js +0 -40
- package/dist/views/page-builder/AIBlockAssist.js.map +0 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts +0 -8
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +0 -1
- package/dist/views/page-builder/AIGenerateDialog.js +0 -173
- package/dist/views/page-builder/AIGenerateDialog.js.map +0 -1
- package/dist/views/page-builder/BlockEditor.d.ts +0 -11
- package/dist/views/page-builder/BlockEditor.d.ts.map +0 -1
- package/dist/views/page-builder/BlockEditor.js +0 -159
- package/dist/views/page-builder/BlockEditor.js.map +0 -1
- package/dist/views/page-builder/BlockPicker.d.ts +0 -7
- package/dist/views/page-builder/BlockPicker.d.ts.map +0 -1
- package/dist/views/page-builder/BlockPicker.js +0 -103
- package/dist/views/page-builder/BlockPicker.js.map +0 -1
- package/dist/views/page-builder/BottomBar.d.ts +0 -9
- package/dist/views/page-builder/BottomBar.d.ts.map +0 -1
- package/dist/views/page-builder/BottomBar.js +0 -13
- package/dist/views/page-builder/BottomBar.js.map +0 -1
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +0 -20
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +0 -1
- package/dist/views/page-builder/BuilderSectionsPanel.js +0 -105
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +0 -1
- package/dist/views/page-builder/BuilderToolbar.d.ts +0 -21
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +0 -1
- package/dist/views/page-builder/BuilderToolbar.js +0 -18
- package/dist/views/page-builder/BuilderToolbar.js.map +0 -1
- package/dist/views/page-builder/ContextPanel.d.ts +0 -20
- package/dist/views/page-builder/ContextPanel.d.ts.map +0 -1
- package/dist/views/page-builder/ContextPanel.js +0 -43
- package/dist/views/page-builder/ContextPanel.js.map +0 -1
- package/dist/views/page-builder/DesignScore.d.ts +0 -6
- package/dist/views/page-builder/DesignScore.d.ts.map +0 -1
- package/dist/views/page-builder/DesignScore.js +0 -93
- package/dist/views/page-builder/DesignScore.js.map +0 -1
- package/dist/views/page-builder/NodeSettings.d.ts +0 -12
- package/dist/views/page-builder/NodeSettings.d.ts.map +0 -1
- package/dist/views/page-builder/NodeSettings.js +0 -80
- package/dist/views/page-builder/NodeSettings.js.map +0 -1
- package/dist/views/page-builder/PageBuilder.d.ts +0 -10
- package/dist/views/page-builder/PageBuilder.d.ts.map +0 -1
- package/dist/views/page-builder/PageBuilder.js +0 -222
- package/dist/views/page-builder/PageBuilder.js.map +0 -1
- package/dist/views/page-builder/PageSettings.d.ts +0 -7
- package/dist/views/page-builder/PageSettings.d.ts.map +0 -1
- package/dist/views/page-builder/PageSettings.js +0 -27
- package/dist/views/page-builder/PageSettings.js.map +0 -1
- package/dist/views/page-builder/PageTemplates.d.ts +0 -5
- package/dist/views/page-builder/PageTemplates.d.ts.map +0 -1
- package/dist/views/page-builder/PageTemplates.js +0 -146
- package/dist/views/page-builder/PageTemplates.js.map +0 -1
- package/dist/views/page-builder/RelationshipField.d.ts +0 -15
- package/dist/views/page-builder/RelationshipField.d.ts.map +0 -1
- package/dist/views/page-builder/RelationshipField.js +0 -65
- package/dist/views/page-builder/RelationshipField.js.map +0 -1
- package/dist/views/page-builder/SEOPanel.d.ts +0 -9
- package/dist/views/page-builder/SEOPanel.d.ts.map +0 -1
- package/dist/views/page-builder/SEOPanel.js +0 -38
- package/dist/views/page-builder/SEOPanel.js.map +0 -1
- package/dist/views/page-builder/SavedSections.d.ts +0 -6
- package/dist/views/page-builder/SavedSections.d.ts.map +0 -1
- package/dist/views/page-builder/SavedSections.js +0 -141
- package/dist/views/page-builder/SavedSections.js.map +0 -1
- package/dist/views/page-builder/TemplatePicker.d.ts +0 -7
- package/dist/views/page-builder/TemplatePicker.d.ts.map +0 -1
- package/dist/views/page-builder/TemplatePicker.js +0 -68
- package/dist/views/page-builder/TemplatePicker.js.map +0 -1
- package/dist/views/page-builder/Widgets.d.ts +0 -13
- package/dist/views/page-builder/Widgets.d.ts.map +0 -1
- package/dist/views/page-builder/Widgets.js +0 -15
- package/dist/views/page-builder/Widgets.js.map +0 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/CTAPreview.js +0 -19
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/CardsPreview.js +0 -22
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +0 -12
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +0 -27
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +0 -6
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +0 -7
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +0 -9
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +0 -52
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +0 -19
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/HeroPreview.js +0 -19
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/ImagePreview.js +0 -17
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/TextPreview.js +0 -22
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +0 -3
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/VideoPreview.js +0 -18
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +0 -1
- package/dist/views/page-builder/block-renderers/index.d.ts +0 -9
- package/dist/views/page-builder/block-renderers/index.d.ts.map +0 -1
- package/dist/views/page-builder/block-renderers/index.js +0 -25
- package/dist/views/page-builder/block-renderers/index.js.map +0 -1
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts +0 -8
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/BlockRenderer.js +0 -20
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +0 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +0 -10
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js +0 -26
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +0 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +0 -8
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/ColumnRenderer.js +0 -32
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +0 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +0 -8
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/ContainerRenderer.js +0 -29
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +0 -1
- package/dist/views/page-builder/canvas/RowRenderer.d.ts +0 -8
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/RowRenderer.js +0 -28
- package/dist/views/page-builder/canvas/RowRenderer.js.map +0 -1
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts +0 -8
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/SectionRenderer.js +0 -50
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +0 -1
- package/dist/views/page-builder/canvas/index.d.ts +0 -3
- package/dist/views/page-builder/canvas/index.d.ts.map +0 -1
- package/dist/views/page-builder/canvas/index.js +0 -2
- package/dist/views/page-builder/canvas/index.js.map +0 -1
- package/src/__tests__/views/page-builder-block-renderer.render.test.tsx +0 -66
- package/src/__tests__/views/page-builder-form-picker.render.test.tsx +0 -54
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +0 -138
- package/src/__tests__/views/page-builder-seo-panel.render.test.tsx +0 -77
- package/src/__tests__/views/page-builder-template-seed.render.test.tsx +0 -59
- package/src/__tests__/views/page-templates.render.test.tsx +0 -182
- package/src/hooks/useBuilderState.ts +0 -361
- package/src/views/page-builder/AIBlockAssist.tsx +0 -64
- package/src/views/page-builder/AIGenerateDialog.tsx +0 -593
- package/src/views/page-builder/BlockEditor.tsx +0 -523
- package/src/views/page-builder/BlockPicker.tsx +0 -324
- package/src/views/page-builder/BottomBar.tsx +0 -65
- package/src/views/page-builder/BuilderSectionsPanel.tsx +0 -304
- package/src/views/page-builder/BuilderToolbar.tsx +0 -221
- package/src/views/page-builder/ContextPanel.tsx +0 -144
- package/src/views/page-builder/DesignScore.tsx +0 -251
- package/src/views/page-builder/NodeSettings.tsx +0 -500
- package/src/views/page-builder/PageBuilder.tsx +0 -405
- package/src/views/page-builder/PageSettings.tsx +0 -162
- package/src/views/page-builder/PageTemplates.tsx +0 -435
- package/src/views/page-builder/RelationshipField.tsx +0 -113
- package/src/views/page-builder/SEOPanel.tsx +0 -222
- package/src/views/page-builder/SavedSections.tsx +0 -480
- package/src/views/page-builder/TemplatePicker.tsx +0 -197
- package/src/views/page-builder/Widgets.tsx +0 -44
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +0 -79
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +0 -69
- package/src/views/page-builder/block-renderers/CodePreview.tsx +0 -46
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +0 -87
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +0 -18
- package/src/views/page-builder/block-renderers/FormPreview.tsx +0 -117
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +0 -87
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +0 -90
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +0 -54
- package/src/views/page-builder/block-renderers/TextPreview.tsx +0 -81
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +0 -73
- package/src/views/page-builder/block-renderers/index.ts +0 -34
- package/src/views/page-builder/canvas/BlockRenderer.tsx +0 -56
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +0 -87
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +0 -82
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +0 -67
- package/src/views/page-builder/canvas/RowRenderer.tsx +0 -68
- package/src/views/page-builder/canvas/SectionRenderer.tsx +0 -93
- package/src/views/page-builder/canvas/index.ts +0 -2
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
|
4
|
-
import * as Dialog from '@radix-ui/react-dialog'
|
|
5
|
-
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
6
|
-
import {
|
|
7
|
-
Star,
|
|
8
|
-
Type,
|
|
9
|
-
Image,
|
|
10
|
-
LayoutGrid,
|
|
11
|
-
MousePointer,
|
|
12
|
-
Play,
|
|
13
|
-
Grid,
|
|
14
|
-
HelpCircle,
|
|
15
|
-
FileText,
|
|
16
|
-
Code,
|
|
17
|
-
Box,
|
|
18
|
-
X,
|
|
19
|
-
ArrowLeft,
|
|
20
|
-
Search,
|
|
21
|
-
Check,
|
|
22
|
-
} from 'lucide-react'
|
|
23
|
-
import type { LucideIcon } from 'lucide-react'
|
|
24
|
-
import { BlockCatalog } from '@prenta/core/page-builder'
|
|
25
|
-
import type { BlockTypeDefinition, VariantDefinition } from '@prenta/core'
|
|
26
|
-
|
|
27
|
-
export interface BlockPickerProps {
|
|
28
|
-
open: boolean
|
|
29
|
-
onClose: () => void
|
|
30
|
-
onSelect: (blockType: string, variant?: string) => void
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const iconMap: Record<string, LucideIcon> = {
|
|
34
|
-
Star,
|
|
35
|
-
Type,
|
|
36
|
-
Image,
|
|
37
|
-
LayoutGrid,
|
|
38
|
-
MousePointer,
|
|
39
|
-
Play,
|
|
40
|
-
Grid,
|
|
41
|
-
HelpCircle,
|
|
42
|
-
FileText,
|
|
43
|
-
Code,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function resolveIcon(name: string): LucideIcon {
|
|
47
|
-
return iconMap[name] ?? Box
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function BlockPicker({ open, onClose, onSelect }: BlockPickerProps) {
|
|
51
|
-
const [search, setSearch] = useState('')
|
|
52
|
-
const [selectedBlock, setSelectedBlock] = useState<BlockTypeDefinition | null>(null)
|
|
53
|
-
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
54
|
-
const variantListRef = useRef<HTMLDivElement>(null)
|
|
55
|
-
|
|
56
|
-
const catalog = useMemo(() => new BlockCatalog(), [])
|
|
57
|
-
const allBlocks = useMemo(() => catalog.getAll(), [catalog])
|
|
58
|
-
|
|
59
|
-
const filteredBlocks = useMemo(() => {
|
|
60
|
-
if (!search.trim()) return allBlocks
|
|
61
|
-
const q = search.toLowerCase()
|
|
62
|
-
return allBlocks.filter(
|
|
63
|
-
(block) =>
|
|
64
|
-
block.label.toLowerCase().includes(q) ||
|
|
65
|
-
(block.description?.toLowerCase().includes(q) ?? false),
|
|
66
|
-
)
|
|
67
|
-
}, [allBlocks, search])
|
|
68
|
-
|
|
69
|
-
const resetState = useCallback(() => {
|
|
70
|
-
setSearch('')
|
|
71
|
-
setSelectedBlock(null)
|
|
72
|
-
}, [])
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (open) {
|
|
76
|
-
resetState()
|
|
77
|
-
}
|
|
78
|
-
}, [open, resetState])
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (open && !selectedBlock) {
|
|
82
|
-
const timer = setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
83
|
-
return () => clearTimeout(timer)
|
|
84
|
-
}
|
|
85
|
-
}, [open, selectedBlock])
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (selectedBlock && variantListRef.current) {
|
|
89
|
-
const firstButton =
|
|
90
|
-
variantListRef.current.querySelector<HTMLButtonElement>('button[data-variant]')
|
|
91
|
-
firstButton?.focus()
|
|
92
|
-
}
|
|
93
|
-
}, [selectedBlock])
|
|
94
|
-
|
|
95
|
-
function handleBlockClick(block: BlockTypeDefinition) {
|
|
96
|
-
if (block.variants.length === 1) {
|
|
97
|
-
onSelect(block.type, block.variants[0]!.name)
|
|
98
|
-
onClose()
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
setSelectedBlock(block)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function handleVariantClick(variant: VariantDefinition) {
|
|
105
|
-
if (!selectedBlock) return
|
|
106
|
-
onSelect(selectedBlock.type, variant.name)
|
|
107
|
-
onClose()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function handleBack() {
|
|
111
|
-
setSelectedBlock(null)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function handleKeyDownOnGrid(e: React.KeyboardEvent) {
|
|
115
|
-
if (e.key === 'Escape') {
|
|
116
|
-
e.stopPropagation()
|
|
117
|
-
onClose()
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function handleKeyDownOnVariants(e: React.KeyboardEvent) {
|
|
122
|
-
if (e.key === 'Escape') {
|
|
123
|
-
e.stopPropagation()
|
|
124
|
-
handleBack()
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<Dialog.Root open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
130
|
-
<Dialog.Portal container={adminPortalContainer()}>
|
|
131
|
-
<Dialog.Overlay className="motion-safe:animate-in motion-safe:fade-in-0 fixed inset-0 z-50 bg-black/50" />
|
|
132
|
-
<Dialog.Content
|
|
133
|
-
className="bg-card border-border fixed top-1/2 left-1/2 z-50 flex max-h-[80vh] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border shadow-2xl"
|
|
134
|
-
aria-describedby={undefined}
|
|
135
|
-
onEscapeKeyDown={() => {
|
|
136
|
-
if (selectedBlock) {
|
|
137
|
-
handleBack()
|
|
138
|
-
}
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
{selectedBlock ? (
|
|
142
|
-
<VariantView
|
|
143
|
-
block={selectedBlock}
|
|
144
|
-
variantListRef={variantListRef}
|
|
145
|
-
onBack={handleBack}
|
|
146
|
-
onVariantClick={handleVariantClick}
|
|
147
|
-
onKeyDown={handleKeyDownOnVariants}
|
|
148
|
-
/>
|
|
149
|
-
) : (
|
|
150
|
-
<BlockTypeGrid
|
|
151
|
-
search={search}
|
|
152
|
-
filteredBlocks={filteredBlocks}
|
|
153
|
-
searchInputRef={searchInputRef}
|
|
154
|
-
onSearchChange={setSearch}
|
|
155
|
-
onBlockClick={handleBlockClick}
|
|
156
|
-
onClose={onClose}
|
|
157
|
-
onKeyDown={handleKeyDownOnGrid}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
</Dialog.Content>
|
|
161
|
-
</Dialog.Portal>
|
|
162
|
-
</Dialog.Root>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
interface BlockTypeGridProps {
|
|
167
|
-
search: string
|
|
168
|
-
filteredBlocks: BlockTypeDefinition[]
|
|
169
|
-
searchInputRef: React.RefObject<HTMLInputElement | null>
|
|
170
|
-
onSearchChange: (value: string) => void
|
|
171
|
-
onBlockClick: (block: BlockTypeDefinition) => void
|
|
172
|
-
onClose: () => void
|
|
173
|
-
onKeyDown: (e: React.KeyboardEvent) => void
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function BlockTypeGrid({
|
|
177
|
-
search,
|
|
178
|
-
filteredBlocks,
|
|
179
|
-
searchInputRef,
|
|
180
|
-
onSearchChange,
|
|
181
|
-
onBlockClick,
|
|
182
|
-
onClose,
|
|
183
|
-
onKeyDown,
|
|
184
|
-
}: BlockTypeGridProps) {
|
|
185
|
-
return (
|
|
186
|
-
<div className="flex h-full flex-col" onKeyDown={onKeyDown}>
|
|
187
|
-
<div className="flex items-center justify-between gap-3 px-5 pt-5 pb-3">
|
|
188
|
-
<Dialog.Title className="text-foreground text-lg font-medium">Add Block</Dialog.Title>
|
|
189
|
-
<Dialog.Close asChild>
|
|
190
|
-
<button
|
|
191
|
-
type="button"
|
|
192
|
-
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-md p-1.5 transition-colors"
|
|
193
|
-
aria-label="Close"
|
|
194
|
-
>
|
|
195
|
-
<X size={18} />
|
|
196
|
-
</button>
|
|
197
|
-
</Dialog.Close>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="px-5 pb-3">
|
|
201
|
-
<div className="relative">
|
|
202
|
-
<Search
|
|
203
|
-
className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
|
204
|
-
size={16}
|
|
205
|
-
/>
|
|
206
|
-
<input
|
|
207
|
-
ref={searchInputRef}
|
|
208
|
-
type="text"
|
|
209
|
-
placeholder="Search blocks..."
|
|
210
|
-
value={search}
|
|
211
|
-
onChange={(e) => onSearchChange(e.target.value)}
|
|
212
|
-
className="bg-background border-input focus:ring-ring w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none"
|
|
213
|
-
aria-label="Search block types"
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
|
|
218
|
-
<div className="flex-1 overflow-y-auto px-5 pb-5">
|
|
219
|
-
{filteredBlocks.length === 0 ? (
|
|
220
|
-
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
221
|
-
<Search size={24} className="text-muted-foreground mb-2" />
|
|
222
|
-
<p className="text-muted-foreground text-sm">No blocks match “{search}”</p>
|
|
223
|
-
</div>
|
|
224
|
-
) : (
|
|
225
|
-
<div className="grid grid-cols-2 gap-3" role="list" aria-label="Available block types">
|
|
226
|
-
{filteredBlocks.map((block) => {
|
|
227
|
-
const Icon = resolveIcon(block.icon)
|
|
228
|
-
return (
|
|
229
|
-
<button
|
|
230
|
-
key={block.type}
|
|
231
|
-
type="button"
|
|
232
|
-
role="listitem"
|
|
233
|
-
onClick={() => onBlockClick(block)}
|
|
234
|
-
className="border-border hover:border-primary bg-card group focus:ring-ring flex cursor-pointer items-start gap-3 rounded-lg border p-4 text-left transition-colors focus:ring-2 focus:outline-none"
|
|
235
|
-
>
|
|
236
|
-
<div className="bg-accent text-foreground group-hover:bg-primary/10 group-hover:text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-md transition-colors">
|
|
237
|
-
<Icon size={18} />
|
|
238
|
-
</div>
|
|
239
|
-
<div className="min-w-0">
|
|
240
|
-
<p className="text-foreground truncate text-sm font-medium">{block.label}</p>
|
|
241
|
-
{block.description && (
|
|
242
|
-
<p className="text-muted-foreground mt-0.5 line-clamp-2 text-xs">
|
|
243
|
-
{block.description}
|
|
244
|
-
</p>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
|
-
</button>
|
|
248
|
-
)
|
|
249
|
-
})}
|
|
250
|
-
</div>
|
|
251
|
-
)}
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
interface VariantViewProps {
|
|
258
|
-
block: BlockTypeDefinition
|
|
259
|
-
variantListRef: React.RefObject<HTMLDivElement | null>
|
|
260
|
-
onBack: () => void
|
|
261
|
-
onVariantClick: (variant: VariantDefinition) => void
|
|
262
|
-
onKeyDown: (e: React.KeyboardEvent) => void
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function VariantView({
|
|
266
|
-
block,
|
|
267
|
-
variantListRef,
|
|
268
|
-
onBack,
|
|
269
|
-
onVariantClick,
|
|
270
|
-
onKeyDown,
|
|
271
|
-
}: VariantViewProps) {
|
|
272
|
-
const Icon = resolveIcon(block.icon)
|
|
273
|
-
|
|
274
|
-
return (
|
|
275
|
-
<div className="flex h-full flex-col" onKeyDown={onKeyDown}>
|
|
276
|
-
<div className="flex items-center gap-3 px-5 pt-5 pb-3">
|
|
277
|
-
<button
|
|
278
|
-
type="button"
|
|
279
|
-
onClick={onBack}
|
|
280
|
-
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-md p-1.5 transition-colors"
|
|
281
|
-
aria-label="Back to block types"
|
|
282
|
-
>
|
|
283
|
-
<ArrowLeft size={18} />
|
|
284
|
-
</button>
|
|
285
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
286
|
-
<Icon size={18} className="text-foreground shrink-0" />
|
|
287
|
-
<h2 className="text-foreground truncate text-lg font-medium">{block.label}</h2>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
{block.description && (
|
|
292
|
-
<p className="text-muted-foreground px-5 pb-3 text-sm">{block.description}</p>
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
<div className="flex-1 overflow-y-auto px-5 pb-5" ref={variantListRef}>
|
|
296
|
-
<div className="space-y-2" role="list" aria-label={`${block.label} variants`}>
|
|
297
|
-
{block.variants.map((variant, index) => (
|
|
298
|
-
<button
|
|
299
|
-
key={variant.name}
|
|
300
|
-
type="button"
|
|
301
|
-
role="listitem"
|
|
302
|
-
data-variant={variant.name}
|
|
303
|
-
onClick={() => onVariantClick(variant)}
|
|
304
|
-
className="border-border hover:border-primary bg-card group focus:ring-ring flex w-full cursor-pointer items-center gap-3 rounded-lg border p-4 text-left transition-colors focus:ring-2 focus:outline-none"
|
|
305
|
-
>
|
|
306
|
-
<div className="min-w-0 flex-1">
|
|
307
|
-
<div className="flex items-center gap-2">
|
|
308
|
-
<p className="text-foreground text-sm font-medium">{variant.label}</p>
|
|
309
|
-
{index === 0 && (
|
|
310
|
-
<span className="text-primary bg-primary/10 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs">
|
|
311
|
-
<Check size={10} />
|
|
312
|
-
Default
|
|
313
|
-
</span>
|
|
314
|
-
)}
|
|
315
|
-
</div>
|
|
316
|
-
<p className="text-muted-foreground mt-0.5 text-xs">{variant.description}</p>
|
|
317
|
-
</div>
|
|
318
|
-
</button>
|
|
319
|
-
))}
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
)
|
|
324
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Plus, Grid3X3 } from 'lucide-react'
|
|
4
|
-
import { Toggle } from '../../components/ui/Toggle.js'
|
|
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
|
|
28
|
-
className="bg-card border-border flex h-10 shrink-0 items-center gap-4 border-t px-4"
|
|
29
|
-
role="toolbar"
|
|
30
|
-
aria-label="Builder actions"
|
|
31
|
-
>
|
|
32
|
-
{/* Add Section */}
|
|
33
|
-
<button
|
|
34
|
-
onClick={onAddSection}
|
|
35
|
-
className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 px-3 py-2 text-sm transition-colors"
|
|
36
|
-
aria-label="Add a new section"
|
|
37
|
-
>
|
|
38
|
-
<Plus size={14} />
|
|
39
|
-
<span>Add Section</span>
|
|
40
|
-
</button>
|
|
41
|
-
|
|
42
|
-
{/* Divider */}
|
|
43
|
-
<div className="bg-border h-5 w-px" />
|
|
44
|
-
|
|
45
|
-
{/* Grid overlay toggle */}
|
|
46
|
-
<label className="flex cursor-pointer items-center gap-2">
|
|
47
|
-
<Grid3X3 size={14} className="text-muted-foreground" />
|
|
48
|
-
<Toggle
|
|
49
|
-
checked={showGridOverlay}
|
|
50
|
-
onCheckedChange={onToggleGrid}
|
|
51
|
-
aria-label="Toggle grid overlay"
|
|
52
|
-
/>
|
|
53
|
-
<span className="text-muted-foreground text-xs">Grid</span>
|
|
54
|
-
</label>
|
|
55
|
-
|
|
56
|
-
{/* Spacer */}
|
|
57
|
-
<div className="flex-1" />
|
|
58
|
-
|
|
59
|
-
{/* Zoom / device width display */}
|
|
60
|
-
<span className="text-muted-foreground text-xs tabular-nums">
|
|
61
|
-
{DEVICE_LABELS[deviceMode]}
|
|
62
|
-
</span>
|
|
63
|
-
</div>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react'
|
|
4
|
-
import {
|
|
5
|
-
DndContext,
|
|
6
|
-
KeyboardSensor,
|
|
7
|
-
PointerSensor,
|
|
8
|
-
closestCenter,
|
|
9
|
-
useSensor,
|
|
10
|
-
useSensors,
|
|
11
|
-
type DragEndEvent,
|
|
12
|
-
} from '@dnd-kit/core'
|
|
13
|
-
import {
|
|
14
|
-
SortableContext,
|
|
15
|
-
sortableKeyboardCoordinates,
|
|
16
|
-
useSortable,
|
|
17
|
-
verticalListSortingStrategy,
|
|
18
|
-
} from '@dnd-kit/sortable'
|
|
19
|
-
import { CSS } from '@dnd-kit/utilities'
|
|
20
|
-
import {
|
|
21
|
-
Bookmark,
|
|
22
|
-
Code,
|
|
23
|
-
FileText,
|
|
24
|
-
Grid,
|
|
25
|
-
GripVertical,
|
|
26
|
-
HelpCircle,
|
|
27
|
-
Image,
|
|
28
|
-
LayoutGrid,
|
|
29
|
-
LayoutTemplate,
|
|
30
|
-
MousePointer,
|
|
31
|
-
PanelLeftClose,
|
|
32
|
-
PanelLeftOpen,
|
|
33
|
-
Play,
|
|
34
|
-
Plus,
|
|
35
|
-
Star,
|
|
36
|
-
Type,
|
|
37
|
-
type LucideIcon,
|
|
38
|
-
} from 'lucide-react'
|
|
39
|
-
import { BlockCatalog } from '@prenta/core/page-builder'
|
|
40
|
-
import type { BuilderNode, PageNode } from '@prenta/core'
|
|
41
|
-
|
|
42
|
-
export interface BuilderSectionsPanelProps {
|
|
43
|
-
tree: PageNode
|
|
44
|
-
selectedNodeId: string | null
|
|
45
|
-
/** Collapsed to a slim rail. State is owned by PageBuilder (persisted). */
|
|
46
|
-
collapsed: boolean
|
|
47
|
-
onToggleCollapsed: () => void
|
|
48
|
-
onSelect: (id: string) => void
|
|
49
|
-
/** Move a top-level section to `toIndex` within the page. */
|
|
50
|
-
onReorder: (id: string, toIndex: number) => void
|
|
51
|
-
onAddSection: () => void
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface SectionEntry {
|
|
55
|
-
id: string
|
|
56
|
-
name: string
|
|
57
|
-
meta: string
|
|
58
|
-
icon: LucideIcon
|
|
59
|
-
hidden: boolean
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Same icon-name → component mapping the BlockPicker uses for the catalog. */
|
|
63
|
-
const BLOCK_ICONS: Record<string, LucideIcon> = {
|
|
64
|
-
Star,
|
|
65
|
-
Type,
|
|
66
|
-
Image,
|
|
67
|
-
LayoutGrid,
|
|
68
|
-
MousePointer,
|
|
69
|
-
Play,
|
|
70
|
-
Grid,
|
|
71
|
-
HelpCircle,
|
|
72
|
-
FileText,
|
|
73
|
-
Code,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function collectBlockTypes(node: BuilderNode, out: string[]): void {
|
|
77
|
-
if (node.type === 'block') {
|
|
78
|
-
out.push(node.settings.blockType)
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
if (node.type === 'savedSectionRef') return
|
|
82
|
-
for (const child of node.children) collectBlockTypes(child, out)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function humanize(type: string): string {
|
|
86
|
-
return type.charAt(0).toUpperCase() + type.slice(1)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Builder sections have no user-facing title, so each row derives its name
|
|
91
|
-
* from the section's first block (via the catalog label) and summarizes its
|
|
92
|
-
* contents as the kind label — enough for the editor to recognize and jump
|
|
93
|
-
* between sections without inventing data we don't store.
|
|
94
|
-
*/
|
|
95
|
-
function buildEntries(tree: PageNode, catalog: BlockCatalog): SectionEntry[] {
|
|
96
|
-
return tree.children.map((node, index) => {
|
|
97
|
-
if (node.type === 'savedSectionRef') {
|
|
98
|
-
return {
|
|
99
|
-
id: node.id,
|
|
100
|
-
name: 'Saved section',
|
|
101
|
-
meta: 'From the section library',
|
|
102
|
-
icon: Bookmark,
|
|
103
|
-
hidden: false,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const blockTypes: string[] = []
|
|
107
|
-
collectBlockTypes(node, blockTypes)
|
|
108
|
-
const firstType = blockTypes[0]
|
|
109
|
-
const def = firstType ? catalog.get(firstType) : undefined
|
|
110
|
-
return {
|
|
111
|
-
id: node.id,
|
|
112
|
-
name: def?.label ?? (firstType ? humanize(firstType) : `Section ${index + 1}`),
|
|
113
|
-
meta:
|
|
114
|
-
blockTypes.length === 0
|
|
115
|
-
? 'Empty section'
|
|
116
|
-
: `${blockTypes.length} block${blockTypes.length === 1 ? '' : 's'}`,
|
|
117
|
-
icon: (def && BLOCK_ICONS[def.icon]) ?? LayoutTemplate,
|
|
118
|
-
hidden: node.settings.visibility === 'hidden',
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const ICON_BUTTON_CLASS =
|
|
124
|
-
'flex h-8 w-8 items-center justify-center rounded-[7px] text-[var(--sub)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none'
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Design-parity left rail for the Page Builder (PageBuilder.html): the page's
|
|
128
|
-
* sections listed in order with drag-to-reorder, click-to-select (which
|
|
129
|
-
* highlights the section on the canvas and opens its settings in the existing
|
|
130
|
-
* right-hand context panel), and an "Add Section" affordance at the bottom.
|
|
131
|
-
*/
|
|
132
|
-
export function BuilderSectionsPanel({
|
|
133
|
-
tree,
|
|
134
|
-
selectedNodeId,
|
|
135
|
-
collapsed,
|
|
136
|
-
onToggleCollapsed,
|
|
137
|
-
onSelect,
|
|
138
|
-
onReorder,
|
|
139
|
-
onAddSection,
|
|
140
|
-
}: BuilderSectionsPanelProps) {
|
|
141
|
-
const catalog = useMemo(() => new BlockCatalog(), [])
|
|
142
|
-
const entries = useMemo(() => buildEntries(tree, catalog), [tree, catalog])
|
|
143
|
-
|
|
144
|
-
const sensors = useSensors(
|
|
145
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
146
|
-
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
function handleDragEnd(event: DragEndEvent) {
|
|
150
|
-
const { active, over } = event
|
|
151
|
-
if (!over || active.id === over.id) return
|
|
152
|
-
const ids = entries.map((e) => e.id)
|
|
153
|
-
const to = ids.indexOf(String(over.id))
|
|
154
|
-
if (to === -1 || !ids.includes(String(active.id))) return
|
|
155
|
-
onReorder(String(active.id), to)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (collapsed) {
|
|
159
|
-
return (
|
|
160
|
-
<nav
|
|
161
|
-
aria-label="Page sections"
|
|
162
|
-
className="flex w-11 shrink-0 flex-col items-center border-r border-[var(--bdr)] bg-[var(--sb)] py-2"
|
|
163
|
-
>
|
|
164
|
-
<button
|
|
165
|
-
type="button"
|
|
166
|
-
onClick={onToggleCollapsed}
|
|
167
|
-
aria-expanded={false}
|
|
168
|
-
aria-label="Expand sections panel"
|
|
169
|
-
className={ICON_BUTTON_CLASS}
|
|
170
|
-
>
|
|
171
|
-
<PanelLeftOpen size={16} aria-hidden />
|
|
172
|
-
</button>
|
|
173
|
-
</nav>
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<nav
|
|
179
|
-
aria-label="Page sections"
|
|
180
|
-
className="flex w-[280px] shrink-0 flex-col overflow-hidden border-r border-[var(--bdr)] bg-[var(--sb)]"
|
|
181
|
-
>
|
|
182
|
-
<div className="flex items-center justify-between border-b border-[var(--bdr)] py-2 pr-2 pl-3.5">
|
|
183
|
-
<div className="flex items-center gap-2">
|
|
184
|
-
<h2 className="text-[13px] font-medium text-[var(--txt)]">Sections</h2>
|
|
185
|
-
<span className="rounded-[4px] bg-[var(--acc-l)] px-[7px] py-0.5 text-[11px] font-medium text-[var(--acc)] tabular-nums">
|
|
186
|
-
{entries.length}
|
|
187
|
-
</span>
|
|
188
|
-
</div>
|
|
189
|
-
<button
|
|
190
|
-
type="button"
|
|
191
|
-
onClick={onToggleCollapsed}
|
|
192
|
-
aria-expanded
|
|
193
|
-
aria-label="Collapse sections panel"
|
|
194
|
-
className={ICON_BUTTON_CLASS}
|
|
195
|
-
>
|
|
196
|
-
<PanelLeftClose size={16} aria-hidden />
|
|
197
|
-
</button>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="flex-1 overflow-y-auto">
|
|
201
|
-
{entries.length === 0 ? (
|
|
202
|
-
<p className="px-4 py-6 text-center text-[12.5px] text-[var(--muted)]">
|
|
203
|
-
No sections yet. Add one below to start building.
|
|
204
|
-
</p>
|
|
205
|
-
) : (
|
|
206
|
-
<DndContext
|
|
207
|
-
sensors={sensors}
|
|
208
|
-
collisionDetection={closestCenter}
|
|
209
|
-
onDragEnd={handleDragEnd}
|
|
210
|
-
>
|
|
211
|
-
<SortableContext
|
|
212
|
-
items={entries.map((e) => e.id)}
|
|
213
|
-
strategy={verticalListSortingStrategy}
|
|
214
|
-
>
|
|
215
|
-
<ul aria-label="Sections in page order">
|
|
216
|
-
{entries.map((entry) => (
|
|
217
|
-
<SortableSectionRow
|
|
218
|
-
key={entry.id}
|
|
219
|
-
entry={entry}
|
|
220
|
-
selected={entry.id === selectedNodeId}
|
|
221
|
-
onSelect={onSelect}
|
|
222
|
-
/>
|
|
223
|
-
))}
|
|
224
|
-
</ul>
|
|
225
|
-
</SortableContext>
|
|
226
|
-
</DndContext>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<div className="border-t border-[var(--bdr)]">
|
|
231
|
-
<button
|
|
232
|
-
type="button"
|
|
233
|
-
onClick={onAddSection}
|
|
234
|
-
className="flex w-full items-center gap-2 px-3.5 py-3 text-[13px] font-medium text-[var(--acc)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none focus-visible:ring-inset"
|
|
235
|
-
>
|
|
236
|
-
<Plus size={14} aria-hidden />
|
|
237
|
-
Add Section
|
|
238
|
-
</button>
|
|
239
|
-
</div>
|
|
240
|
-
</nav>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
interface SortableSectionRowProps {
|
|
245
|
-
entry: SectionEntry
|
|
246
|
-
selected: boolean
|
|
247
|
-
onSelect: (id: string) => void
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function SortableSectionRow({ entry, selected, onSelect }: SortableSectionRowProps) {
|
|
251
|
-
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
252
|
-
id: entry.id,
|
|
253
|
-
})
|
|
254
|
-
const Icon = entry.icon
|
|
255
|
-
|
|
256
|
-
const style = {
|
|
257
|
-
transform: CSS.Transform.toString(transform),
|
|
258
|
-
transition,
|
|
259
|
-
zIndex: isDragging ? 10 : undefined,
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<li ref={setNodeRef} style={style} className={isDragging ? 'shadow-[var(--shd2)]' : undefined}>
|
|
264
|
-
<div
|
|
265
|
-
className={`flex items-center gap-1.5 border-b border-[var(--bdr)] py-2 pr-2.5 pl-1.5 transition-colors duration-[var(--motion-fast)] ${
|
|
266
|
-
selected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
267
|
-
}`}
|
|
268
|
-
>
|
|
269
|
-
<button
|
|
270
|
-
type="button"
|
|
271
|
-
aria-label={`Drag to reorder ${entry.name}`}
|
|
272
|
-
{...attributes}
|
|
273
|
-
{...listeners}
|
|
274
|
-
className="shrink-0 cursor-grab touch-none rounded p-1 text-[var(--muted)] transition-colors duration-[var(--motion-fast)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none active:cursor-grabbing"
|
|
275
|
-
>
|
|
276
|
-
<GripVertical size={15} aria-hidden />
|
|
277
|
-
</button>
|
|
278
|
-
<button
|
|
279
|
-
type="button"
|
|
280
|
-
onClick={() => onSelect(entry.id)}
|
|
281
|
-
aria-current={selected ? 'true' : undefined}
|
|
282
|
-
className="flex min-w-0 flex-1 items-center gap-2.5 rounded text-left focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
283
|
-
>
|
|
284
|
-
<span
|
|
285
|
-
aria-hidden
|
|
286
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] bg-[var(--acc-l)] text-[var(--acc)]"
|
|
287
|
-
>
|
|
288
|
-
<Icon size={14} />
|
|
289
|
-
</span>
|
|
290
|
-
<span className="flex min-w-0 flex-col">
|
|
291
|
-
<span
|
|
292
|
-
className={`truncate text-[13px] font-medium ${
|
|
293
|
-
entry.hidden ? 'text-[var(--muted)] line-through' : 'text-[var(--txt)]'
|
|
294
|
-
}`}
|
|
295
|
-
>
|
|
296
|
-
{entry.name}
|
|
297
|
-
</span>
|
|
298
|
-
<span className="truncate text-[11px] text-[var(--muted)]">{entry.meta}</span>
|
|
299
|
-
</span>
|
|
300
|
-
</button>
|
|
301
|
-
</div>
|
|
302
|
-
</li>
|
|
303
|
-
)
|
|
304
|
-
}
|