@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,469 @@
|
|
|
1
|
+
import { markdownEditorState, openMediaLibraryWithCallback, updateMarkdownFrontmatter } from '../signals'
|
|
2
|
+
import type { CollectionDefinition, FieldDefinition, MarkdownPageEntry } from '../types'
|
|
3
|
+
import { ComboBoxField, ImageField, NumberField, TextField, ToggleField } from './fields'
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Generic Frontmatter Field (auto-detect by value type)
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
interface FrontmatterFieldProps {
|
|
10
|
+
fieldKey: string
|
|
11
|
+
value: unknown
|
|
12
|
+
onChange: (value: unknown) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function FrontmatterField({
|
|
16
|
+
fieldKey,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
}: FrontmatterFieldProps) {
|
|
20
|
+
// Format field key as label (e.g., "featuredImage" -> "Featured Image")
|
|
21
|
+
const label = fieldKey
|
|
22
|
+
.replace(/([A-Z])/g, ' $1')
|
|
23
|
+
.replace(/^./, (str) => str.toUpperCase())
|
|
24
|
+
.trim()
|
|
25
|
+
|
|
26
|
+
// Detect field type based on value
|
|
27
|
+
const isBoolean = typeof value === 'boolean'
|
|
28
|
+
const isDate = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)
|
|
29
|
+
const isArray = Array.isArray(value)
|
|
30
|
+
|
|
31
|
+
// Boolean field - checkbox
|
|
32
|
+
if (isBoolean) {
|
|
33
|
+
return (
|
|
34
|
+
<label
|
|
35
|
+
class="flex items-center gap-2 text-sm text-white/80 cursor-pointer"
|
|
36
|
+
data-cms-ui
|
|
37
|
+
>
|
|
38
|
+
<input
|
|
39
|
+
type="checkbox"
|
|
40
|
+
checked={value}
|
|
41
|
+
onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
|
|
42
|
+
class="w-4 h-4 rounded border-white/20 bg-white/10 text-cms-primary focus:ring-cms-primary focus:ring-offset-0 cursor-pointer"
|
|
43
|
+
data-cms-ui
|
|
44
|
+
/>
|
|
45
|
+
{label}
|
|
46
|
+
</label>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Date field
|
|
51
|
+
if (isDate) {
|
|
52
|
+
return (
|
|
53
|
+
<div class="flex flex-col gap-1" data-cms-ui>
|
|
54
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
55
|
+
<input
|
|
56
|
+
type="date"
|
|
57
|
+
value={typeof value === 'string' ? value.split('T')[0] : ''}
|
|
58
|
+
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
59
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-cms-primary"
|
|
60
|
+
data-cms-ui
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Array field (e.g., categories) - comma-separated input
|
|
67
|
+
if (isArray) {
|
|
68
|
+
return (
|
|
69
|
+
<div class="flex flex-col gap-1 col-span-2" data-cms-ui>
|
|
70
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
value={(value as unknown[]).join(', ')}
|
|
74
|
+
onChange={(e) => {
|
|
75
|
+
const inputValue = (e.target as HTMLInputElement).value
|
|
76
|
+
const arrayValue = inputValue
|
|
77
|
+
.split(',')
|
|
78
|
+
.map((s) => s.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
onChange(arrayValue)
|
|
81
|
+
}}
|
|
82
|
+
placeholder={`Enter ${label.toLowerCase()} separated by commas`}
|
|
83
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary"
|
|
84
|
+
data-cms-ui
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// String field (default) - check if it's a long text (excerpt, etc.)
|
|
91
|
+
const isLongText = fieldKey.toLowerCase().includes('excerpt')
|
|
92
|
+
|| fieldKey.toLowerCase().includes('description')
|
|
93
|
+
|| (typeof value === 'string' && value.length > 100)
|
|
94
|
+
|
|
95
|
+
if (isLongText) {
|
|
96
|
+
return (
|
|
97
|
+
<div class="flex flex-col gap-1 col-span-2" data-cms-ui>
|
|
98
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
99
|
+
<textarea
|
|
100
|
+
value={typeof value === 'string' ? value : ''}
|
|
101
|
+
onChange={(e) => onChange((e.target as HTMLTextAreaElement).value)}
|
|
102
|
+
rows={3}
|
|
103
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary resize-none"
|
|
104
|
+
data-cms-ui
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Default text input
|
|
111
|
+
return (
|
|
112
|
+
<div class="flex flex-col gap-1" data-cms-ui>
|
|
113
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
value={typeof value === 'string' ? value : String(value ?? '')}
|
|
117
|
+
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
118
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary"
|
|
119
|
+
data-cms-ui
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Create Mode Frontmatter — schema-aware field rendering
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
interface CreateModeFrontmatterProps {
|
|
130
|
+
page: MarkdownPageEntry
|
|
131
|
+
collectionDefinition: CollectionDefinition
|
|
132
|
+
onSlugManualEdit: () => void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function CreateModeFrontmatter({
|
|
136
|
+
page,
|
|
137
|
+
collectionDefinition,
|
|
138
|
+
onSlugManualEdit,
|
|
139
|
+
}: CreateModeFrontmatterProps) {
|
|
140
|
+
return (
|
|
141
|
+
<div class="space-y-4">
|
|
142
|
+
{/* Slug field */}
|
|
143
|
+
<div>
|
|
144
|
+
<label class="block text-xs font-medium text-white/70 mb-1.5">
|
|
145
|
+
URL Slug
|
|
146
|
+
</label>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
value={page.slug}
|
|
150
|
+
onInput={(e) => {
|
|
151
|
+
onSlugManualEdit()
|
|
152
|
+
const slug = (e.target as HTMLInputElement).value
|
|
153
|
+
markdownEditorState.value = {
|
|
154
|
+
...markdownEditorState.value,
|
|
155
|
+
currentPage: markdownEditorState.value.currentPage
|
|
156
|
+
? { ...markdownEditorState.value.currentPage, slug }
|
|
157
|
+
: null,
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
160
|
+
placeholder="url-friendly-slug"
|
|
161
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-cms-md text-sm text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
|
|
162
|
+
data-cms-ui
|
|
163
|
+
/>
|
|
164
|
+
<p class="mt-1 text-xs text-white/40">
|
|
165
|
+
Will be saved to: src/content/{collectionDefinition.name}/
|
|
166
|
+
{page.slug || 'your-slug'}.{collectionDefinition.fileExtension}
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Schema fields */}
|
|
171
|
+
<div class="grid grid-cols-2 gap-4">
|
|
172
|
+
{collectionDefinition.fields.map((field) => (
|
|
173
|
+
<SchemaFrontmatterField
|
|
174
|
+
key={field.name}
|
|
175
|
+
field={field}
|
|
176
|
+
value={page.frontmatter[field.name]}
|
|
177
|
+
onChange={(newValue) => updateMarkdownFrontmatter({ [field.name]: newValue })}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Edit Mode Frontmatter — uses schema fields when available, falls back to generic
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
interface EditModeFrontmatterProps {
|
|
190
|
+
page: MarkdownPageEntry
|
|
191
|
+
collectionDefinition?: CollectionDefinition
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function EditModeFrontmatter({
|
|
195
|
+
page,
|
|
196
|
+
collectionDefinition,
|
|
197
|
+
}: EditModeFrontmatterProps) {
|
|
198
|
+
// Collect schema field names for filtering extra keys
|
|
199
|
+
const schemaFieldNames = new Set(
|
|
200
|
+
collectionDefinition?.fields.map((f) => f.name) ?? [],
|
|
201
|
+
)
|
|
202
|
+
// Frontmatter keys not covered by the schema (user-added fields)
|
|
203
|
+
const extraKeys = Object.keys(page.frontmatter).filter(
|
|
204
|
+
(key) => !schemaFieldNames.has(key),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div class="space-y-4">
|
|
209
|
+
{/* Slug field (always disabled in edit mode) */}
|
|
210
|
+
<div>
|
|
211
|
+
<label class="block text-xs font-medium text-white/70 mb-1.5">
|
|
212
|
+
URL Slug
|
|
213
|
+
</label>
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
value={page.slug}
|
|
217
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-cms-md text-sm text-white/50 focus:outline-none cursor-not-allowed"
|
|
218
|
+
disabled
|
|
219
|
+
data-cms-ui
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="grid grid-cols-2 gap-4">
|
|
223
|
+
{collectionDefinition
|
|
224
|
+
? (
|
|
225
|
+
<>
|
|
226
|
+
{/* Schema-aware fields */}
|
|
227
|
+
{collectionDefinition.fields.map((field) => (
|
|
228
|
+
<SchemaFrontmatterField
|
|
229
|
+
key={field.name}
|
|
230
|
+
field={field}
|
|
231
|
+
value={page.frontmatter[field.name]}
|
|
232
|
+
onChange={(newValue) => updateMarkdownFrontmatter({ [field.name]: newValue })}
|
|
233
|
+
/>
|
|
234
|
+
))}
|
|
235
|
+
{/* Extra fields not in schema */}
|
|
236
|
+
{extraKeys.map((key) => (
|
|
237
|
+
<FrontmatterField
|
|
238
|
+
key={key}
|
|
239
|
+
fieldKey={key}
|
|
240
|
+
value={page.frontmatter[key]}
|
|
241
|
+
onChange={(newValue) => updateMarkdownFrontmatter({ [key]: newValue })}
|
|
242
|
+
/>
|
|
243
|
+
))}
|
|
244
|
+
</>
|
|
245
|
+
)
|
|
246
|
+
: (
|
|
247
|
+
/* Generic fallback when no schema is available */
|
|
248
|
+
Object.entries(page.frontmatter).map(([key, value]) => (
|
|
249
|
+
<FrontmatterField
|
|
250
|
+
key={key}
|
|
251
|
+
fieldKey={key}
|
|
252
|
+
value={value}
|
|
253
|
+
onChange={(newValue) => updateMarkdownFrontmatter({ [key]: newValue })}
|
|
254
|
+
/>
|
|
255
|
+
))
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Schema-aware Frontmatter Field
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
interface SchemaFrontmatterFieldProps {
|
|
267
|
+
field: FieldDefinition
|
|
268
|
+
value: unknown
|
|
269
|
+
onChange: (value: unknown) => void
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function SchemaFrontmatterField({
|
|
273
|
+
field,
|
|
274
|
+
value,
|
|
275
|
+
onChange,
|
|
276
|
+
}: SchemaFrontmatterFieldProps) {
|
|
277
|
+
const label = formatFieldLabel(field.name)
|
|
278
|
+
|
|
279
|
+
switch (field.type) {
|
|
280
|
+
case 'text':
|
|
281
|
+
case 'url':
|
|
282
|
+
return (
|
|
283
|
+
<TextField
|
|
284
|
+
label={label}
|
|
285
|
+
value={(value as string) ?? ''}
|
|
286
|
+
placeholder={getPlaceholder(field)}
|
|
287
|
+
onChange={(v) => onChange(v)}
|
|
288
|
+
/>
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
case 'image':
|
|
292
|
+
return (
|
|
293
|
+
<ImageField
|
|
294
|
+
label={label}
|
|
295
|
+
value={(value as string) ?? ''}
|
|
296
|
+
placeholder={getPlaceholder(field)}
|
|
297
|
+
onChange={(v) => onChange(v)}
|
|
298
|
+
onBrowse={() => {
|
|
299
|
+
openMediaLibraryWithCallback((url: string) => {
|
|
300
|
+
onChange(url)
|
|
301
|
+
})
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
case 'textarea':
|
|
307
|
+
return (
|
|
308
|
+
<div class="flex flex-col gap-1 col-span-2" data-cms-ui>
|
|
309
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
310
|
+
<textarea
|
|
311
|
+
value={(value as string) ?? ''}
|
|
312
|
+
onInput={(e) => onChange((e.target as HTMLTextAreaElement).value)}
|
|
313
|
+
placeholder={getPlaceholder(field)}
|
|
314
|
+
rows={3}
|
|
315
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40 resize-none"
|
|
316
|
+
data-cms-ui
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
case 'date':
|
|
322
|
+
return (
|
|
323
|
+
<div class="flex flex-col gap-1" data-cms-ui>
|
|
324
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
325
|
+
<input
|
|
326
|
+
type="date"
|
|
327
|
+
value={(value as string) ?? ''}
|
|
328
|
+
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
329
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-white/40"
|
|
330
|
+
data-cms-ui
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
case 'number':
|
|
336
|
+
return (
|
|
337
|
+
<NumberField
|
|
338
|
+
label={label}
|
|
339
|
+
value={(value as number) ?? undefined}
|
|
340
|
+
onChange={(v) => onChange(v ?? 0)}
|
|
341
|
+
/>
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
case 'boolean':
|
|
345
|
+
return (
|
|
346
|
+
<ToggleField
|
|
347
|
+
label={label}
|
|
348
|
+
value={!!value}
|
|
349
|
+
onChange={(v) => onChange(v)}
|
|
350
|
+
/>
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
case 'select':
|
|
354
|
+
return (
|
|
355
|
+
<ComboBoxField
|
|
356
|
+
label={label}
|
|
357
|
+
value={(value as string) ?? ''}
|
|
358
|
+
placeholder={getPlaceholder(field)}
|
|
359
|
+
options={(field.options ?? []).map((opt) => ({
|
|
360
|
+
value: opt,
|
|
361
|
+
label: opt,
|
|
362
|
+
}))}
|
|
363
|
+
onChange={(v) => onChange(v)}
|
|
364
|
+
/>
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
case 'array': {
|
|
368
|
+
const items = Array.isArray(value) ? value : []
|
|
369
|
+
if (field.options && field.options.length > 0) {
|
|
370
|
+
return (
|
|
371
|
+
<div class="col-span-2 space-y-1.5" data-cms-ui>
|
|
372
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
373
|
+
<div class="space-y-2">
|
|
374
|
+
{field.options.map((opt) => (
|
|
375
|
+
<label key={opt} class="flex items-center gap-2 cursor-pointer">
|
|
376
|
+
<input
|
|
377
|
+
type="checkbox"
|
|
378
|
+
checked={items.includes(opt)}
|
|
379
|
+
onChange={(e) => {
|
|
380
|
+
if ((e.target as HTMLInputElement).checked) {
|
|
381
|
+
onChange([...items, opt])
|
|
382
|
+
} else {
|
|
383
|
+
onChange(items.filter((i: unknown) => i !== opt))
|
|
384
|
+
}
|
|
385
|
+
}}
|
|
386
|
+
class="rounded border-white/20 bg-white/10 text-cms-primary focus:ring-cms-primary"
|
|
387
|
+
data-cms-ui
|
|
388
|
+
/>
|
|
389
|
+
<span class="text-sm text-white/80">{opt}</span>
|
|
390
|
+
</label>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
return (
|
|
397
|
+
<div class="col-span-2 flex flex-col gap-1" data-cms-ui>
|
|
398
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
399
|
+
<input
|
|
400
|
+
type="text"
|
|
401
|
+
value={(items as unknown[]).join(', ')}
|
|
402
|
+
onInput={(e) => {
|
|
403
|
+
const inputValue = (e.target as HTMLInputElement).value
|
|
404
|
+
const arrayValue = inputValue
|
|
405
|
+
.split(',')
|
|
406
|
+
.map((s) => s.trim())
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
onChange(arrayValue)
|
|
409
|
+
}}
|
|
410
|
+
placeholder={`Enter ${label.toLowerCase()} separated by commas`}
|
|
411
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
|
|
412
|
+
data-cms-ui
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
default:
|
|
419
|
+
return (
|
|
420
|
+
<div class="flex flex-col gap-1" data-cms-ui>
|
|
421
|
+
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
422
|
+
<input
|
|
423
|
+
type="text"
|
|
424
|
+
value={String(value ?? '')}
|
|
425
|
+
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
426
|
+
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
|
|
427
|
+
data-cms-ui
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ============================================================================
|
|
435
|
+
// Helper Functions
|
|
436
|
+
// ============================================================================
|
|
437
|
+
|
|
438
|
+
export function formatFieldLabel(name: string): string {
|
|
439
|
+
return name
|
|
440
|
+
.replace(/([A-Z])/g, ' $1')
|
|
441
|
+
.replace(/[-_]/g, ' ')
|
|
442
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
443
|
+
.trim()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function getPlaceholder(field: FieldDefinition): string {
|
|
447
|
+
if (field.examples && field.examples.length > 0) {
|
|
448
|
+
return String(field.examples[0])
|
|
449
|
+
}
|
|
450
|
+
switch (field.type) {
|
|
451
|
+
case 'url':
|
|
452
|
+
return 'https://...'
|
|
453
|
+
case 'image':
|
|
454
|
+
return '/images/...'
|
|
455
|
+
case 'date':
|
|
456
|
+
return 'YYYY-MM-DD'
|
|
457
|
+
default:
|
|
458
|
+
return `Enter ${formatFieldLabel(field.name).toLowerCase()}...`
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function slugify(text: string): string {
|
|
463
|
+
return text
|
|
464
|
+
.toLowerCase()
|
|
465
|
+
.trim()
|
|
466
|
+
.replace(/[^\w\s-]/g, '')
|
|
467
|
+
.replace(/[\s_-]+/g, '-')
|
|
468
|
+
.replace(/^-+|-+$/g, '')
|
|
469
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM-based highlight overlay for CMS elements.
|
|
3
|
+
* This component renders highlights without modifying the target element's styles.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface HighlightState {
|
|
7
|
+
color: string
|
|
8
|
+
style: 'solid' | 'dashed'
|
|
9
|
+
visible: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Map to track highlights by element
|
|
13
|
+
const highlightMap = new WeakMap<HTMLElement, HTMLElement>()
|
|
14
|
+
|
|
15
|
+
// Container for all highlight overlays
|
|
16
|
+
let highlightContainer: HTMLElement | null = null
|
|
17
|
+
|
|
18
|
+
// Flag to track if custom element is registered
|
|
19
|
+
let customElementRegistered = false
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom element that renders a highlight overlay using Shadow DOM
|
|
23
|
+
*/
|
|
24
|
+
class CmsHighlightOverlay extends HTMLElement {
|
|
25
|
+
private shadow: ShadowRoot
|
|
26
|
+
private overlayElement: HTMLDivElement
|
|
27
|
+
private resizeObserver: ResizeObserver | null = null
|
|
28
|
+
private targetElement: HTMLElement | null = null
|
|
29
|
+
private animationFrameId: number | null = null
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super()
|
|
33
|
+
this.shadow = this.attachShadow({ mode: 'open' })
|
|
34
|
+
|
|
35
|
+
// Create styles
|
|
36
|
+
const style = document.createElement('style')
|
|
37
|
+
style.textContent = `
|
|
38
|
+
:host {
|
|
39
|
+
position: absolute;
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
z-index: 2147483645;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.overlay {
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
transition: border-color 150ms ease, border-style 150ms ease;
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
this.overlayElement = document.createElement('div')
|
|
55
|
+
this.overlayElement.className = 'overlay'
|
|
56
|
+
|
|
57
|
+
this.shadow.appendChild(style)
|
|
58
|
+
this.shadow.appendChild(this.overlayElement)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
connectedCallback() {
|
|
62
|
+
this.startPositionTracking()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disconnectedCallback() {
|
|
66
|
+
this.stopPositionTracking()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the target element to highlight
|
|
71
|
+
*/
|
|
72
|
+
setTarget(element: HTMLElement) {
|
|
73
|
+
this.targetElement = element
|
|
74
|
+
this.updatePosition()
|
|
75
|
+
|
|
76
|
+
// Set up resize observer
|
|
77
|
+
if (this.resizeObserver) {
|
|
78
|
+
this.resizeObserver.disconnect()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
82
|
+
this.updatePosition()
|
|
83
|
+
})
|
|
84
|
+
this.resizeObserver.observe(element)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the highlight style
|
|
89
|
+
*/
|
|
90
|
+
setHighlightStyle(color: string, style: 'solid' | 'dashed') {
|
|
91
|
+
this.overlayElement.style.borderWidth = '2px'
|
|
92
|
+
this.overlayElement.style.borderColor = color
|
|
93
|
+
this.overlayElement.style.borderStyle = style
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Update position to match target element
|
|
98
|
+
*/
|
|
99
|
+
private updatePosition() {
|
|
100
|
+
if (!this.targetElement) return
|
|
101
|
+
|
|
102
|
+
const rect = this.targetElement.getBoundingClientRect()
|
|
103
|
+
const scrollX = window.scrollX
|
|
104
|
+
const scrollY = window.scrollY
|
|
105
|
+
|
|
106
|
+
// Position with offset to give breathing room from content
|
|
107
|
+
this.style.left = `${rect.left + scrollX - 6}px`
|
|
108
|
+
this.style.top = `${rect.top + scrollY - 6}px`
|
|
109
|
+
this.style.width = `${rect.width + 12}px`
|
|
110
|
+
this.style.height = `${rect.height + 12}px`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Start continuous position tracking for scroll/resize
|
|
115
|
+
*/
|
|
116
|
+
private startPositionTracking() {
|
|
117
|
+
const track = () => {
|
|
118
|
+
this.updatePosition()
|
|
119
|
+
this.animationFrameId = requestAnimationFrame(track)
|
|
120
|
+
}
|
|
121
|
+
this.animationFrameId = requestAnimationFrame(track)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Stop position tracking
|
|
126
|
+
*/
|
|
127
|
+
private stopPositionTracking() {
|
|
128
|
+
if (this.animationFrameId !== null) {
|
|
129
|
+
cancelAnimationFrame(this.animationFrameId)
|
|
130
|
+
this.animationFrameId = null
|
|
131
|
+
}
|
|
132
|
+
if (this.resizeObserver) {
|
|
133
|
+
this.resizeObserver.disconnect()
|
|
134
|
+
this.resizeObserver = null
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Register the custom element (safe to call multiple times)
|
|
141
|
+
*/
|
|
142
|
+
function ensureCustomElementRegistered(): void {
|
|
143
|
+
if (customElementRegistered) return
|
|
144
|
+
if (typeof window === 'undefined' || typeof customElements === 'undefined') return
|
|
145
|
+
|
|
146
|
+
if (!customElements.get('cms-highlight-overlay')) {
|
|
147
|
+
customElements.define('cms-highlight-overlay', CmsHighlightOverlay)
|
|
148
|
+
}
|
|
149
|
+
customElementRegistered = true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Initialize the highlight container
|
|
154
|
+
*/
|
|
155
|
+
export function initHighlightContainer(): void {
|
|
156
|
+
if (typeof document === 'undefined') return
|
|
157
|
+
if (highlightContainer) return
|
|
158
|
+
|
|
159
|
+
ensureCustomElementRegistered()
|
|
160
|
+
|
|
161
|
+
highlightContainer = document.createElement('div')
|
|
162
|
+
highlightContainer.id = 'cms-highlight-container'
|
|
163
|
+
highlightContainer.style.cssText = `
|
|
164
|
+
position: absolute;
|
|
165
|
+
top: 0;
|
|
166
|
+
left: 0;
|
|
167
|
+
width: 0;
|
|
168
|
+
height: 0;
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
z-index: 2147483645;
|
|
171
|
+
`
|
|
172
|
+
document.body.appendChild(highlightContainer)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clean up the highlight container
|
|
177
|
+
*/
|
|
178
|
+
export function destroyHighlightContainer(): void {
|
|
179
|
+
if (highlightContainer) {
|
|
180
|
+
highlightContainer.remove()
|
|
181
|
+
highlightContainer = null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Set highlight outline on an element using Shadow DOM overlay
|
|
187
|
+
*/
|
|
188
|
+
export function setElementHighlight(
|
|
189
|
+
el: HTMLElement,
|
|
190
|
+
color: string,
|
|
191
|
+
style: 'solid' | 'dashed' = 'solid',
|
|
192
|
+
): void {
|
|
193
|
+
initHighlightContainer()
|
|
194
|
+
|
|
195
|
+
let overlay = highlightMap.get(el)
|
|
196
|
+
|
|
197
|
+
if (!overlay) {
|
|
198
|
+
overlay = document.createElement('cms-highlight-overlay') as HTMLElement
|
|
199
|
+
highlightMap.set(el, overlay)
|
|
200
|
+
highlightContainer?.appendChild(overlay) // Set target after adding to DOM
|
|
201
|
+
;(overlay as CmsHighlightOverlay).setTarget(el)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
;(overlay as CmsHighlightOverlay).setHighlightStyle(color, style)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Clear highlight from an element
|
|
209
|
+
*/
|
|
210
|
+
export function clearElementHighlight(el: HTMLElement): void {
|
|
211
|
+
const overlay = highlightMap.get(el)
|
|
212
|
+
if (overlay) {
|
|
213
|
+
overlay.remove()
|
|
214
|
+
highlightMap.delete(el)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear all highlights
|
|
220
|
+
*/
|
|
221
|
+
export function clearAllHighlights(): void {
|
|
222
|
+
if (highlightContainer) {
|
|
223
|
+
highlightContainer.innerHTML = ''
|
|
224
|
+
}
|
|
225
|
+
// WeakMap entries will be garbage collected
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Export the class for type checking
|
|
229
|
+
export { CmsHighlightOverlay }
|