@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,163 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'preact/hooks'
|
|
2
|
+
import { isCreatePageOpen, manifest, openMarkdownEditorForNewPage, resetCreatePageState } from '../signals'
|
|
3
|
+
|
|
4
|
+
export function CreatePageModal() {
|
|
5
|
+
const visible = isCreatePageOpen.value
|
|
6
|
+
|
|
7
|
+
// Get collection definitions from manifest (read signal directly for reactivity)
|
|
8
|
+
const collectionDefinitions = manifest.value.collectionDefinitions ?? {}
|
|
9
|
+
|
|
10
|
+
const collections = useMemo(() => {
|
|
11
|
+
return Object.values(collectionDefinitions)
|
|
12
|
+
}, [collectionDefinitions])
|
|
13
|
+
|
|
14
|
+
// Single collection — skip picker and go straight to editor
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (visible && collections.length === 1) {
|
|
17
|
+
const col = collections[0]
|
|
18
|
+
resetCreatePageState()
|
|
19
|
+
if (col) {
|
|
20
|
+
openMarkdownEditorForNewPage(col?.name, col)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, [visible, collections])
|
|
24
|
+
|
|
25
|
+
const handleClose = () => {
|
|
26
|
+
resetCreatePageState()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleSelectCollection = (name: string) => {
|
|
30
|
+
const def = collectionDefinitions[name]
|
|
31
|
+
if (def) {
|
|
32
|
+
resetCreatePageState()
|
|
33
|
+
openMarkdownEditorForNewPage(name, def)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!visible) return null
|
|
38
|
+
|
|
39
|
+
// No collections available
|
|
40
|
+
if (collections.length === 0) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
44
|
+
onClick={handleClose}
|
|
45
|
+
data-cms-ui
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-md w-full border border-white/10"
|
|
49
|
+
onClick={(e) => e.stopPropagation()}
|
|
50
|
+
data-cms-ui
|
|
51
|
+
>
|
|
52
|
+
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
53
|
+
<h2 class="text-lg font-semibold text-white">Create New Page</h2>
|
|
54
|
+
<CloseButton onClick={handleClose} />
|
|
55
|
+
</div>
|
|
56
|
+
<div class="p-8 text-center">
|
|
57
|
+
<div class="text-white/60 mb-4">
|
|
58
|
+
No content collections found.
|
|
59
|
+
</div>
|
|
60
|
+
<p class="text-white/40 text-sm">
|
|
61
|
+
Add markdown files to <code class="bg-white/10 px-1.5 py-0.5 rounded">src/content/</code> subdirectories to enable page creation.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="flex items-center justify-end p-5 border-t border-white/10 bg-white/5 rounded-b-cms-xl">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={handleClose}
|
|
68
|
+
class="px-4 py-2.5 text-sm text-white/80 font-medium rounded-cms-pill hover:bg-white/10 hover:text-white transition-colors"
|
|
69
|
+
data-cms-ui
|
|
70
|
+
>
|
|
71
|
+
Close
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Single collection auto-selected via useEffect above
|
|
80
|
+
if (collections.length === 1) return null
|
|
81
|
+
|
|
82
|
+
// Collection picker
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
86
|
+
onClick={handleClose}
|
|
87
|
+
data-cms-ui
|
|
88
|
+
>
|
|
89
|
+
<div
|
|
90
|
+
class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-md w-full border border-white/10"
|
|
91
|
+
onClick={(e) => e.stopPropagation()}
|
|
92
|
+
data-cms-ui
|
|
93
|
+
>
|
|
94
|
+
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
95
|
+
<h2 class="text-lg font-semibold text-white">Choose Collection</h2>
|
|
96
|
+
<CloseButton onClick={handleClose} />
|
|
97
|
+
</div>
|
|
98
|
+
<div class="p-5 space-y-2">
|
|
99
|
+
{collections.map((col) => (
|
|
100
|
+
<button
|
|
101
|
+
key={col.name}
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => handleSelectCollection(col.name)}
|
|
104
|
+
class="w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left"
|
|
105
|
+
data-cms-ui
|
|
106
|
+
>
|
|
107
|
+
<div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-md flex items-center justify-center">
|
|
108
|
+
<CollectionIcon />
|
|
109
|
+
</div>
|
|
110
|
+
<div class="flex-1 min-w-0">
|
|
111
|
+
<div class="text-white font-medium">{col.label}</div>
|
|
112
|
+
<div class="text-white/50 text-sm">
|
|
113
|
+
{col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'} · {col.fields.length} fields
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<ChevronRightIcon />
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Icons
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
function CloseButton({ onClick }: { onClick: () => void }) {
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
|
|
135
|
+
data-cms-ui
|
|
136
|
+
>
|
|
137
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
138
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
139
|
+
</svg>
|
|
140
|
+
</button>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function CollectionIcon() {
|
|
145
|
+
return (
|
|
146
|
+
<svg class="w-5 h-5 text-cms-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
147
|
+
<path
|
|
148
|
+
stroke-linecap="round"
|
|
149
|
+
stroke-linejoin="round"
|
|
150
|
+
stroke-width="2"
|
|
151
|
+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
152
|
+
/>
|
|
153
|
+
</svg>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ChevronRightIcon() {
|
|
158
|
+
return (
|
|
159
|
+
<svg class="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
160
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
161
|
+
</svg>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks'
|
|
2
|
+
import { Z_INDEX } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
|
|
5
|
+
export interface EditableHighlightsProps {
|
|
6
|
+
visible: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface HighlightRect {
|
|
10
|
+
cmsId: string
|
|
11
|
+
type: 'text' | 'component' | 'image'
|
|
12
|
+
rect: DOMRect
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders lightweight dashed outlines around all editable elements.
|
|
17
|
+
* Uses a single canvas-like approach with Shadow DOM for performance.
|
|
18
|
+
*/
|
|
19
|
+
export function EditableHighlights({ visible }: EditableHighlightsProps) {
|
|
20
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
21
|
+
const shadowRootRef = useRef<ShadowRoot | null>(null)
|
|
22
|
+
const overlaysContainerRef = useRef<HTMLDivElement | null>(null)
|
|
23
|
+
const rafRef = useRef<number | null>(null)
|
|
24
|
+
|
|
25
|
+
// Initialize Shadow DOM once
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (containerRef.current && !shadowRootRef.current) {
|
|
28
|
+
shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' })
|
|
29
|
+
|
|
30
|
+
const style = document.createElement('style')
|
|
31
|
+
style.textContent = `
|
|
32
|
+
:host {
|
|
33
|
+
position: fixed;
|
|
34
|
+
top: 0;
|
|
35
|
+
left: 0;
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
z-index: ${Z_INDEX.HIGHLIGHT};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.highlights-container {
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 0;
|
|
45
|
+
left: 0;
|
|
46
|
+
width: 100%;
|
|
47
|
+
height: 100%;
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.highlight-overlay {
|
|
52
|
+
position: fixed;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
border: 2px dashed #1A1A1A;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
opacity: 0.5;
|
|
58
|
+
transition: opacity 100ms ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.highlight-overlay.component {
|
|
62
|
+
border-style: solid;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.highlight-overlay.image {
|
|
66
|
+
border-style: dotted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.highlights-container.hidden {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
overlaysContainerRef.current = document.createElement('div')
|
|
75
|
+
overlaysContainerRef.current.className = 'highlights-container hidden'
|
|
76
|
+
|
|
77
|
+
shadowRootRef.current.appendChild(style)
|
|
78
|
+
shadowRootRef.current.appendChild(overlaysContainerRef.current)
|
|
79
|
+
}
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
// Update highlights when visible changes or on scroll/resize
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!overlaysContainerRef.current) return
|
|
85
|
+
|
|
86
|
+
if (!visible) {
|
|
87
|
+
overlaysContainerRef.current.className = 'highlights-container hidden'
|
|
88
|
+
if (rafRef.current) {
|
|
89
|
+
cancelAnimationFrame(rafRef.current)
|
|
90
|
+
rafRef.current = null
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
overlaysContainerRef.current.className = 'highlights-container'
|
|
96
|
+
|
|
97
|
+
const updateHighlights = () => {
|
|
98
|
+
if (!overlaysContainerRef.current || !visible) return
|
|
99
|
+
|
|
100
|
+
const highlights = collectEditableElements()
|
|
101
|
+
renderHighlights(overlaysContainerRef.current, highlights)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Initial render
|
|
105
|
+
updateHighlights()
|
|
106
|
+
|
|
107
|
+
// Use RAF loop for smooth updates during scroll
|
|
108
|
+
let lastScrollY = window.scrollY
|
|
109
|
+
let lastScrollX = window.scrollX
|
|
110
|
+
|
|
111
|
+
const rafLoop = () => {
|
|
112
|
+
if (!visible) return
|
|
113
|
+
|
|
114
|
+
// Only update if scroll position changed
|
|
115
|
+
if (window.scrollY !== lastScrollY || window.scrollX !== lastScrollX) {
|
|
116
|
+
lastScrollY = window.scrollY
|
|
117
|
+
lastScrollX = window.scrollX
|
|
118
|
+
updateHighlights()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
rafRef.current = requestAnimationFrame(rafLoop)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
rafRef.current = requestAnimationFrame(rafLoop)
|
|
125
|
+
|
|
126
|
+
// Listen for resize
|
|
127
|
+
const handleResize = () => {
|
|
128
|
+
updateHighlights()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
window.addEventListener('resize', handleResize)
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
window.removeEventListener('resize', handleResize)
|
|
135
|
+
if (rafRef.current) {
|
|
136
|
+
cancelAnimationFrame(rafRef.current)
|
|
137
|
+
rafRef.current = null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, [visible])
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
ref={containerRef}
|
|
145
|
+
data-cms-ui
|
|
146
|
+
style={{
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
top: 0,
|
|
149
|
+
left: 0,
|
|
150
|
+
width: 0,
|
|
151
|
+
height: 0,
|
|
152
|
+
pointerEvents: 'none',
|
|
153
|
+
zIndex: Z_INDEX.HIGHLIGHT,
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Collect all editable elements from the DOM
|
|
161
|
+
*/
|
|
162
|
+
function collectEditableElements(): HighlightRect[] {
|
|
163
|
+
const highlights: HighlightRect[] = []
|
|
164
|
+
const manifest = signals.manifest.value
|
|
165
|
+
|
|
166
|
+
// Query all elements with CMS data attributes
|
|
167
|
+
const textElements = document.querySelectorAll('[data-cms-id]')
|
|
168
|
+
const componentElements = document.querySelectorAll('[data-cms-component-id]')
|
|
169
|
+
const imageElements = document.querySelectorAll('img[data-cms-img]')
|
|
170
|
+
|
|
171
|
+
// Process text elements
|
|
172
|
+
textElements.forEach((el) => {
|
|
173
|
+
const cmsId = el.getAttribute('data-cms-id')
|
|
174
|
+
if (!cmsId) return
|
|
175
|
+
|
|
176
|
+
// Skip if this is also a component or image
|
|
177
|
+
if (el.hasAttribute('data-cms-component-id') || el.tagName === 'IMG') return
|
|
178
|
+
|
|
179
|
+
// Skip if not in manifest (invalid element)
|
|
180
|
+
if (!manifest.entries[cmsId]) return
|
|
181
|
+
|
|
182
|
+
const rect = el.getBoundingClientRect()
|
|
183
|
+
// Skip elements not in viewport or too small
|
|
184
|
+
if (rect.width < 10 || rect.height < 10) return
|
|
185
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
186
|
+
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
187
|
+
|
|
188
|
+
highlights.push({ cmsId, type: 'text', rect })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Process component elements
|
|
192
|
+
componentElements.forEach((el) => {
|
|
193
|
+
const componentId = el.getAttribute('data-cms-component-id')
|
|
194
|
+
if (!componentId) return
|
|
195
|
+
|
|
196
|
+
// Skip if not in manifest
|
|
197
|
+
if (!manifest.components[componentId]) return
|
|
198
|
+
|
|
199
|
+
const rect = el.getBoundingClientRect()
|
|
200
|
+
if (rect.width < 10 || rect.height < 10) return
|
|
201
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
202
|
+
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
203
|
+
|
|
204
|
+
highlights.push({ cmsId: componentId, type: 'component', rect })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Process image elements
|
|
208
|
+
imageElements.forEach((el) => {
|
|
209
|
+
const cmsId = el.getAttribute('data-cms-img')
|
|
210
|
+
if (!cmsId) return
|
|
211
|
+
|
|
212
|
+
const rect = el.getBoundingClientRect()
|
|
213
|
+
if (rect.width < 10 || rect.height < 10) return
|
|
214
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) return
|
|
215
|
+
if (rect.right < 0 || rect.left > window.innerWidth) return
|
|
216
|
+
|
|
217
|
+
highlights.push({ cmsId, type: 'image', rect })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return highlights
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Render highlight overlays efficiently by reusing DOM elements
|
|
225
|
+
*/
|
|
226
|
+
function renderHighlights(container: HTMLDivElement, highlights: HighlightRect[]): void {
|
|
227
|
+
// Get existing overlay elements
|
|
228
|
+
const existingOverlays = container.querySelectorAll('.highlight-overlay')
|
|
229
|
+
const existingCount = existingOverlays.length
|
|
230
|
+
const neededCount = highlights.length
|
|
231
|
+
|
|
232
|
+
// Update or create overlays
|
|
233
|
+
highlights.forEach((highlight, index) => {
|
|
234
|
+
let overlay: HTMLDivElement
|
|
235
|
+
|
|
236
|
+
if (index < existingCount) {
|
|
237
|
+
// Reuse existing overlay
|
|
238
|
+
overlay = existingOverlays[index] as HTMLDivElement
|
|
239
|
+
} else {
|
|
240
|
+
// Create new overlay
|
|
241
|
+
overlay = document.createElement('div')
|
|
242
|
+
overlay.className = 'highlight-overlay'
|
|
243
|
+
container.appendChild(overlay)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Update class based on type
|
|
247
|
+
overlay.className = `highlight-overlay ${highlight.type}`
|
|
248
|
+
|
|
249
|
+
// Update position
|
|
250
|
+
overlay.style.left = `${highlight.rect.left - 6}px`
|
|
251
|
+
overlay.style.top = `${highlight.rect.top - 6}px`
|
|
252
|
+
overlay.style.width = `${highlight.rect.width + 12}px`
|
|
253
|
+
overlay.style.height = `${highlight.rect.height + 12}px`
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Remove extra overlays
|
|
257
|
+
for (let i = neededCount; i < existingCount; i++) {
|
|
258
|
+
existingOverlays[i]!.remove()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Component, type ComponentChildren } from 'preact'
|
|
2
|
+
|
|
3
|
+
interface ErrorBoundaryProps {
|
|
4
|
+
children: ComponentChildren
|
|
5
|
+
fallback?: ComponentChildren
|
|
6
|
+
onError?: (error: Error, errorInfo: { componentStack: string }) => void
|
|
7
|
+
componentName?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ErrorBoundaryState {
|
|
11
|
+
hasError: boolean
|
|
12
|
+
error: Error | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error boundary component to catch and handle errors in CMS UI components.
|
|
17
|
+
* Prevents the entire CMS overlay from crashing when a component fails.
|
|
18
|
+
*/
|
|
19
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
20
|
+
constructor(props: ErrorBoundaryProps) {
|
|
21
|
+
super(props)
|
|
22
|
+
this.state = { hasError: false, error: null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static override getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
26
|
+
return { hasError: true, error }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override componentDidCatch(error: Error, errorInfo: { componentStack: string }): void {
|
|
30
|
+
console.error('[CMS] Component error:', error)
|
|
31
|
+
console.error('[CMS] Component stack:', errorInfo.componentStack)
|
|
32
|
+
this.props.onError?.(error, errorInfo)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private handleRetry = (): void => {
|
|
36
|
+
this.setState({ hasError: false, error: null })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
if (this.state.hasError) {
|
|
41
|
+
if (this.props.fallback) {
|
|
42
|
+
return this.props.fallback
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const componentName = this.props.componentName || 'Component'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-cms-ui
|
|
50
|
+
class="p-4 bg-red-50 border-2 border-red-500 text-red-800 font-sans text-sm"
|
|
51
|
+
style={{
|
|
52
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<div class="font-bold mb-2 flex items-center gap-2">
|
|
56
|
+
<span class="text-red-600">⚠</span>
|
|
57
|
+
{componentName} Error
|
|
58
|
+
</div>
|
|
59
|
+
<div class="text-xs text-red-600 mb-3 font-mono">
|
|
60
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
61
|
+
</div>
|
|
62
|
+
<button
|
|
63
|
+
onClick={this.handleRetry}
|
|
64
|
+
class="px-3 py-1.5 bg-red-600 text-white border-2 border-red-800 text-xs font-bold cursor-pointer hover:bg-red-700 transition-colors"
|
|
65
|
+
>
|
|
66
|
+
Retry
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this.props.children
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const SilentErrorFallback = () => null
|
|
77
|
+
|
|
78
|
+
export const CompactErrorFallback = ({ message }: { message?: string }) => {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
data-cms-ui
|
|
82
|
+
class="px-2 py-1 bg-red-100 text-red-700 text-xs font-sans border border-red-300 inline-block"
|
|
83
|
+
>
|
|
84
|
+
⚠ {message || 'Error'}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|