@prenta/admin 0.98.0 → 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 +12 -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 -10
- 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-audit-input.test.js +77 -1
- package/dist/__tests__/lib/page-audit-input.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-audit-input.d.ts +14 -1
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +19 -0
- package/dist/lib/page-audit-input.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 -10
- 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-audit-input.test.ts +91 -1
- 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-audit-input.ts +36 -1
- 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-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 -10
- package/dist/views/page-builder/SEOPanel.d.ts.map +0 -1
- package/dist/views/page-builder/SEOPanel.js +0 -103
- 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-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 -145
- 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 -464
- 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,523 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useMemo, useCallback } from 'react'
|
|
4
|
-
import { Star, Copy, Trash2 } from 'lucide-react'
|
|
5
|
-
import type { BlockNode, BlockTypeDefinition, FieldDefinition } from '@prenta/core'
|
|
6
|
-
import { BlockCatalog } from '@prenta/core/page-builder'
|
|
7
|
-
import { AIBlockAssist } from './AIBlockAssist.js'
|
|
8
|
-
import { RelationshipField } from './RelationshipField.js'
|
|
9
|
-
import { TipTapEditor } from '../../components/TipTapEditor.js'
|
|
10
|
-
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
11
|
-
import { Toggle } from '../../components/ui/Toggle.js'
|
|
12
|
-
|
|
13
|
-
export interface BlockEditorProps {
|
|
14
|
-
node: BlockNode
|
|
15
|
-
onUpdateBlock: (id: string, data: Record<string, unknown>) => void
|
|
16
|
-
onUpdateSettings: (id: string, settings: Record<string, unknown>) => void
|
|
17
|
-
onRemoveNode: (id: string) => void
|
|
18
|
-
onDuplicateNode: (id: string) => void
|
|
19
|
-
config: any
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const INPUT_CLASS =
|
|
23
|
-
'w-full px-3 py-2 text-sm bg-background border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring'
|
|
24
|
-
const LABEL_CLASS = 'text-sm font-medium text-foreground mb-1 block'
|
|
25
|
-
|
|
26
|
-
export function BlockEditor({
|
|
27
|
-
node,
|
|
28
|
-
onUpdateBlock,
|
|
29
|
-
onUpdateSettings,
|
|
30
|
-
onRemoveNode,
|
|
31
|
-
onDuplicateNode,
|
|
32
|
-
config,
|
|
33
|
-
}: BlockEditorProps) {
|
|
34
|
-
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
35
|
-
|
|
36
|
-
const catalog = useMemo(() => new BlockCatalog(), [])
|
|
37
|
-
const blockDef = useMemo(
|
|
38
|
-
() => catalog.get(node.settings.blockType),
|
|
39
|
-
[catalog, node.settings.blockType],
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
const handleFieldChange = useCallback(
|
|
43
|
-
(fieldName: string, value: unknown) => {
|
|
44
|
-
onUpdateBlock(node.id, { [fieldName]: value })
|
|
45
|
-
},
|
|
46
|
-
[node.id, onUpdateBlock],
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
const handleVariantChange = useCallback(
|
|
50
|
-
(variantName: string) => {
|
|
51
|
-
onUpdateSettings(node.id, { variant: variantName })
|
|
52
|
-
},
|
|
53
|
-
[node.id, onUpdateSettings],
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
const handleDelete = useCallback(() => {
|
|
57
|
-
if (confirmDelete) {
|
|
58
|
-
onRemoveNode(node.id)
|
|
59
|
-
setConfirmDelete(false)
|
|
60
|
-
} else {
|
|
61
|
-
setConfirmDelete(true)
|
|
62
|
-
}
|
|
63
|
-
}, [confirmDelete, node.id, onRemoveNode])
|
|
64
|
-
|
|
65
|
-
if (!blockDef) {
|
|
66
|
-
return (
|
|
67
|
-
<div className="p-4">
|
|
68
|
-
<p className="text-muted-foreground text-sm">
|
|
69
|
-
Unknown block type: <code className="text-xs">{node.settings.blockType}</code>
|
|
70
|
-
</p>
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div className="flex h-full flex-col">
|
|
77
|
-
<div className="border-border border-b p-4">
|
|
78
|
-
<div className="flex items-center gap-2">
|
|
79
|
-
<Star size={16} className="text-muted-foreground" />
|
|
80
|
-
<span className="text-foreground flex-1 text-sm font-medium">{blockDef.label}</span>
|
|
81
|
-
<AIBlockAssist block={node} onUpdateData={(data) => onUpdateBlock(node.id, data)} />
|
|
82
|
-
</div>
|
|
83
|
-
{blockDef.description && (
|
|
84
|
-
<p className="text-muted-foreground mt-1 text-xs">{blockDef.description}</p>
|
|
85
|
-
)}
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
{blockDef.variants.length > 1 && (
|
|
89
|
-
<div className="border-border border-b p-4">
|
|
90
|
-
<p className="text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase">
|
|
91
|
-
Variant
|
|
92
|
-
</p>
|
|
93
|
-
<div className="grid grid-cols-2 gap-1.5">
|
|
94
|
-
{blockDef.variants.map((variant) => (
|
|
95
|
-
<button
|
|
96
|
-
key={variant.name}
|
|
97
|
-
type="button"
|
|
98
|
-
onClick={() => handleVariantChange(variant.name)}
|
|
99
|
-
className={`rounded-md border px-2 py-1.5 text-xs transition-colors ${
|
|
100
|
-
node.settings.variant === variant.name
|
|
101
|
-
? 'bg-primary text-primary-foreground border-primary'
|
|
102
|
-
: 'bg-background border-input text-foreground hover:bg-accent'
|
|
103
|
-
}`}
|
|
104
|
-
>
|
|
105
|
-
{variant.label}
|
|
106
|
-
</button>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
|
|
112
|
-
<div className="flex-1 space-y-4 p-4">
|
|
113
|
-
<p className="text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase">
|
|
114
|
-
Fields
|
|
115
|
-
</p>
|
|
116
|
-
{Object.entries(blockDef.fields).map(([fieldName, fieldDef]) => (
|
|
117
|
-
<FieldRenderer
|
|
118
|
-
key={fieldName}
|
|
119
|
-
name={fieldName}
|
|
120
|
-
definition={fieldDef}
|
|
121
|
-
value={node.data[fieldName]}
|
|
122
|
-
onChange={(value) => handleFieldChange(fieldName, value)}
|
|
123
|
-
/>
|
|
124
|
-
))}
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<div className="border-border space-y-2 border-t p-4">
|
|
128
|
-
<button
|
|
129
|
-
type="button"
|
|
130
|
-
onClick={() => onDuplicateNode(node.id)}
|
|
131
|
-
className="bg-background border-input hover:bg-accent flex w-full items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors"
|
|
132
|
-
>
|
|
133
|
-
<Copy size={14} />
|
|
134
|
-
Duplicate
|
|
135
|
-
</button>
|
|
136
|
-
<button
|
|
137
|
-
type="button"
|
|
138
|
-
onClick={handleDelete}
|
|
139
|
-
onBlur={() => setConfirmDelete(false)}
|
|
140
|
-
className={`flex w-full items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
|
|
141
|
-
confirmDelete
|
|
142
|
-
? 'bg-destructive text-destructive-foreground'
|
|
143
|
-
: 'bg-background border-destructive text-destructive hover:bg-destructive/10 border'
|
|
144
|
-
}`}
|
|
145
|
-
>
|
|
146
|
-
<Trash2 size={14} />
|
|
147
|
-
{confirmDelete ? 'Click again to confirm' : 'Delete'}
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface FieldRendererProps {
|
|
155
|
-
name: string
|
|
156
|
-
definition: FieldDefinition
|
|
157
|
-
value: unknown
|
|
158
|
-
onChange: (value: unknown) => void
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function FieldRenderer({ name, definition, value, onChange }: FieldRendererProps) {
|
|
162
|
-
const label = definition.label || name
|
|
163
|
-
|
|
164
|
-
switch (definition.type) {
|
|
165
|
-
case 'text':
|
|
166
|
-
return (
|
|
167
|
-
<div>
|
|
168
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
169
|
-
<input
|
|
170
|
-
type="text"
|
|
171
|
-
value={(value as string) ?? ''}
|
|
172
|
-
onChange={(e) => onChange(e.target.value)}
|
|
173
|
-
placeholder={definition.admin?.placeholder}
|
|
174
|
-
className={INPUT_CLASS}
|
|
175
|
-
/>
|
|
176
|
-
</div>
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
case 'richText':
|
|
180
|
-
return (
|
|
181
|
-
<div>
|
|
182
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
183
|
-
<div className="border-input bg-background overflow-hidden rounded-md border">
|
|
184
|
-
<TipTapEditor
|
|
185
|
-
content={(value as string) ?? ''}
|
|
186
|
-
onChange={(html) => onChange(html)}
|
|
187
|
-
placeholder={definition.admin?.placeholder}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
case 'url':
|
|
194
|
-
return (
|
|
195
|
-
<div>
|
|
196
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
197
|
-
<input
|
|
198
|
-
type="url"
|
|
199
|
-
value={(value as string) ?? ''}
|
|
200
|
-
onChange={(e) => onChange(e.target.value)}
|
|
201
|
-
placeholder={definition.admin?.placeholder ?? 'https://'}
|
|
202
|
-
className={INPUT_CLASS}
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
case 'number':
|
|
208
|
-
return (
|
|
209
|
-
<div>
|
|
210
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
211
|
-
<input
|
|
212
|
-
type="number"
|
|
213
|
-
value={(value as number) ?? ''}
|
|
214
|
-
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
215
|
-
min={definition.min}
|
|
216
|
-
max={definition.max}
|
|
217
|
-
step={definition.step}
|
|
218
|
-
className={INPUT_CLASS}
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
case 'boolean':
|
|
224
|
-
return (
|
|
225
|
-
<div className="flex items-center justify-between">
|
|
226
|
-
<label className="text-foreground text-sm font-medium">{label}</label>
|
|
227
|
-
<Toggle
|
|
228
|
-
checked={!!value}
|
|
229
|
-
onCheckedChange={(checked) => onChange(checked)}
|
|
230
|
-
aria-label={label}
|
|
231
|
-
/>
|
|
232
|
-
</div>
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
case 'media':
|
|
236
|
-
return <MediaFieldRenderer label={label} value={value} onChange={onChange} />
|
|
237
|
-
|
|
238
|
-
case 'select':
|
|
239
|
-
return (
|
|
240
|
-
<div>
|
|
241
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
242
|
-
<select
|
|
243
|
-
value={(value as string) ?? ''}
|
|
244
|
-
onChange={(e) => onChange(e.target.value)}
|
|
245
|
-
className={INPUT_CLASS}
|
|
246
|
-
>
|
|
247
|
-
<option value="">Select...</option>
|
|
248
|
-
{definition.options.map((opt) => (
|
|
249
|
-
<option key={opt.value} value={opt.value}>
|
|
250
|
-
{opt.label}
|
|
251
|
-
</option>
|
|
252
|
-
))}
|
|
253
|
-
</select>
|
|
254
|
-
</div>
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
case 'array':
|
|
258
|
-
return <ArrayFieldRenderer label={label} value={value} onChange={onChange} />
|
|
259
|
-
|
|
260
|
-
case 'json':
|
|
261
|
-
return <JsonFieldRenderer label={label} value={value} onChange={onChange} />
|
|
262
|
-
|
|
263
|
-
case 'date':
|
|
264
|
-
return (
|
|
265
|
-
<div>
|
|
266
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
267
|
-
<input
|
|
268
|
-
type="date"
|
|
269
|
-
value={typeof value === 'string' && value ? value.slice(0, 10) : ''}
|
|
270
|
-
onChange={(e) => onChange(e.target.value || undefined)}
|
|
271
|
-
className={INPUT_CLASS}
|
|
272
|
-
/>
|
|
273
|
-
</div>
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
case 'slug':
|
|
277
|
-
return (
|
|
278
|
-
<div>
|
|
279
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
280
|
-
<input
|
|
281
|
-
type="text"
|
|
282
|
-
value={(value as string) ?? ''}
|
|
283
|
-
onChange={(e) => {
|
|
284
|
-
const slug = e.target.value
|
|
285
|
-
.toLowerCase()
|
|
286
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
287
|
-
.replace(/^-+|-+$/g, '')
|
|
288
|
-
onChange(slug)
|
|
289
|
-
}}
|
|
290
|
-
placeholder={definition.admin?.placeholder ?? 'page-slug'}
|
|
291
|
-
className={INPUT_CLASS}
|
|
292
|
-
/>
|
|
293
|
-
</div>
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
case 'relationship':
|
|
297
|
-
return (
|
|
298
|
-
<RelationshipField
|
|
299
|
-
definition={definition}
|
|
300
|
-
value={value}
|
|
301
|
-
onChange={onChange}
|
|
302
|
-
label={label}
|
|
303
|
-
/>
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
case 'email':
|
|
307
|
-
return (
|
|
308
|
-
<div>
|
|
309
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
310
|
-
<input
|
|
311
|
-
type="email"
|
|
312
|
-
value={(value as string) ?? ''}
|
|
313
|
-
onChange={(e) => onChange(e.target.value)}
|
|
314
|
-
placeholder={definition.admin?.placeholder ?? 'email@example.com'}
|
|
315
|
-
className={INPUT_CLASS}
|
|
316
|
-
/>
|
|
317
|
-
</div>
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
case 'color':
|
|
321
|
-
return (
|
|
322
|
-
<div>
|
|
323
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
324
|
-
<div className="flex items-center gap-2">
|
|
325
|
-
<input
|
|
326
|
-
type="color"
|
|
327
|
-
value={(value as string) ?? '#000000'}
|
|
328
|
-
onChange={(e) => onChange(e.target.value)}
|
|
329
|
-
className="border-input h-9 w-9 cursor-pointer rounded-md border"
|
|
330
|
-
/>
|
|
331
|
-
<input
|
|
332
|
-
type="text"
|
|
333
|
-
value={(value as string) ?? ''}
|
|
334
|
-
onChange={(e) => onChange(e.target.value)}
|
|
335
|
-
placeholder="#000000"
|
|
336
|
-
className={`${INPUT_CLASS} flex-1`}
|
|
337
|
-
/>
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
default:
|
|
343
|
-
return (
|
|
344
|
-
<div>
|
|
345
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
346
|
-
<input
|
|
347
|
-
type="text"
|
|
348
|
-
value={String(value ?? '')}
|
|
349
|
-
onChange={(e) => onChange(e.target.value)}
|
|
350
|
-
className={INPUT_CLASS}
|
|
351
|
-
/>
|
|
352
|
-
</div>
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface SimpleFieldProps {
|
|
358
|
-
label: string
|
|
359
|
-
value: unknown
|
|
360
|
-
onChange: (value: unknown) => void
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Block-level media picker. Wires the cross-cutting MediaPickerModal so
|
|
365
|
-
* custom blocks declared with `type: 'media'` actually get a working
|
|
366
|
-
* browse-and-select flow instead of a free-text input.
|
|
367
|
-
*/
|
|
368
|
-
function MediaFieldRenderer({ label, value, onChange }: SimpleFieldProps) {
|
|
369
|
-
const [open, setOpen] = useState(false)
|
|
370
|
-
const url =
|
|
371
|
-
typeof value === 'string'
|
|
372
|
-
? value
|
|
373
|
-
: value && typeof value === 'object' && 'url' in (value as any)
|
|
374
|
-
? String((value as any).url)
|
|
375
|
-
: ''
|
|
376
|
-
|
|
377
|
-
return (
|
|
378
|
-
<div>
|
|
379
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
380
|
-
<div className="flex gap-2">
|
|
381
|
-
<input
|
|
382
|
-
type="text"
|
|
383
|
-
value={url}
|
|
384
|
-
onChange={(e) => onChange(e.target.value)}
|
|
385
|
-
placeholder="https://..."
|
|
386
|
-
className={`${INPUT_CLASS} flex-1`}
|
|
387
|
-
/>
|
|
388
|
-
<button
|
|
389
|
-
type="button"
|
|
390
|
-
onClick={() => setOpen(true)}
|
|
391
|
-
className="bg-accent text-foreground border-input hover:bg-accent/80 rounded-md border px-3 py-2 text-xs font-medium transition-colors"
|
|
392
|
-
>
|
|
393
|
-
Browse
|
|
394
|
-
</button>
|
|
395
|
-
</div>
|
|
396
|
-
{url && /\.(png|jpe?g|gif|webp|avif|svg)(\?|$)/i.test(url) && (
|
|
397
|
-
<div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
|
|
398
|
-
{/* Plain img is fine here; admin only and we don't have next/image in scope. */}
|
|
399
|
-
<img src={url} alt="" className="bg-checkered h-32 w-full object-contain" />
|
|
400
|
-
</div>
|
|
401
|
-
)}
|
|
402
|
-
<MediaPickerModal
|
|
403
|
-
open={open}
|
|
404
|
-
onClose={() => setOpen(false)}
|
|
405
|
-
onSelect={(selectedUrl) => {
|
|
406
|
-
onChange(selectedUrl)
|
|
407
|
-
setOpen(false)
|
|
408
|
-
}}
|
|
409
|
-
/>
|
|
410
|
-
</div>
|
|
411
|
-
)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* JSON editor for free-form object/array fields. The textarea is the
|
|
416
|
-
* source of truth while the user is typing — we only commit a parsed
|
|
417
|
-
* value back upstream once the JSON is well-formed, otherwise the user
|
|
418
|
-
* sees a validation error inline. This prevents typing-induced state
|
|
419
|
-
* loss and keeps the UI usable for arrays of objects (a common shape
|
|
420
|
-
* for custom blocks).
|
|
421
|
-
*/
|
|
422
|
-
function JsonFieldRenderer({ label, value, onChange }: SimpleFieldProps) {
|
|
423
|
-
const initial = useMemo(() => {
|
|
424
|
-
if (value === undefined || value === null) return ''
|
|
425
|
-
try {
|
|
426
|
-
return JSON.stringify(value, null, 2)
|
|
427
|
-
} catch {
|
|
428
|
-
return ''
|
|
429
|
-
}
|
|
430
|
-
}, [value])
|
|
431
|
-
const [draft, setDraft] = useState(initial)
|
|
432
|
-
const [error, setError] = useState<string | null>(null)
|
|
433
|
-
|
|
434
|
-
const handleChange = (next: string) => {
|
|
435
|
-
setDraft(next)
|
|
436
|
-
if (next.trim() === '') {
|
|
437
|
-
setError(null)
|
|
438
|
-
onChange(undefined)
|
|
439
|
-
return
|
|
440
|
-
}
|
|
441
|
-
try {
|
|
442
|
-
onChange(JSON.parse(next))
|
|
443
|
-
setError(null)
|
|
444
|
-
} catch (err) {
|
|
445
|
-
setError(err instanceof Error ? err.message : 'Invalid JSON')
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
<div>
|
|
451
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
452
|
-
<textarea
|
|
453
|
-
value={draft}
|
|
454
|
-
onChange={(e) => handleChange(e.target.value)}
|
|
455
|
-
rows={6}
|
|
456
|
-
spellCheck={false}
|
|
457
|
-
className={`${INPUT_CLASS} resize-y font-mono text-xs`}
|
|
458
|
-
/>
|
|
459
|
-
{error && (
|
|
460
|
-
<p className="text-destructive mt-1 text-xs" role="alert">
|
|
461
|
-
{error}
|
|
462
|
-
</p>
|
|
463
|
-
)}
|
|
464
|
-
</div>
|
|
465
|
-
)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Lightweight array editor — supports homogenous arrays of strings or
|
|
470
|
-
* objects. For block fields we deliberately keep this simple: full
|
|
471
|
-
* nested-field editing belongs in the document-level FieldRenderer.
|
|
472
|
-
*/
|
|
473
|
-
function ArrayFieldRenderer({ label, value, onChange }: SimpleFieldProps) {
|
|
474
|
-
const items = Array.isArray(value) ? value : []
|
|
475
|
-
const isObjectArray = items.length > 0 && typeof items[0] === 'object' && items[0] !== null
|
|
476
|
-
|
|
477
|
-
if (isObjectArray) {
|
|
478
|
-
// For object arrays fall back to JSON — mirrors what most consumers
|
|
479
|
-
// expect when their block declares `type: 'array'` with no schema.
|
|
480
|
-
return <JsonFieldRenderer label={label} value={value} onChange={onChange} />
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const updateItem = (idx: number, next: string) => {
|
|
484
|
-
const copy = [...items]
|
|
485
|
-
copy[idx] = next
|
|
486
|
-
onChange(copy)
|
|
487
|
-
}
|
|
488
|
-
const addItem = () => onChange([...items, ''])
|
|
489
|
-
const removeItem = (idx: number) => onChange(items.filter((_, i) => i !== idx))
|
|
490
|
-
|
|
491
|
-
return (
|
|
492
|
-
<div>
|
|
493
|
-
<label className={LABEL_CLASS}>{label}</label>
|
|
494
|
-
<div className="space-y-2">
|
|
495
|
-
{items.map((item, idx) => (
|
|
496
|
-
<div key={idx} className="flex gap-2">
|
|
497
|
-
<input
|
|
498
|
-
type="text"
|
|
499
|
-
value={typeof item === 'string' ? item : ''}
|
|
500
|
-
onChange={(e) => updateItem(idx, e.target.value)}
|
|
501
|
-
className={`${INPUT_CLASS} flex-1`}
|
|
502
|
-
/>
|
|
503
|
-
<button
|
|
504
|
-
type="button"
|
|
505
|
-
onClick={() => removeItem(idx)}
|
|
506
|
-
aria-label={`Remove item ${idx + 1}`}
|
|
507
|
-
className="text-destructive border-input hover:bg-destructive/10 rounded-md border px-2 py-2 text-xs transition-colors"
|
|
508
|
-
>
|
|
509
|
-
<Trash2 size={14} />
|
|
510
|
-
</button>
|
|
511
|
-
</div>
|
|
512
|
-
))}
|
|
513
|
-
<button
|
|
514
|
-
type="button"
|
|
515
|
-
onClick={addItem}
|
|
516
|
-
className="bg-accent text-foreground border-input hover:bg-accent/80 w-full rounded-md border px-3 py-2 text-xs font-medium transition-colors"
|
|
517
|
-
>
|
|
518
|
-
+ Add item
|
|
519
|
-
</button>
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
)
|
|
523
|
-
}
|