@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,297 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { fetchMediaLibrary, uploadMedia } from '../markdown-api'
|
|
3
|
+
import {
|
|
4
|
+
config,
|
|
5
|
+
isMediaLibraryLoading,
|
|
6
|
+
isMediaLibraryOpen,
|
|
7
|
+
mediaLibraryItems,
|
|
8
|
+
mediaLibraryState,
|
|
9
|
+
resetMediaLibraryState,
|
|
10
|
+
setMediaLibraryItems,
|
|
11
|
+
setMediaLibraryLoading,
|
|
12
|
+
showToast,
|
|
13
|
+
} from '../signals'
|
|
14
|
+
import type { MediaItem } from '../types'
|
|
15
|
+
|
|
16
|
+
export function MediaLibrary() {
|
|
17
|
+
const visible = isMediaLibraryOpen.value
|
|
18
|
+
const items = mediaLibraryItems.value
|
|
19
|
+
const isLoading = isMediaLibraryLoading.value
|
|
20
|
+
const insertCallback = mediaLibraryState.value.insertCallback
|
|
21
|
+
|
|
22
|
+
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
|
|
23
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
24
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
25
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
26
|
+
|
|
27
|
+
// Load media items on open
|
|
28
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: know what i am doing
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (visible && items.length === 0) {
|
|
31
|
+
loadMediaItems()
|
|
32
|
+
}
|
|
33
|
+
}, [visible])
|
|
34
|
+
|
|
35
|
+
const loadMediaItems = async () => {
|
|
36
|
+
setMediaLibraryLoading(true)
|
|
37
|
+
try {
|
|
38
|
+
const result = await fetchMediaLibrary(config.value)
|
|
39
|
+
setMediaLibraryItems(result.items)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
showToast('Failed to load media library', 'error')
|
|
42
|
+
} finally {
|
|
43
|
+
setMediaLibraryLoading(false)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleClose = useCallback(() => {
|
|
48
|
+
resetMediaLibraryState()
|
|
49
|
+
setSearchQuery('')
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const handleSelectImage = useCallback(
|
|
53
|
+
(item: MediaItem) => {
|
|
54
|
+
if (insertCallback) {
|
|
55
|
+
const alt = item.annotation || item.filename || 'Image'
|
|
56
|
+
insertCallback(item.url, alt)
|
|
57
|
+
handleClose()
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[insertCallback, handleClose],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const handleUploadClick = useCallback(() => {
|
|
64
|
+
fileInputRef.current?.click()
|
|
65
|
+
}, [])
|
|
66
|
+
|
|
67
|
+
const handleFileChange = async (e: Event) => {
|
|
68
|
+
const target = e.target as HTMLInputElement
|
|
69
|
+
const file = target.files?.[0]
|
|
70
|
+
if (!file) return
|
|
71
|
+
|
|
72
|
+
setUploadProgress(0)
|
|
73
|
+
try {
|
|
74
|
+
const result = await uploadMedia(config.value, file, (percent) => {
|
|
75
|
+
setUploadProgress(percent)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (result.success && result.url) {
|
|
79
|
+
// Add the new item to the list
|
|
80
|
+
const newItem: MediaItem = {
|
|
81
|
+
id: result.id || crypto.randomUUID(),
|
|
82
|
+
url: result.url,
|
|
83
|
+
filename: result.filename || file.name,
|
|
84
|
+
annotation: result.annotation,
|
|
85
|
+
contentType: file.type,
|
|
86
|
+
}
|
|
87
|
+
setMediaLibraryItems([newItem, ...items])
|
|
88
|
+
showToast('Image uploaded successfully', 'success')
|
|
89
|
+
} else {
|
|
90
|
+
showToast(result.error || 'Upload failed', 'error')
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
showToast('Upload failed', 'error')
|
|
94
|
+
} finally {
|
|
95
|
+
setUploadProgress(null)
|
|
96
|
+
target.value = ''
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleDrop = async (e: DragEvent) => {
|
|
101
|
+
e.preventDefault()
|
|
102
|
+
e.stopPropagation()
|
|
103
|
+
|
|
104
|
+
const file = e.dataTransfer?.files[0]
|
|
105
|
+
if (!file || !file.type.startsWith('image/')) {
|
|
106
|
+
showToast('Please drop an image file', 'error')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setUploadProgress(0)
|
|
111
|
+
try {
|
|
112
|
+
const result = await uploadMedia(config.value, file, (percent) => {
|
|
113
|
+
setUploadProgress(percent)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (result.success && result.url) {
|
|
117
|
+
const newItem: MediaItem = {
|
|
118
|
+
id: result.id || crypto.randomUUID(),
|
|
119
|
+
url: result.url,
|
|
120
|
+
filename: result.filename || file.name,
|
|
121
|
+
annotation: result.annotation,
|
|
122
|
+
contentType: file.type,
|
|
123
|
+
}
|
|
124
|
+
setMediaLibraryItems([newItem, ...items])
|
|
125
|
+
showToast('Image uploaded successfully', 'success')
|
|
126
|
+
} else {
|
|
127
|
+
showToast(result.error || 'Upload failed', 'error')
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
showToast('Upload failed', 'error')
|
|
131
|
+
} finally {
|
|
132
|
+
setUploadProgress(null)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const handleDragOver = (e: DragEvent) => {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
e.stopPropagation()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Filter items by search query
|
|
142
|
+
const filteredItems = searchQuery
|
|
143
|
+
? items.filter((item) => item.filename.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
144
|
+
: items
|
|
145
|
+
|
|
146
|
+
if (!visible) return null
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
151
|
+
onClick={handleClose}
|
|
152
|
+
data-cms-ui
|
|
153
|
+
>
|
|
154
|
+
<div
|
|
155
|
+
ref={containerRef}
|
|
156
|
+
class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-3xl w-full max-h-[80vh] flex flex-col border border-white/10"
|
|
157
|
+
onClick={(e) => e.stopPropagation()}
|
|
158
|
+
onDrop={handleDrop}
|
|
159
|
+
onDragOver={handleDragOver}
|
|
160
|
+
data-cms-ui
|
|
161
|
+
>
|
|
162
|
+
{/* Header */}
|
|
163
|
+
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
164
|
+
<h2 class="text-lg font-semibold text-white">Media Library</h2>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={handleClose}
|
|
168
|
+
class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
|
|
169
|
+
data-cms-ui
|
|
170
|
+
>
|
|
171
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
172
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
173
|
+
</svg>
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Toolbar */}
|
|
178
|
+
<div class="flex items-center gap-3 p-4 border-b border-white/10">
|
|
179
|
+
<input
|
|
180
|
+
type="text"
|
|
181
|
+
placeholder="Search images..."
|
|
182
|
+
value={searchQuery}
|
|
183
|
+
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}
|
|
184
|
+
class="flex-1 px-4 py-2.5 bg-white/10 border border-white/20 rounded-cms-md text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
|
|
185
|
+
data-cms-ui
|
|
186
|
+
/>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={handleUploadClick}
|
|
190
|
+
class="px-5 py-2.5 bg-cms-primary text-cms-primary-text rounded-cms-pill text-sm font-medium hover:bg-cms-primary-hover transition-colors"
|
|
191
|
+
data-cms-ui
|
|
192
|
+
>
|
|
193
|
+
Upload
|
|
194
|
+
</button>
|
|
195
|
+
<input
|
|
196
|
+
ref={fileInputRef}
|
|
197
|
+
type="file"
|
|
198
|
+
accept="image/*"
|
|
199
|
+
class="hidden"
|
|
200
|
+
onChange={handleFileChange}
|
|
201
|
+
data-cms-ui
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Upload progress */}
|
|
206
|
+
{uploadProgress !== null && (
|
|
207
|
+
<div class="px-4 py-3 bg-white/5 border-b border-white/10">
|
|
208
|
+
<div class="flex items-center gap-3">
|
|
209
|
+
<div class="flex-1 h-2 bg-white/10 rounded-full overflow-hidden">
|
|
210
|
+
<div
|
|
211
|
+
class="h-full bg-cms-primary transition-all duration-200 rounded-full"
|
|
212
|
+
style={{ width: `${uploadProgress}%` }}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
<span class="text-sm text-white font-medium">{uploadProgress}%</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Grid */}
|
|
221
|
+
<div class="flex-1 overflow-auto p-4">
|
|
222
|
+
{isLoading
|
|
223
|
+
? (
|
|
224
|
+
<div class="flex items-center justify-center h-48">
|
|
225
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-cms-primary" />
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
: filteredItems.length === 0
|
|
229
|
+
? (
|
|
230
|
+
<div class="flex flex-col items-center justify-center h-48 text-white/50">
|
|
231
|
+
<svg class="w-12 h-12 mb-3 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
|
+
<path
|
|
233
|
+
stroke-linecap="round"
|
|
234
|
+
stroke-linejoin="round"
|
|
235
|
+
stroke-width="1.5"
|
|
236
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
237
|
+
/>
|
|
238
|
+
</svg>
|
|
239
|
+
<p class="text-sm">
|
|
240
|
+
{searchQuery ? 'No images found' : 'No images yet. Upload one to get started.'}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
: (
|
|
245
|
+
<div class="grid grid-cols-4 gap-3">
|
|
246
|
+
{filteredItems.map((item) => (
|
|
247
|
+
<div key={item.id} class="group relative aspect-square" data-cms-ui>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
onClick={() => handleSelectImage(item)}
|
|
251
|
+
class="w-full h-full rounded-cms-md overflow-hidden border-2 border-white/10 hover:border-cms-primary focus:outline-none focus:border-cms-primary transition-all"
|
|
252
|
+
data-cms-ui
|
|
253
|
+
>
|
|
254
|
+
<img
|
|
255
|
+
src={item.url}
|
|
256
|
+
alt={item.annotation || item.filename}
|
|
257
|
+
class="w-full h-full object-cover"
|
|
258
|
+
/>
|
|
259
|
+
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors pointer-events-none" />
|
|
260
|
+
<div class="absolute bottom-0 left-0 right-0 p-2 bg-linear-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
|
261
|
+
<p class="text-xs text-white truncate">{item.filename}</p>
|
|
262
|
+
</div>
|
|
263
|
+
</button>
|
|
264
|
+
{item.annotation && (
|
|
265
|
+
<div class="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
266
|
+
<div class="relative group/tooltip">
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="p-1 bg-black/60 hover:bg-black/80 rounded-full text-white/70 hover:text-white transition-colors"
|
|
270
|
+
onClick={(e) => e.stopPropagation()}
|
|
271
|
+
title={item.annotation}
|
|
272
|
+
data-cms-ui
|
|
273
|
+
>
|
|
274
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
275
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
276
|
+
</svg>
|
|
277
|
+
</button>
|
|
278
|
+
<div class="absolute right-0 top-full mt-1 w-48 p-2 bg-black/90 text-white text-xs rounded-md opacity-0 invisible group-hover/tooltip:opacity-100 group-hover/tooltip:visible transition-all z-10 pointer-events-none">
|
|
279
|
+
{item.annotation}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Footer with drop hint */}
|
|
291
|
+
<div class="px-4 py-4 border-t border-white/10 bg-white/5 text-center text-sm text-white/50 rounded-b-cms-xl">
|
|
292
|
+
Drag and drop images here to upload
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks'
|
|
2
|
+
import { getColorPreview, parseColorClass } from '../color-utils'
|
|
3
|
+
import { Z_INDEX } from '../constants'
|
|
4
|
+
import * as signals from '../signals'
|
|
5
|
+
|
|
6
|
+
export interface OutlineProps {
|
|
7
|
+
visible: boolean
|
|
8
|
+
rect: DOMRect | null
|
|
9
|
+
isComponent?: boolean
|
|
10
|
+
componentName?: string
|
|
11
|
+
tagName?: string
|
|
12
|
+
/** The actual element being outlined - used for scroll tracking */
|
|
13
|
+
element?: HTMLElement | null
|
|
14
|
+
/** CMS ID of the hovered element */
|
|
15
|
+
cmsId?: string | null
|
|
16
|
+
/** Callback when a color swatch is clicked */
|
|
17
|
+
onColorClick?: (cmsId: string, rect: DOMRect) => void
|
|
18
|
+
/** Callback when an attribute indicator is clicked */
|
|
19
|
+
onAttributeClick?: (cmsId: string, rect: DOMRect) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Minimum space needed to show label outside the element
|
|
23
|
+
const LABEL_OUTSIDE_THRESHOLD = 28
|
|
24
|
+
// Padding from viewport edges for sticky label
|
|
25
|
+
const STICKY_PADDING = 8
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shadow DOM-based hover outline component.
|
|
29
|
+
* Uses a custom element with Shadow DOM to avoid style conflicts.
|
|
30
|
+
*/
|
|
31
|
+
export function Outline(
|
|
32
|
+
{ visible, rect, isComponent = false, componentName, tagName, element, cmsId, onColorClick, onAttributeClick }: OutlineProps,
|
|
33
|
+
) {
|
|
34
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
35
|
+
const shadowRootRef = useRef<ShadowRoot | null>(null)
|
|
36
|
+
const overlayRef = useRef<HTMLDivElement | null>(null)
|
|
37
|
+
const labelRef = useRef<HTMLDivElement | null>(null)
|
|
38
|
+
const toolbarRef = useRef<HTMLDivElement | null>(null)
|
|
39
|
+
|
|
40
|
+
// Initialize Shadow DOM once
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (containerRef.current && !shadowRootRef.current) {
|
|
43
|
+
shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' })
|
|
44
|
+
|
|
45
|
+
// Create styles
|
|
46
|
+
const style = document.createElement('style')
|
|
47
|
+
style.textContent = `
|
|
48
|
+
:host {
|
|
49
|
+
position: fixed;
|
|
50
|
+
top: 0;
|
|
51
|
+
left: 0;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
z-index: ${Z_INDEX.OVERLAY};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.outline-overlay {
|
|
57
|
+
position: fixed;
|
|
58
|
+
border-radius: 16px;
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
transition: opacity 100ms ease;
|
|
61
|
+
overflow: visible;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.outline-overlay.hidden {
|
|
65
|
+
opacity: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.outline-overlay.visible {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.outline-label {
|
|
73
|
+
position: fixed;
|
|
74
|
+
padding: 6px 14px;
|
|
75
|
+
border-radius: 9999px;
|
|
76
|
+
font-size: 11px;
|
|
77
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 6px;
|
|
83
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
84
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
85
|
+
z-index: ${Z_INDEX.MODAL};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.outline-label .tag {
|
|
89
|
+
opacity: 0.85;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.outline-label .component-name {
|
|
93
|
+
font-weight: 700;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.outline-label .separator {
|
|
97
|
+
opacity: 0.5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.element-toolbar {
|
|
101
|
+
position: fixed;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 6px;
|
|
105
|
+
padding: 8px;
|
|
106
|
+
background: #1A1A1A;
|
|
107
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
108
|
+
border-radius: 12px;
|
|
109
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
110
|
+
pointer-events: auto;
|
|
111
|
+
z-index: ${Z_INDEX.MODAL};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.element-toolbar.hidden {
|
|
115
|
+
display: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.color-swatch {
|
|
119
|
+
width: 24px;
|
|
120
|
+
height: 24px;
|
|
121
|
+
border: 2px solid transparent;
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: all 150ms ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.color-swatch:hover {
|
|
128
|
+
transform: scale(1.15);
|
|
129
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
130
|
+
border-color: rgba(223, 255, 64, 0.5);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.color-swatch.white {
|
|
134
|
+
border-color: rgba(255,255,255,0.2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.toolbar-divider {
|
|
138
|
+
width: 1px;
|
|
139
|
+
height: 20px;
|
|
140
|
+
background: rgba(255,255,255,0.15);
|
|
141
|
+
margin: 0 2px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.attr-button {
|
|
145
|
+
width: 24px;
|
|
146
|
+
height: 24px;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
background: transparent;
|
|
151
|
+
border: none;
|
|
152
|
+
border-radius: 50%;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
transition: all 150ms ease;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.attr-button:hover {
|
|
158
|
+
transform: scale(1.15);
|
|
159
|
+
background: rgba(255,255,255,0.1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.attr-button svg {
|
|
163
|
+
width: 14px;
|
|
164
|
+
height: 14px;
|
|
165
|
+
color: rgba(255,255,255,0.7);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.attr-button:hover svg {
|
|
169
|
+
color: #DFFF40;
|
|
170
|
+
}
|
|
171
|
+
`
|
|
172
|
+
|
|
173
|
+
overlayRef.current = document.createElement('div')
|
|
174
|
+
overlayRef.current.className = 'outline-overlay hidden'
|
|
175
|
+
|
|
176
|
+
labelRef.current = document.createElement('div')
|
|
177
|
+
labelRef.current.className = 'outline-label'
|
|
178
|
+
// Label is now a sibling, not a child, for independent positioning
|
|
179
|
+
|
|
180
|
+
toolbarRef.current = document.createElement('div')
|
|
181
|
+
toolbarRef.current.className = 'element-toolbar hidden'
|
|
182
|
+
|
|
183
|
+
// Add hover listeners for toolbar to signal hover state
|
|
184
|
+
toolbarRef.current.addEventListener('mouseenter', () => {
|
|
185
|
+
signals.setHoveringSwatches(true)
|
|
186
|
+
signals.setHoveringAttributeButton(true)
|
|
187
|
+
})
|
|
188
|
+
toolbarRef.current.addEventListener('mouseleave', () => {
|
|
189
|
+
signals.setHoveringSwatches(false)
|
|
190
|
+
signals.setHoveringAttributeButton(false)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
shadowRootRef.current.appendChild(style)
|
|
194
|
+
shadowRootRef.current.appendChild(overlayRef.current)
|
|
195
|
+
shadowRootRef.current.appendChild(labelRef.current)
|
|
196
|
+
shadowRootRef.current.appendChild(toolbarRef.current)
|
|
197
|
+
}
|
|
198
|
+
}, [])
|
|
199
|
+
|
|
200
|
+
// Update overlay visibility and position
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!overlayRef.current || !labelRef.current || !toolbarRef.current) return
|
|
203
|
+
|
|
204
|
+
if (!visible || !rect) {
|
|
205
|
+
overlayRef.current.className = 'outline-overlay hidden'
|
|
206
|
+
labelRef.current.style.display = 'none'
|
|
207
|
+
toolbarRef.current.className = 'element-toolbar hidden'
|
|
208
|
+
signals.setHoveringSwatches(false)
|
|
209
|
+
signals.setHoveringAttributeButton(false)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
overlayRef.current.className = 'outline-overlay visible'
|
|
214
|
+
|
|
215
|
+
const viewportHeight = window.innerHeight
|
|
216
|
+
const viewportWidth = window.innerWidth
|
|
217
|
+
|
|
218
|
+
// Use viewport-relative coordinates (fixed positioning)
|
|
219
|
+
const left = rect.left - 6
|
|
220
|
+
const top = rect.top - 6
|
|
221
|
+
const width = rect.width + 12
|
|
222
|
+
const height = rect.height + 12
|
|
223
|
+
|
|
224
|
+
overlayRef.current.style.left = `${left}px`
|
|
225
|
+
overlayRef.current.style.top = `${top}px`
|
|
226
|
+
overlayRef.current.style.width = `${width}px`
|
|
227
|
+
overlayRef.current.style.height = `${height}px`
|
|
228
|
+
|
|
229
|
+
// Different styling for components vs text elements
|
|
230
|
+
if (isComponent) {
|
|
231
|
+
overlayRef.current.style.border = `2px solid #1A1A1A` // Dark border
|
|
232
|
+
overlayRef.current.style.backgroundColor = 'rgba(0, 0, 0, 0.03)'
|
|
233
|
+
overlayRef.current.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)'
|
|
234
|
+
labelRef.current.style.display = 'flex'
|
|
235
|
+
labelRef.current.style.backgroundColor = '#1A1A1A'
|
|
236
|
+
labelRef.current.style.color = 'white'
|
|
237
|
+
toolbarRef.current.className = 'element-toolbar hidden' // Hide toolbar for components
|
|
238
|
+
|
|
239
|
+
// Build label content
|
|
240
|
+
let labelContent = ''
|
|
241
|
+
if (tagName) {
|
|
242
|
+
labelContent += `<span class="tag"><${tagName}></span>`
|
|
243
|
+
}
|
|
244
|
+
if (componentName) {
|
|
245
|
+
if (tagName) labelContent += `<span class="separator">·</span>`
|
|
246
|
+
labelContent += `<span class="component-name">${componentName}</span>`
|
|
247
|
+
}
|
|
248
|
+
if (!tagName && !componentName) {
|
|
249
|
+
labelContent = 'COMPONENT'
|
|
250
|
+
}
|
|
251
|
+
labelRef.current.innerHTML = labelContent
|
|
252
|
+
|
|
253
|
+
// Calculate sticky label position
|
|
254
|
+
// The label should stay visible within the viewport, attached to the element
|
|
255
|
+
const elementTop = rect.top
|
|
256
|
+
const elementBottom = rect.bottom
|
|
257
|
+
const elementLeft = rect.left
|
|
258
|
+
|
|
259
|
+
// Ideal position is above the element
|
|
260
|
+
let labelTop = elementTop - 36 // Increased offset for thicker border/shadow
|
|
261
|
+
let labelLeft = Math.max(STICKY_PADDING, elementLeft)
|
|
262
|
+
|
|
263
|
+
// If element top is above viewport, stick label to top of viewport
|
|
264
|
+
if (elementTop < STICKY_PADDING + 36) {
|
|
265
|
+
// Element is partially scrolled up - stick label to visible portion
|
|
266
|
+
labelTop = Math.max(STICKY_PADDING, Math.min(elementTop + STICKY_PADDING, elementBottom - 36))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// If element is below viewport, position at bottom
|
|
270
|
+
if (elementTop > viewportHeight) {
|
|
271
|
+
labelRef.current.style.display = 'none'
|
|
272
|
+
} else if (elementBottom < 0) {
|
|
273
|
+
labelRef.current.style.display = 'none'
|
|
274
|
+
} else {
|
|
275
|
+
// Clamp label position to viewport
|
|
276
|
+
labelTop = Math.max(STICKY_PADDING, Math.min(labelTop, viewportHeight - 36))
|
|
277
|
+
labelLeft = Math.min(labelLeft, viewportWidth - 150) // Ensure label doesn't go off-screen right
|
|
278
|
+
|
|
279
|
+
labelRef.current.style.top = `${labelTop}px`
|
|
280
|
+
labelRef.current.style.left = `${labelLeft}px`
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
overlayRef.current.style.border = `2px dashed #1A1A1A`
|
|
284
|
+
overlayRef.current.style.backgroundColor = 'transparent'
|
|
285
|
+
overlayRef.current.style.boxShadow = 'none'
|
|
286
|
+
labelRef.current.style.display = 'none'
|
|
287
|
+
|
|
288
|
+
// Check for color swatches and attribute button
|
|
289
|
+
const manifest = signals.manifest.value
|
|
290
|
+
const pendingColorChange = cmsId ? signals.pendingColorChanges.value.get(cmsId) : null
|
|
291
|
+
const entry = cmsId ? manifest.entries[cmsId] : null
|
|
292
|
+
const colorClasses = pendingColorChange?.newClasses ?? entry?.colorClasses
|
|
293
|
+
|
|
294
|
+
const hasColorSwatches = colorClasses && (colorClasses.bg?.value || colorClasses.text?.value) && onColorClick
|
|
295
|
+
const hasEditableAttributes = entry?.attributes && Object.keys(entry.attributes).length > 0
|
|
296
|
+
|
|
297
|
+
// Show unified toolbar if there are swatches or attribute button
|
|
298
|
+
if ((hasColorSwatches || hasEditableAttributes) && (onColorClick || onAttributeClick)) {
|
|
299
|
+
toolbarRef.current.className = 'element-toolbar'
|
|
300
|
+
toolbarRef.current.innerHTML = ''
|
|
301
|
+
|
|
302
|
+
// Position toolbar at bottom center of the element
|
|
303
|
+
const toolbarTop = rect.bottom + 6
|
|
304
|
+
const toolbarLeft = rect.left + rect.width / 2
|
|
305
|
+
|
|
306
|
+
toolbarRef.current.style.top = `${toolbarTop}px`
|
|
307
|
+
toolbarRef.current.style.left = `${toolbarLeft}px`
|
|
308
|
+
toolbarRef.current.style.transform = 'translateX(-50%)'
|
|
309
|
+
|
|
310
|
+
// Helper to apply swatch styles including transparent checkerboard
|
|
311
|
+
const applySwatchStyle = (swatch: HTMLDivElement, colorName: string, preview: string) => {
|
|
312
|
+
if (colorName === 'transparent') {
|
|
313
|
+
swatch.style.backgroundColor = 'transparent'
|
|
314
|
+
swatch.style.backgroundImage =
|
|
315
|
+
'linear-gradient(45deg, #555 25%, transparent 25%, transparent 75%, #555 75%, #555), linear-gradient(45deg, #555 25%, transparent 25%, transparent 75%, #555 75%, #555)'
|
|
316
|
+
swatch.style.backgroundSize = '8px 8px'
|
|
317
|
+
swatch.style.backgroundPosition = '0 0, 4px 4px'
|
|
318
|
+
} else {
|
|
319
|
+
swatch.style.backgroundColor = preview
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add color swatches
|
|
324
|
+
if (hasColorSwatches && colorClasses) {
|
|
325
|
+
// Create bg swatch
|
|
326
|
+
if (colorClasses.bg?.value) {
|
|
327
|
+
const parsed = parseColorClass(colorClasses.bg.value)
|
|
328
|
+
if (parsed) {
|
|
329
|
+
const preview = getColorPreview(parsed.colorName, parsed.shade)
|
|
330
|
+
const swatch = document.createElement('div')
|
|
331
|
+
swatch.className = `color-swatch${parsed.colorName === 'white' ? ' white' : ''}`
|
|
332
|
+
applySwatchStyle(swatch, parsed.colorName, preview)
|
|
333
|
+
swatch.title = `Background: ${colorClasses.bg.value}`
|
|
334
|
+
swatch.onclick = (e) => {
|
|
335
|
+
e.stopPropagation()
|
|
336
|
+
if (cmsId && onColorClick) onColorClick(cmsId, rect)
|
|
337
|
+
}
|
|
338
|
+
toolbarRef.current.appendChild(swatch)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create text swatch
|
|
343
|
+
if (colorClasses.text?.value) {
|
|
344
|
+
const parsed = parseColorClass(colorClasses.text.value)
|
|
345
|
+
if (parsed) {
|
|
346
|
+
const preview = getColorPreview(parsed.colorName, parsed.shade)
|
|
347
|
+
const swatch = document.createElement('div')
|
|
348
|
+
swatch.className = `color-swatch${parsed.colorName === 'white' ? ' white' : ''}`
|
|
349
|
+
applySwatchStyle(swatch, parsed.colorName, preview)
|
|
350
|
+
swatch.title = `Text: ${colorClasses.text.value}`
|
|
351
|
+
swatch.onclick = (e) => {
|
|
352
|
+
e.stopPropagation()
|
|
353
|
+
if (cmsId && onColorClick) onColorClick(cmsId, rect)
|
|
354
|
+
}
|
|
355
|
+
toolbarRef.current.appendChild(swatch)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Add divider and attribute button if needed
|
|
361
|
+
if (hasEditableAttributes && onAttributeClick) {
|
|
362
|
+
// Add divider if there are swatches
|
|
363
|
+
if (hasColorSwatches) {
|
|
364
|
+
const divider = document.createElement('div')
|
|
365
|
+
divider.className = 'toolbar-divider'
|
|
366
|
+
toolbarRef.current.appendChild(divider)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Add attribute button
|
|
370
|
+
const attrButton = document.createElement('button')
|
|
371
|
+
attrButton.className = 'attr-button'
|
|
372
|
+
attrButton.innerHTML =
|
|
373
|
+
`<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
|
|
374
|
+
attrButton.title = 'Edit attributes'
|
|
375
|
+
attrButton.onclick = (e) => {
|
|
376
|
+
e.stopPropagation()
|
|
377
|
+
if (cmsId) onAttributeClick(cmsId, rect)
|
|
378
|
+
}
|
|
379
|
+
toolbarRef.current.appendChild(attrButton)
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
toolbarRef.current.className = 'element-toolbar hidden'
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}, [visible, rect, isComponent, componentName, tagName, cmsId, onColorClick, onAttributeClick])
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<div
|
|
389
|
+
ref={containerRef}
|
|
390
|
+
data-cms-ui
|
|
391
|
+
style={{
|
|
392
|
+
position: 'fixed',
|
|
393
|
+
top: 0,
|
|
394
|
+
left: 0,
|
|
395
|
+
width: 0,
|
|
396
|
+
height: 0,
|
|
397
|
+
pointerEvents: 'none',
|
|
398
|
+
zIndex: Z_INDEX.OVERLAY,
|
|
399
|
+
}}
|
|
400
|
+
/>
|
|
401
|
+
)
|
|
402
|
+
}
|