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