@nuasite/cms 0.1.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/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { manifest } from '../signals'
|
|
3
|
+
import { LAYOUT } from '../constants'
|
|
4
|
+
import { getComponentDefinition, getComponentDefinitions, getComponentInstance } from '../manifest'
|
|
5
|
+
import type { ComponentProp, InsertPosition } from '../types'
|
|
6
|
+
|
|
7
|
+
export interface BlockEditorProps {
|
|
8
|
+
visible: boolean
|
|
9
|
+
componentId: string | null
|
|
10
|
+
rect: DOMRect | null
|
|
11
|
+
onClose: () => void
|
|
12
|
+
onUpdateProps: (componentId: string, props: Record<string, any>) => void
|
|
13
|
+
onInsertComponent: (
|
|
14
|
+
position: InsertPosition,
|
|
15
|
+
referenceComponentId: string,
|
|
16
|
+
componentName: string,
|
|
17
|
+
props: Record<string, any>,
|
|
18
|
+
) => void
|
|
19
|
+
onRemoveBlock: (componentId: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type EditorMode = 'edit' | 'insert-picker' | 'insert-props' | 'confirm-remove'
|
|
23
|
+
|
|
24
|
+
export function BlockEditor({
|
|
25
|
+
visible,
|
|
26
|
+
componentId,
|
|
27
|
+
rect,
|
|
28
|
+
onClose,
|
|
29
|
+
onUpdateProps,
|
|
30
|
+
onInsertComponent,
|
|
31
|
+
onRemoveBlock,
|
|
32
|
+
}: BlockEditorProps) {
|
|
33
|
+
const [mode, setMode] = useState<EditorMode>('edit')
|
|
34
|
+
const [insertPosition, setInsertPosition] = useState<InsertPosition>('after')
|
|
35
|
+
const [selectedComponent, setSelectedComponent] = useState<string | null>(null)
|
|
36
|
+
const [propValues, setPropValues] = useState<Record<string, any>>({})
|
|
37
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
38
|
+
const mockPreviewRef = useRef<HTMLElement | null>(null)
|
|
39
|
+
const removeOverlayRef = useRef<HTMLElement | null>(null)
|
|
40
|
+
const [editorPosition, setEditorPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 })
|
|
41
|
+
const componentDefinitions = getComponentDefinitions(manifest.value)
|
|
42
|
+
const currentInstance = componentId ? getComponentInstance(manifest.value, componentId) : null
|
|
43
|
+
const currentDefinition = currentInstance ? getComponentDefinition(manifest.value, currentInstance.componentName) : null
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (currentInstance) {
|
|
47
|
+
setPropValues(currentInstance.props || {})
|
|
48
|
+
}
|
|
49
|
+
}, [currentInstance])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!visible) return
|
|
53
|
+
|
|
54
|
+
const updatePosition = () => {
|
|
55
|
+
const editorWidth = LAYOUT.BLOCK_EDITOR_WIDTH
|
|
56
|
+
const editorHeight = LAYOUT.BLOCK_EDITOR_HEIGHT
|
|
57
|
+
const padding = LAYOUT.VIEWPORT_PADDING
|
|
58
|
+
const viewportWidth = window.innerWidth
|
|
59
|
+
const viewportHeight = window.innerHeight
|
|
60
|
+
|
|
61
|
+
let top: number
|
|
62
|
+
let left: number
|
|
63
|
+
|
|
64
|
+
if (rect) {
|
|
65
|
+
top = rect.bottom + padding
|
|
66
|
+
left = rect.left
|
|
67
|
+
|
|
68
|
+
if (top + editorHeight > viewportHeight - padding) {
|
|
69
|
+
top = Math.max(padding, rect.top - editorHeight - padding)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (top < padding) {
|
|
73
|
+
top = Math.max(padding, (viewportHeight - editorHeight) / 2)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (left + editorWidth > viewportWidth - padding) {
|
|
77
|
+
left = viewportWidth - editorWidth - padding
|
|
78
|
+
}
|
|
79
|
+
if (left < padding) {
|
|
80
|
+
left = padding
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
top = (viewportHeight - editorHeight) / 2
|
|
84
|
+
left = (viewportWidth - editorWidth) / 2
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setEditorPosition({ top, left })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
updatePosition()
|
|
91
|
+
window.addEventListener('resize', updatePosition)
|
|
92
|
+
window.addEventListener('scroll', updatePosition)
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
window.removeEventListener('resize', updatePosition)
|
|
96
|
+
window.removeEventListener('scroll', updatePosition)
|
|
97
|
+
}
|
|
98
|
+
}, [visible, rect])
|
|
99
|
+
|
|
100
|
+
// Inject/remove inline mock preview into the real page at the insertion point
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (mode !== 'insert-props' || !selectedComponent || !componentId) {
|
|
103
|
+
// Clean up if we exit insert-props mode
|
|
104
|
+
if (mockPreviewRef.current) {
|
|
105
|
+
mockPreviewRef.current.remove()
|
|
106
|
+
mockPreviewRef.current = null
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const def = componentDefinitions[selectedComponent]
|
|
112
|
+
if (!def?.previewUrl) return
|
|
113
|
+
|
|
114
|
+
// Find the reference component element in the page
|
|
115
|
+
const refEl = document.querySelector(`[data-cms-component-id="${componentId}"]`)
|
|
116
|
+
if (!refEl) return
|
|
117
|
+
|
|
118
|
+
// Create the mock wrapper
|
|
119
|
+
const mockEl = document.createElement('div')
|
|
120
|
+
mockEl.setAttribute('data-cms-preview-mock', 'true')
|
|
121
|
+
mockEl.style.cssText =
|
|
122
|
+
'outline: 2px dashed rgba(59, 130, 246, 0.6); outline-offset: -2px; position: relative; opacity: 0.75; transition: opacity 0.2s;'
|
|
123
|
+
|
|
124
|
+
// Insert at the correct position
|
|
125
|
+
if (insertPosition === 'before') {
|
|
126
|
+
refEl.parentNode?.insertBefore(mockEl, refEl)
|
|
127
|
+
} else {
|
|
128
|
+
refEl.parentNode?.insertBefore(mockEl, refEl.nextSibling)
|
|
129
|
+
}
|
|
130
|
+
mockPreviewRef.current = mockEl
|
|
131
|
+
|
|
132
|
+
// Fetch preview HTML and inject the component content
|
|
133
|
+
fetch(def.previewUrl)
|
|
134
|
+
.then((res) => res.text())
|
|
135
|
+
.then((html) => {
|
|
136
|
+
const parser = new DOMParser()
|
|
137
|
+
const doc = parser.parseFromString(html, 'text/html')
|
|
138
|
+
const container = doc.querySelector('.cms-preview-container')
|
|
139
|
+
if (container && mockPreviewRef.current) {
|
|
140
|
+
mockPreviewRef.current.innerHTML = container.innerHTML
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {
|
|
144
|
+
// Silently ignore fetch errors - the mock just stays empty
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Scroll the mock into view
|
|
148
|
+
mockEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
mockEl.remove()
|
|
152
|
+
mockPreviewRef.current = null
|
|
153
|
+
}
|
|
154
|
+
}, [mode, selectedComponent, componentId, insertPosition, componentDefinitions[selectedComponent ?? '']])
|
|
155
|
+
|
|
156
|
+
// Update text props in the inline mock when propValues change
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (mode !== 'insert-props' || !mockPreviewRef.current) return
|
|
159
|
+
const propEls = mockPreviewRef.current.querySelectorAll('[data-cms-preview-prop]')
|
|
160
|
+
for (const el of propEls) {
|
|
161
|
+
const propName = el.getAttribute('data-cms-preview-prop')
|
|
162
|
+
if (propName && propValues[propName] !== undefined) {
|
|
163
|
+
el.textContent = String(propValues[propName])
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, [propValues, mode])
|
|
167
|
+
|
|
168
|
+
// Show red overlay on the component when in confirm-remove mode
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (mode !== 'confirm-remove' || !componentId) {
|
|
171
|
+
if (removeOverlayRef.current) {
|
|
172
|
+
removeOverlayRef.current.remove()
|
|
173
|
+
removeOverlayRef.current = null
|
|
174
|
+
}
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const targetEl = document.querySelector(`[data-cms-component-id="${componentId}"]`) as HTMLElement | null
|
|
179
|
+
if (!targetEl) return
|
|
180
|
+
|
|
181
|
+
// Create overlay positioned on top of the component
|
|
182
|
+
const overlay = document.createElement('div')
|
|
183
|
+
overlay.setAttribute('data-cms-remove-overlay', 'true')
|
|
184
|
+
overlay.style.cssText =
|
|
185
|
+
'position: absolute; inset: 0; background: rgba(239, 68, 68, 0.15); outline: 2px dashed rgba(239, 68, 68, 0.7); outline-offset: -2px; pointer-events: none; z-index: 9999; transition: opacity 0.2s;'
|
|
186
|
+
|
|
187
|
+
// Ensure the target is positioned so the overlay can cover it
|
|
188
|
+
const originalPosition = targetEl.style.position
|
|
189
|
+
if (getComputedStyle(targetEl).position === 'static') {
|
|
190
|
+
targetEl.style.position = 'relative'
|
|
191
|
+
}
|
|
192
|
+
targetEl.appendChild(overlay)
|
|
193
|
+
removeOverlayRef.current = overlay
|
|
194
|
+
|
|
195
|
+
targetEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
overlay.remove()
|
|
199
|
+
removeOverlayRef.current = null
|
|
200
|
+
// Restore original position if we changed it
|
|
201
|
+
if (originalPosition !== undefined) {
|
|
202
|
+
targetEl.style.position = originalPosition
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, [mode, componentId])
|
|
206
|
+
|
|
207
|
+
const handlePropChange = (propName: string, value: string) => {
|
|
208
|
+
setPropValues(prev => ({ ...prev, [propName]: value }))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const handleSave = () => {
|
|
212
|
+
if (componentId) {
|
|
213
|
+
onUpdateProps(componentId, propValues)
|
|
214
|
+
onClose()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const handleStartInsert = (position: InsertPosition) => {
|
|
219
|
+
setInsertPosition(position)
|
|
220
|
+
setMode('insert-picker')
|
|
221
|
+
setSelectedComponent(null)
|
|
222
|
+
setPropValues({})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const handleSelectComponentForInsert = (componentName: string) => {
|
|
226
|
+
const definition = componentDefinitions[componentName]
|
|
227
|
+
if (!definition) return
|
|
228
|
+
|
|
229
|
+
// Initialize with default values
|
|
230
|
+
const defaultProps: Record<string, any> = {}
|
|
231
|
+
for (const prop of definition.props) {
|
|
232
|
+
if (prop.defaultValue !== undefined) {
|
|
233
|
+
defaultProps[prop.name] = prop.defaultValue
|
|
234
|
+
} else if (prop.required) {
|
|
235
|
+
defaultProps[prop.name] = ''
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setSelectedComponent(componentName)
|
|
240
|
+
setPropValues(defaultProps)
|
|
241
|
+
setMode('insert-props')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const handleConfirmInsert = () => {
|
|
245
|
+
if (selectedComponent && componentId) {
|
|
246
|
+
onInsertComponent(insertPosition, componentId, selectedComponent, propValues)
|
|
247
|
+
onClose()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const handleBackToEdit = () => {
|
|
252
|
+
setMode('edit')
|
|
253
|
+
setSelectedComponent(null)
|
|
254
|
+
setPropValues(currentInstance?.props || {})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!visible) return null
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<>
|
|
261
|
+
{/* Backdrop overlay — transparent so the page remains visible */}
|
|
262
|
+
<div
|
|
263
|
+
data-cms-ui
|
|
264
|
+
onClick={onClose}
|
|
265
|
+
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
|
266
|
+
class="fixed inset-0 z-2147483646"
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
{/* Editor panel */}
|
|
270
|
+
<div
|
|
271
|
+
ref={containerRef}
|
|
272
|
+
data-cms-ui
|
|
273
|
+
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
|
274
|
+
onClick={(e: MouseEvent) => e.stopPropagation()}
|
|
275
|
+
class="fixed z-2147483647 w-100 max-w-[calc(100vw-32px)] max-h-[calc(100vh-32px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] font-sans text-sm overflow-hidden flex flex-col rounded-cms-xl border border-white/10"
|
|
276
|
+
style={{
|
|
277
|
+
top: `${editorPosition.top}px`,
|
|
278
|
+
left: `${editorPosition.left}px`,
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
{/* Header */}
|
|
282
|
+
<div class="px-5 py-4 flex justify-between items-center border-b border-white/10">
|
|
283
|
+
<span class="font-semibold text-white">
|
|
284
|
+
{mode === 'edit'
|
|
285
|
+
? (
|
|
286
|
+
currentDefinition ? `Edit ${currentDefinition.name}` : 'Block Editor'
|
|
287
|
+
)
|
|
288
|
+
: mode === 'confirm-remove'
|
|
289
|
+
? (
|
|
290
|
+
`Remove ${currentDefinition?.name ?? 'Component'}`
|
|
291
|
+
)
|
|
292
|
+
: mode === 'insert-picker'
|
|
293
|
+
? (
|
|
294
|
+
`Insert ${insertPosition === 'before' ? 'Before' : 'After'}`
|
|
295
|
+
)
|
|
296
|
+
: (
|
|
297
|
+
`Add ${selectedComponent}`
|
|
298
|
+
)}
|
|
299
|
+
</span>
|
|
300
|
+
<button
|
|
301
|
+
onClick={onClose}
|
|
302
|
+
class="bg-white/10 border-none cursor-pointer p-1.5 text-white/80 hover:bg-white/20 hover:text-white transition-colors rounded-full w-8 h-8 flex items-center justify-center text-lg"
|
|
303
|
+
>
|
|
304
|
+
✕
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{/* Content */}
|
|
309
|
+
<div class="p-5 overflow-y-auto flex-1 bg-cms-dark">
|
|
310
|
+
{mode === 'edit' && currentDefinition
|
|
311
|
+
? (
|
|
312
|
+
<>
|
|
313
|
+
{/* Insert buttons */}
|
|
314
|
+
<div class="mb-5 flex gap-2">
|
|
315
|
+
<button
|
|
316
|
+
onClick={() => handleStartInsert('before')}
|
|
317
|
+
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-md cursor-pointer text-[13px] font-medium flex items-center justify-center gap-1.5 hover:bg-white/20 hover:text-white transition-colors"
|
|
318
|
+
>
|
|
319
|
+
<span class="text-base">↑</span> Insert before
|
|
320
|
+
</button>
|
|
321
|
+
<button
|
|
322
|
+
onClick={() => handleStartInsert('after')}
|
|
323
|
+
class="flex-1 py-2.5 px-3 bg-white/10 text-white/80 rounded-cms-md cursor-pointer text-[13px] font-medium flex items-center justify-center gap-1.5 hover:bg-white/20 hover:text-white transition-colors"
|
|
324
|
+
>
|
|
325
|
+
<span class="text-base">↓</span> Insert after
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{/* Props editor */}
|
|
330
|
+
<div class="mb-5">
|
|
331
|
+
<div class="text-xs font-medium text-white/50 tracking-wide mb-3 uppercase">
|
|
332
|
+
Properties
|
|
333
|
+
</div>
|
|
334
|
+
{currentDefinition.props.map((prop) => (
|
|
335
|
+
<PropEditor
|
|
336
|
+
key={prop.name}
|
|
337
|
+
prop={prop}
|
|
338
|
+
value={propValues[prop.name] || ''}
|
|
339
|
+
onChange={(value) => handlePropChange(prop.name, value)}
|
|
340
|
+
/>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Actions */}
|
|
345
|
+
<div class="flex gap-2 justify-between pt-4 border-t border-white/10 mt-4">
|
|
346
|
+
<button
|
|
347
|
+
onClick={() => setMode('confirm-remove')}
|
|
348
|
+
class="px-4 py-2.5 bg-cms-error text-white rounded-cms-pill cursor-pointer hover:bg-red-600 transition-colors font-medium"
|
|
349
|
+
>
|
|
350
|
+
Remove
|
|
351
|
+
</button>
|
|
352
|
+
<div class="flex gap-2">
|
|
353
|
+
<button
|
|
354
|
+
onClick={onClose}
|
|
355
|
+
class="px-4 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
356
|
+
>
|
|
357
|
+
Cancel
|
|
358
|
+
</button>
|
|
359
|
+
<button
|
|
360
|
+
onClick={handleSave}
|
|
361
|
+
class="px-4 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill cursor-pointer hover:bg-cms-primary-hover transition-all font-medium"
|
|
362
|
+
>
|
|
363
|
+
Save
|
|
364
|
+
</button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
)
|
|
369
|
+
: mode === 'confirm-remove'
|
|
370
|
+
? (
|
|
371
|
+
<div class="text-center py-4">
|
|
372
|
+
<div class="px-4 py-3 bg-red-500/10 border border-red-500/30 rounded-cms-md mb-5 text-[13px] text-white">
|
|
373
|
+
The <strong>{currentDefinition?.name}</strong> component highlighted in the page will be removed. This cannot be undone.
|
|
374
|
+
</div>
|
|
375
|
+
<div class="flex gap-2 justify-end pt-4 border-t border-white/10 mt-4">
|
|
376
|
+
<button
|
|
377
|
+
onClick={handleBackToEdit}
|
|
378
|
+
class="px-4 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
379
|
+
>
|
|
380
|
+
Cancel
|
|
381
|
+
</button>
|
|
382
|
+
<button
|
|
383
|
+
onClick={() => {
|
|
384
|
+
if (componentId) {
|
|
385
|
+
onRemoveBlock(componentId)
|
|
386
|
+
onClose()
|
|
387
|
+
}
|
|
388
|
+
}}
|
|
389
|
+
class="px-4 py-2.5 bg-cms-error text-white rounded-cms-pill cursor-pointer hover:bg-red-600 transition-colors font-medium"
|
|
390
|
+
>
|
|
391
|
+
Confirm remove
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
)
|
|
396
|
+
: mode === 'insert-props' && selectedComponent
|
|
397
|
+
? (
|
|
398
|
+
<>
|
|
399
|
+
{/* New component props */}
|
|
400
|
+
<div class="mb-5">
|
|
401
|
+
<div class="px-4 py-3 bg-white/10 rounded-cms-md mb-4 text-[13px] text-white">
|
|
402
|
+
Inserting <strong>{selectedComponent}</strong> {insertPosition} current component
|
|
403
|
+
</div>
|
|
404
|
+
{componentDefinitions[selectedComponent]?.props.map((prop) => (
|
|
405
|
+
<PropEditor
|
|
406
|
+
key={prop.name}
|
|
407
|
+
prop={prop}
|
|
408
|
+
value={propValues[prop.name] || ''}
|
|
409
|
+
onChange={(value) => handlePropChange(prop.name, value)}
|
|
410
|
+
/>
|
|
411
|
+
))}
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div class="flex gap-2 justify-end pt-4 border-t border-white/10 mt-4">
|
|
415
|
+
<button
|
|
416
|
+
onClick={() => setMode('insert-picker')}
|
|
417
|
+
class="px-4 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
418
|
+
>
|
|
419
|
+
Back
|
|
420
|
+
</button>
|
|
421
|
+
<button
|
|
422
|
+
onClick={handleConfirmInsert}
|
|
423
|
+
class="px-4 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill cursor-pointer hover:bg-cms-primary-hover transition-all font-medium"
|
|
424
|
+
>
|
|
425
|
+
Insert component
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
</>
|
|
429
|
+
)
|
|
430
|
+
: mode === 'insert-picker'
|
|
431
|
+
? (
|
|
432
|
+
/* Component picker for insertion */
|
|
433
|
+
<div>
|
|
434
|
+
<div class="text-xs font-medium text-white/50 tracking-wide mb-4 uppercase">
|
|
435
|
+
Select component to insert
|
|
436
|
+
</div>
|
|
437
|
+
<div class="flex flex-col gap-2">
|
|
438
|
+
{Object.values(componentDefinitions).map((def) => (
|
|
439
|
+
<button
|
|
440
|
+
key={def.name}
|
|
441
|
+
onClick={() => handleSelectComponentForInsert(def.name)}
|
|
442
|
+
class="p-4 bg-white/5 border border-white/10 rounded-cms-md cursor-pointer text-left transition-all hover:border-cms-primary/50 hover:bg-white/10 group"
|
|
443
|
+
>
|
|
444
|
+
{def.previewUrl && (
|
|
445
|
+
<div class="mb-3 rounded overflow-hidden bg-white h-30 relative">
|
|
446
|
+
{(() => {
|
|
447
|
+
const pw = def.previewWidth ?? 1280
|
|
448
|
+
const scale = 320 / pw
|
|
449
|
+
return (
|
|
450
|
+
<iframe
|
|
451
|
+
src={def.previewUrl}
|
|
452
|
+
class="border-none pointer-events-none"
|
|
453
|
+
style={{ width: `${pw}px`, height: `${Math.round(120 / scale)}px`, transform: `scale(${scale})`, transformOrigin: 'top left' }}
|
|
454
|
+
sandbox="allow-same-origin"
|
|
455
|
+
loading="lazy"
|
|
456
|
+
tabIndex={-1}
|
|
457
|
+
/>
|
|
458
|
+
)
|
|
459
|
+
})()}
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
<div class="font-medium text-white">{def.name}</div>
|
|
463
|
+
{def.description && (
|
|
464
|
+
<div class="text-xs text-white/50 mt-1">
|
|
465
|
+
{def.description}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
<div class="text-[11px] text-white/40 mt-2 font-mono">
|
|
469
|
+
{def.props.length} props
|
|
470
|
+
{def.slots && def.slots.length > 0 && ` • ${def.slots.length} slots`}
|
|
471
|
+
</div>
|
|
472
|
+
</button>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
<div class="mt-5 pt-4 border-t border-white/10">
|
|
476
|
+
<button
|
|
477
|
+
onClick={handleBackToEdit}
|
|
478
|
+
class="w-full px-4 py-2.5 bg-white/10 text-white/80 rounded-cms-pill cursor-pointer hover:bg-white/20 hover:text-white transition-colors font-medium"
|
|
479
|
+
>
|
|
480
|
+
Back to edit
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
)
|
|
485
|
+
: (
|
|
486
|
+
/* No component selected - show placeholder */
|
|
487
|
+
<div class="text-center text-white/50 py-8">
|
|
488
|
+
<p>Select a component to edit its properties.</p>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</>
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
interface PropEditorProps {
|
|
498
|
+
prop: ComponentProp
|
|
499
|
+
value: string
|
|
500
|
+
onChange: (value: string) => void
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function PropEditor({ prop, value, onChange }: PropEditorProps) {
|
|
504
|
+
const isBoolean = prop.type === 'boolean'
|
|
505
|
+
const isNumber = prop.type === 'number'
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<div class="mb-4">
|
|
509
|
+
<label class="block text-[13px] font-medium text-white mb-1.5">
|
|
510
|
+
{prop.name}
|
|
511
|
+
{prop.required && <span class="text-cms-error ml-1">*</span>}
|
|
512
|
+
</label>
|
|
513
|
+
{prop.description && (
|
|
514
|
+
<div class="text-[11px] text-white/50 mb-1.5">
|
|
515
|
+
{prop.description}
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
{isBoolean
|
|
519
|
+
? (
|
|
520
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
521
|
+
<input
|
|
522
|
+
type="checkbox"
|
|
523
|
+
checked={value === 'true'}
|
|
524
|
+
onChange={(e) => onChange((e.target as HTMLInputElement).checked ? 'true' : 'false')}
|
|
525
|
+
class="accent-cms-primary w-5 h-5 rounded"
|
|
526
|
+
/>
|
|
527
|
+
<span class="text-[13px] text-white">
|
|
528
|
+
{value === 'true' ? 'Enabled' : 'Disabled'}
|
|
529
|
+
</span>
|
|
530
|
+
</label>
|
|
531
|
+
)
|
|
532
|
+
: (
|
|
533
|
+
<input
|
|
534
|
+
type={isNumber ? 'number' : 'text'}
|
|
535
|
+
value={value}
|
|
536
|
+
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
537
|
+
placeholder={prop.defaultValue || `Enter ${prop.name}...`}
|
|
538
|
+
class="w-full px-4 py-2.5 bg-white/10 border border-white/20 text-[13px] text-white placeholder:text-white/40 outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10 transition-all rounded-cms-md"
|
|
539
|
+
/>
|
|
540
|
+
)}
|
|
541
|
+
<div class="text-[10px] text-white/40 mt-1.5 font-mono">
|
|
542
|
+
{prop.type}
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
)
|
|
546
|
+
}
|