@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,357 @@
1
+ import {
2
+ clearAllHighlights,
3
+ clearElementHighlight,
4
+ destroyHighlightContainer,
5
+ initHighlightContainer,
6
+ setElementHighlight,
7
+ } from './components/highlight-overlay'
8
+ import { CSS } from './constants'
9
+ import type { ChildCmsElement } from './types'
10
+
11
+ /** Style element for contenteditable focus styles injected into the host page */
12
+ let focusStyleElement: HTMLStyleElement | null = null
13
+
14
+ /**
15
+ * Get the best CMS element at a specific position using elementsFromPoint.
16
+ * This is more reliable than using event.target for nested elements.
17
+ *
18
+ * @param x - clientX position
19
+ * @param y - clientY position
20
+ * @param manifestEntries - Optional manifest entries to filter by
21
+ */
22
+ export function getCmsElementAtPosition(
23
+ x: number,
24
+ y: number,
25
+ manifestEntries?: Record<string, any>,
26
+ ): HTMLElement | null {
27
+ const elementsAtPoint = document.elementsFromPoint(x, y)
28
+
29
+ // First pass: find the deepest CMS element that is editable
30
+ // We prioritize elements that have contentEditable="true" and are in the manifest
31
+ for (const el of elementsAtPoint) {
32
+ if (!(el instanceof HTMLElement)) continue
33
+ if (!el.hasAttribute(CSS.ID_ATTRIBUTE)) continue
34
+ // Skip component roots - they should be handled separately
35
+ if (el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) continue
36
+
37
+ const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
38
+
39
+ // If we have manifest entries, only return elements that are in it
40
+ if (manifestEntries && cmsId && !manifestEntries[cmsId]) {
41
+ continue
42
+ }
43
+
44
+ // Check if the element is actually editable
45
+ if (el.contentEditable === 'true') {
46
+ return el
47
+ }
48
+ }
49
+
50
+ // Second pass: find any CMS element (even if not editable yet)
51
+ // This handles the case where we're hovering before edit mode is fully set up
52
+ for (const el of elementsAtPoint) {
53
+ if (!(el instanceof HTMLElement)) continue
54
+ if (!el.hasAttribute(CSS.ID_ATTRIBUTE)) continue
55
+ if (el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) continue
56
+
57
+ const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
58
+
59
+ if (manifestEntries && cmsId && !manifestEntries[cmsId]) {
60
+ continue
61
+ }
62
+
63
+ return el
64
+ }
65
+
66
+ return null
67
+ }
68
+
69
+ /**
70
+ * Get a component element at a specific position.
71
+ * Only returns component roots (elements with data-cms-component-id).
72
+ */
73
+ export function getComponentAtPosition(x: number, y: number): HTMLElement | null {
74
+ const elementsAtPoint = document.elementsFromPoint(x, y)
75
+
76
+ for (const el of elementsAtPoint) {
77
+ if (!(el instanceof HTMLElement)) continue
78
+ if (el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
79
+ return el
80
+ }
81
+ }
82
+
83
+ return null
84
+ }
85
+
86
+ /**
87
+ * Check if a point is near the edge of an element's bounding rect.
88
+ * Used to only trigger component selection when hovering near borders.
89
+ */
90
+ export function isNearElementEdge(x: number, y: number, rect: DOMRect, threshold: number = 24): boolean {
91
+ // Check if point is within the rect at all
92
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
93
+ return false
94
+ }
95
+
96
+ // Check if near any edge
97
+ const nearLeft = x - rect.left < threshold
98
+ const nearRight = rect.right - x < threshold
99
+ const nearTop = y - rect.top < threshold
100
+ const nearBottom = rect.bottom - y < threshold
101
+
102
+ return nearLeft || nearRight || nearTop || nearBottom
103
+ }
104
+
105
+ export function getCmsElementFromEvent(ev: MouseEvent): HTMLElement | null {
106
+ const target = ev.target
107
+ if (!(target instanceof HTMLElement)) return null
108
+ const el = target.closest(`[${CSS.ID_ATTRIBUTE}]`)
109
+ if (!el || !(el instanceof HTMLElement)) return null
110
+ return el
111
+ }
112
+
113
+ /**
114
+ * Check if an element is a CMS-styled span (inline text styling)
115
+ */
116
+ export function isStyledSpan(element: HTMLElement): boolean {
117
+ return element.hasAttribute('data-cms-styled')
118
+ }
119
+
120
+ /**
121
+ * Helper function to recursively extract plain text from child nodes,
122
+ * replacing CMS elements with their placeholders.
123
+ * Note: This returns plain text only - for styled content, use innerHTML directly.
124
+ */
125
+ function extractTextFromChildNodes(parentNode: HTMLElement): string {
126
+ let text = ''
127
+
128
+ parentNode.childNodes.forEach(node => {
129
+ if (node.nodeType === Node.TEXT_NODE) {
130
+ text += node.nodeValue || ''
131
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
132
+ const element = node as HTMLElement
133
+ const tagName = element.tagName.toLowerCase()
134
+
135
+ // Preserve <br> tags as-is (textContent strips them)
136
+ if (tagName === 'br') {
137
+ text += '<br>'
138
+ return
139
+ }
140
+
141
+ const directCmsId = element.getAttribute(CSS.ID_ATTRIBUTE)
142
+
143
+ if (directCmsId) {
144
+ // Element has CMS ID - replace with placeholder
145
+ text += `{{cms:${directCmsId}}}`
146
+ } else {
147
+ // For all other elements (including styled spans), just get their text content
148
+ text += element.textContent || ''
149
+ }
150
+ }
151
+ })
152
+
153
+ return text
154
+ }
155
+
156
+ /**
157
+ * Extract plain text content from a CMS element.
158
+ * Styled spans are reduced to their text content (not HTML).
159
+ * Nested CMS elements are replaced with {{cms:id}} placeholders.
160
+ */
161
+ export function getEditableTextFromElement(el: HTMLElement): string {
162
+ return extractTextFromChildNodes(el).trim()
163
+ }
164
+
165
+ /**
166
+ * Get the HTML content of a CMS element suitable for saving.
167
+ * Preserves styled spans but cleans up editing artifacts.
168
+ */
169
+ export function getEditableHtmlFromElement(el: HTMLElement): string {
170
+ const clone = el.cloneNode(true) as HTMLElement
171
+
172
+ // Remove contenteditable attribute
173
+ clone.removeAttribute('contenteditable')
174
+
175
+ // Clean up any editing-only attributes but keep styled spans
176
+ clone.querySelectorAll('[contenteditable]').forEach(child => {
177
+ child.removeAttribute('contenteditable')
178
+ })
179
+
180
+ return clone.innerHTML
181
+ }
182
+
183
+ export function getChildCmsElements(el: HTMLElement): ChildCmsElement[] {
184
+ return Array.from(el.querySelectorAll(`[${CSS.ID_ATTRIBUTE}]`)).map(child => ({
185
+ id: child.getAttribute(CSS.ID_ATTRIBUTE) || '',
186
+ placeholder: `__CMS_CHILD_${child.getAttribute(CSS.ID_ATTRIBUTE)}__`,
187
+ }))
188
+ }
189
+
190
+ export function findInnermostCmsElement(target: EventTarget | null): HTMLElement | null {
191
+ if (!target || !(target instanceof HTMLElement)) return null
192
+
193
+ let element: HTMLElement | null = target
194
+
195
+ while (element && element !== document.body) {
196
+ if (element.hasAttribute(CSS.ID_ATTRIBUTE) && element.contentEditable === 'true') {
197
+ return element
198
+ }
199
+ element = element.parentElement
200
+ }
201
+
202
+ return null
203
+ }
204
+
205
+ export function getAllCmsElements(): NodeListOf<HTMLElement> {
206
+ return document.querySelectorAll(`[${CSS.ID_ATTRIBUTE}]`)
207
+ }
208
+
209
+ export function makeElementEditable(el: HTMLElement): void {
210
+ el.contentEditable = 'true'
211
+ }
212
+
213
+ export function makeElementNonEditable(el: HTMLElement): void {
214
+ el.contentEditable = 'false'
215
+ }
216
+
217
+ /**
218
+ * Set highlight outline on an element using Shadow DOM overlay.
219
+ * This doesn't modify the element's styles directly.
220
+ */
221
+ export function setElementOutline(el: HTMLElement, color: string, style: 'solid' | 'dashed' = 'solid'): void {
222
+ setElementHighlight(el, color, style)
223
+ }
224
+
225
+ /**
226
+ * Clear highlight from an element
227
+ */
228
+ export function clearElementOutline(el: HTMLElement): void {
229
+ clearElementHighlight(el)
230
+ }
231
+
232
+ /**
233
+ * Initialize the highlight system (call when starting edit mode)
234
+ */
235
+ export function initHighlightSystem(): void {
236
+ initHighlightContainer()
237
+ injectFocusStyles()
238
+ }
239
+
240
+ /**
241
+ * Clean up all highlights (call when stopping edit mode)
242
+ */
243
+ export function cleanupHighlightSystem(): void {
244
+ clearAllHighlights()
245
+ destroyHighlightContainer()
246
+ removeFocusStyles()
247
+ }
248
+
249
+ /**
250
+ * Inject styles into the host page to replace the browser's default
251
+ * blue focus outline on contenteditable CMS elements with a subtle,
252
+ * on-brand indicator.
253
+ */
254
+ function injectFocusStyles(): void {
255
+ if (focusStyleElement) return
256
+ focusStyleElement = document.createElement('style')
257
+ focusStyleElement.id = 'cms-focus-styles'
258
+ focusStyleElement.textContent = `
259
+ [contenteditable="true"][data-cms-id]:focus {
260
+ outline: 2px solid rgba(26, 26, 26, 0.15);
261
+ outline-offset: 6px;
262
+ border-radius: 4px;
263
+ }
264
+ `
265
+ document.head.appendChild(focusStyleElement)
266
+ }
267
+
268
+ /**
269
+ * Remove injected focus styles from the host page.
270
+ */
271
+ function removeFocusStyles(): void {
272
+ if (focusStyleElement) {
273
+ focusStyleElement.remove()
274
+ focusStyleElement = null
275
+ }
276
+ }
277
+
278
+ export function logDebug(debug: boolean, ...args: any[]): void {
279
+ if (!debug) return
280
+ console.debug('[CMS]', ...args)
281
+ }
282
+
283
+ /**
284
+ * Disable all interactive elements (links, buttons, forms) to prevent
285
+ * accidental navigation or form submission while in edit mode.
286
+ */
287
+ export function disableAllInteractiveElements(): void {
288
+ // Disable links
289
+ const links = document.querySelectorAll('a')
290
+ links.forEach(link => {
291
+ link.setAttribute('data-cms-disabled', 'true')
292
+ link.addEventListener('click', preventInteraction, true)
293
+ })
294
+
295
+ // Disable buttons (submit, button, reset)
296
+ const buttons = document.querySelectorAll('button, input[type="submit"], input[type="button"], input[type="reset"]')
297
+ buttons.forEach(button => {
298
+ button.setAttribute('data-cms-disabled', 'true')
299
+ button.addEventListener('click', preventInteraction, true)
300
+ })
301
+
302
+ // Disable form submissions
303
+ const forms = document.querySelectorAll('form')
304
+ forms.forEach(form => {
305
+ form.setAttribute('data-cms-disabled', 'true')
306
+ form.addEventListener('submit', preventInteraction, true)
307
+ })
308
+ }
309
+
310
+ /**
311
+ * Re-enable all interactive elements that were disabled.
312
+ */
313
+ export function enableAllInteractiveElements(): void {
314
+ // Re-enable links
315
+ const links = document.querySelectorAll('a[data-cms-disabled]')
316
+ links.forEach(link => {
317
+ link.removeAttribute('data-cms-disabled')
318
+ link.removeEventListener('click', preventInteraction, true)
319
+ })
320
+
321
+ // Re-enable buttons
322
+ const buttons = document.querySelectorAll('button[data-cms-disabled], input[data-cms-disabled]')
323
+ buttons.forEach(button => {
324
+ button.removeAttribute('data-cms-disabled')
325
+ button.removeEventListener('click', preventInteraction, true)
326
+ })
327
+
328
+ // Re-enable forms
329
+ const forms = document.querySelectorAll('form[data-cms-disabled]')
330
+ forms.forEach(form => {
331
+ form.removeAttribute('data-cms-disabled')
332
+ form.removeEventListener('submit', preventInteraction, true)
333
+ })
334
+ }
335
+
336
+ /**
337
+ * @deprecated Use disableAllInteractiveElements instead
338
+ */
339
+ export function disableAllLinks(): void {
340
+ disableAllInteractiveElements()
341
+ }
342
+
343
+ /**
344
+ * @deprecated Use enableAllInteractiveElements instead
345
+ */
346
+ export function enableAllLinks(): void {
347
+ enableAllInteractiveElements()
348
+ }
349
+
350
+ function preventInteraction(event: Event): void {
351
+ const target = event.currentTarget as HTMLElement
352
+ if (target.hasAttribute('data-cms-disabled')) {
353
+ event.preventDefault()
354
+ event.stopPropagation()
355
+ event.stopImmediatePropagation()
356
+ }
357
+ }