@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,101 @@
1
+ import type { RefObject } from 'preact'
2
+ import { useEffect, useRef } from 'preact/hooks'
3
+ import { CSS } from '../constants'
4
+
5
+ /**
6
+ * Check if a mouse event originates from CMS UI elements.
7
+ * This includes the toolbar, AI chat, color picker, and other CMS overlays.
8
+ */
9
+ export function isEventOnCmsUI(ev: MouseEvent): boolean {
10
+ const path = ev.composedPath()
11
+ const cmsOverlay = document.querySelector(CSS.HIGHLIGHT_ELEMENT)
12
+
13
+ for (const el of path) {
14
+ if (el === cmsOverlay) return true
15
+ if (el instanceof HTMLElement) {
16
+ // Check for CMS custom elements
17
+ if (el.tagName?.startsWith('CMS-')) return true
18
+ // Check for toolbar and AI chat by data attribute
19
+ if (el.hasAttribute?.(CSS.UI_ATTRIBUTE)) return true
20
+ }
21
+ }
22
+ return false
23
+ }
24
+
25
+ /**
26
+ * Check if a target element is part of CMS UI.
27
+ * Non-event based variant for direct element checks.
28
+ */
29
+ export function isElementInCmsUI(target: HTMLElement | null): boolean {
30
+ if (!target) return false
31
+ return target.hasAttribute?.(CSS.UI_ATTRIBUTE) || !!target.closest?.(`[${CSS.UI_ATTRIBUTE}]`)
32
+ }
33
+
34
+ /**
35
+ * Hook for tracking an element's position on scroll and resize.
36
+ * Returns a callback to get the latest rect, and automatically updates
37
+ * when the element scrolls or the window resizes.
38
+ *
39
+ * @param element - The element to track
40
+ * @param onPositionChange - Callback when position changes
41
+ * @param enabled - Whether tracking is enabled
42
+ */
43
+ export function usePositionTracking(
44
+ element: HTMLElement | null,
45
+ onPositionChange: (rect: DOMRect | null) => void,
46
+ enabled: boolean = true,
47
+ ): void {
48
+ const elementRef = useRef(element)
49
+ elementRef.current = element
50
+
51
+ useEffect(() => {
52
+ if (!enabled || !element) return
53
+
54
+ const updatePosition = () => {
55
+ if (elementRef.current && document.contains(elementRef.current)) {
56
+ onPositionChange(elementRef.current.getBoundingClientRect())
57
+ } else {
58
+ onPositionChange(null)
59
+ }
60
+ }
61
+
62
+ window.addEventListener('scroll', updatePosition, true)
63
+ window.addEventListener('resize', updatePosition)
64
+
65
+ return () => {
66
+ window.removeEventListener('scroll', updatePosition, true)
67
+ window.removeEventListener('resize', updatePosition)
68
+ }
69
+ }, [element, enabled, onPositionChange])
70
+ }
71
+
72
+ /**
73
+ * Hook for throttling a callback function.
74
+ * Returns a throttled version that will only execute once per interval.
75
+ */
76
+ export function useThrottle<T extends (...args: unknown[]) => void>(
77
+ callback: T,
78
+ intervalMs: number,
79
+ ): T {
80
+ const lastCallTime = useRef<number>(0)
81
+ const callbackRef = useRef(callback)
82
+ callbackRef.current = callback
83
+
84
+ return ((...args: Parameters<T>) => {
85
+ const now = Date.now()
86
+ if (now - lastCallTime.current >= intervalMs) {
87
+ lastCallTime.current = now
88
+ callbackRef.current(...args)
89
+ }
90
+ }) as T
91
+ }
92
+
93
+ /**
94
+ * Creates a ref that always contains the latest value.
95
+ * Useful for accessing current values in event handlers without stale closures.
96
+ */
97
+ export function useLatestRef<T>(value: T): RefObject<T> {
98
+ const ref = useRef(value)
99
+ ref.current = value
100
+ return ref
101
+ }
@@ -0,0 +1,481 @@
1
+ import { render } from 'preact'
2
+ import { useCallback, useEffect } from 'preact/hooks'
3
+ import { fetchManifest } from './api'
4
+ import { AIChat } from './components/ai-chat'
5
+ import { AITooltip } from './components/ai-tooltip'
6
+ import { AttributeEditor } from './components/attribute-editor'
7
+ import { BlockEditor } from './components/block-editor'
8
+ import { CollectionsBrowser } from './components/collections-browser'
9
+ import { ColorToolbar } from './components/color-toolbar'
10
+ import { ConfirmDialog } from './components/confirm-dialog'
11
+ import { CreatePageModal } from './components/create-page-modal'
12
+ import { EditableHighlights } from './components/editable-highlights'
13
+ import { ErrorBoundary } from './components/error-boundary'
14
+ import { ImageOverlay } from './components/image-overlay'
15
+ import { MarkdownEditorOverlay } from './components/markdown-editor-overlay'
16
+ import { MediaLibrary } from './components/media-library'
17
+ import { Outline } from './components/outline'
18
+ import { RedirectCountdown } from './components/redirect-countdown'
19
+ import { SeoEditor } from './components/seo-editor'
20
+ import { TextStyleToolbar } from './components/text-style-toolbar'
21
+ import { ToastContainer } from './components/toast/toast-container'
22
+ import { Toolbar } from './components/toolbar'
23
+ import { getConfig } from './config'
24
+ import { logDebug } from './dom'
25
+ import {
26
+ discardAllChanges,
27
+ dismissDeploymentStatus,
28
+ handleColorChange,
29
+ saveAllChanges,
30
+ startEditMode,
31
+ stopEditMode,
32
+ toggleShowOriginal,
33
+ } from './editor'
34
+ import { performRedo, performUndo } from './history'
35
+ import CMS_STYLES from './styles.css?inline'
36
+ import {
37
+ useAIHandlers,
38
+ useBlockEditorHandlers,
39
+ useComponentClickHandler,
40
+ useElementDetection,
41
+ useImageHoverDetection,
42
+ useTextSelection,
43
+ useTooltipState,
44
+ } from './hooks'
45
+ import {
46
+ openCollectionsBrowser,
47
+ openMarkdownEditorForCurrentPage,
48
+ openSeoEditor,
49
+ selectBrowserCollection,
50
+ setMediaLibraryOpen,
51
+ toggleShowEditableHighlights,
52
+ updateSettings,
53
+ } from './signals'
54
+ import * as signals from './signals'
55
+ import { hasPendingEntryNavigation, loadSettingsFromStorage, saveSettingsToStorage } from './storage'
56
+ import { generateCSSVariables, resolveTheme } from './themes'
57
+
58
+ const CmsUI = () => {
59
+ const config = signals.config.value
60
+ const outlineState = useElementDetection()
61
+ const imageHoverState = useImageHoverDetection()
62
+ const textSelectionState = useTextSelection()
63
+ const { tooltipState, showTooltipForElement, hideTooltip } = useTooltipState()
64
+ const updateUI = useCallback(() => {
65
+ showTooltipForElement()
66
+ }, [showTooltipForElement])
67
+
68
+ // Load settings from localStorage on mount
69
+ useEffect(() => {
70
+ const savedSettings = loadSettingsFromStorage()
71
+ if (savedSettings) {
72
+ updateSettings(savedSettings)
73
+ }
74
+ }, [])
75
+
76
+ // Fetch manifest on mount so toolbar has collection/SEO data before edit mode
77
+ useEffect(() => {
78
+ fetchManifest().then((manifest) => {
79
+ signals.setManifest(manifest)
80
+ }).catch(() => {})
81
+ }, [])
82
+
83
+ // Auto-open markdown editor when there's a pending entry navigation from collections browser
84
+ useEffect(() => {
85
+ if (hasPendingEntryNavigation()) {
86
+ openMarkdownEditorForCurrentPage()
87
+ }
88
+ }, [])
89
+
90
+ const {
91
+ handleAIChatToggle,
92
+ handleChatClose,
93
+ handleChatCancel,
94
+ handleTooltipPromptSubmit,
95
+ handleChatSend,
96
+ handleApplyToElement,
97
+ } = useAIHandlers({
98
+ config,
99
+ showToast: signals.showToast,
100
+ onTooltipHide: hideTooltip,
101
+ onUIUpdate: updateUI,
102
+ })
103
+
104
+ const {
105
+ blockEditorRect,
106
+ handleComponentSelect,
107
+ handleBlockEditorClose,
108
+ handleUpdateProps,
109
+ handleInsertComponent,
110
+ handleRemoveBlock,
111
+ } = useBlockEditorHandlers({
112
+ config,
113
+ showToast: signals.showToast,
114
+ })
115
+
116
+ useComponentClickHandler({ onComponentSelect: handleComponentSelect })
117
+
118
+ // Editor control handlers
119
+ const handleEditToggle = useCallback(async () => {
120
+ if (signals.isEditing.value) {
121
+ hideTooltip()
122
+ stopEditMode(updateUI)
123
+ } else {
124
+ await startEditMode(config, updateUI)
125
+ }
126
+ }, [config, updateUI, hideTooltip])
127
+
128
+ const handleCompare = useCallback(() => {
129
+ toggleShowOriginal(config, updateUI)
130
+ }, [config, updateUI])
131
+
132
+ const handleSave = useCallback(async () => {
133
+ try {
134
+ const result = await saveAllChanges(config, updateUI)
135
+ if (result.success) {
136
+ signals.showToast(`Saved ${result.updated} change(s) successfully!`, 'success')
137
+ } else if (result.errors) {
138
+ signals.showToast(`Saved ${result.updated}, ${result.errors.length} failed`, 'error')
139
+ }
140
+ } catch (err) {
141
+ signals.showToast('Save failed – see console', 'error')
142
+ }
143
+ }, [config, updateUI])
144
+
145
+ const handleDiscard = useCallback(() => {
146
+ discardAllChanges(updateUI)
147
+ signals.showToast('All changes discarded', 'info')
148
+ }, [updateUI])
149
+
150
+ const handleOpenCollection = useCallback((name: string) => {
151
+ openCollectionsBrowser()
152
+ selectBrowserCollection(name)
153
+ }, [])
154
+
155
+ const handleMediaLibrary = useCallback(() => {
156
+ setMediaLibraryOpen(true)
157
+ }, [])
158
+
159
+ const handleDismissDeployment = useCallback(() => {
160
+ dismissDeploymentStatus()
161
+ }, [])
162
+
163
+ const handleEditContent = useCallback(async () => {
164
+ if (!await openMarkdownEditorForCurrentPage()) {
165
+ signals.showToast('No collection content found on this page', 'error')
166
+ }
167
+ }, [])
168
+
169
+ const handleToggleHighlights = useCallback(() => {
170
+ toggleShowEditableHighlights()
171
+ // Save settings to localStorage
172
+ const newSettings = signals.settings.value
173
+ saveSettingsToStorage(newSettings)
174
+ }, [])
175
+
176
+ const handleSeoEditor = useCallback(() => {
177
+ openSeoEditor()
178
+ }, [])
179
+
180
+ // Color toolbar handlers
181
+ const handleColorToolbarChange = useCallback((colorType: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText', oldClass: string, newClass: string, previousClassName: string, previousStyleCssText: string) => {
182
+ const targetId = signals.colorEditorState.value.targetElementId
183
+ if (!targetId) return
184
+
185
+ handleColorChange(config, targetId, colorType, oldClass, newClass, updateUI, previousClassName, previousStyleCssText)
186
+ }, [config, updateUI])
187
+
188
+ const handleColorToolbarClose = useCallback(() => {
189
+ signals.closeColorEditor()
190
+ }, [])
191
+
192
+ // Handle color swatch click from outline
193
+ const handleOutlineColorClick = useCallback((cmsId: string, rect: DOMRect) => {
194
+ signals.setHoveringSwatches(false)
195
+ signals.openColorEditor(cmsId, rect)
196
+ }, [])
197
+
198
+ // Handle attribute button click from outline
199
+ const handleOutlineAttributeClick = useCallback((cmsId: string, rect: DOMRect) => {
200
+ signals.openAttributeEditor(cmsId, rect)
201
+ }, [])
202
+
203
+ // Handle attribute editor close
204
+ const handleAttributeEditorClose = useCallback(() => {
205
+ signals.closeAttributeEditor()
206
+ }, [])
207
+
208
+ // Get reactive values from signals
209
+ const isEditing = signals.isEditing.value
210
+ const isAIProcessing = signals.isAIProcessing.value
211
+ const isChatOpen = signals.isChatOpen.value
212
+ const blockEditorState = signals.blockEditorState.value
213
+ const colorEditorState = signals.colorEditorState.value
214
+ const manifest = signals.manifest.value
215
+ const toasts = signals.toasts.value
216
+ const collectionDefinitions = manifest.collectionDefinitions ?? {}
217
+ const showEditableHighlights = signals.showEditableHighlights.value
218
+ const hasSeoData = !!(manifest as any).seo
219
+
220
+ // Get color toolbar data
221
+ const colorEditorElement = colorEditorState.targetElementId
222
+ ? signals.pendingColorChanges.value.get(colorEditorState.targetElementId)?.element ?? null
223
+ : null
224
+ const colorEditorCurrentClasses = colorEditorState.targetElementId
225
+ ? signals.pendingColorChanges.value.get(colorEditorState.targetElementId)?.newClasses
226
+ : undefined
227
+
228
+ return (
229
+ <>
230
+ <ErrorBoundary componentName="Editable Highlights">
231
+ <EditableHighlights visible={showEditableHighlights && isEditing} />
232
+ </ErrorBoundary>
233
+
234
+ <ErrorBoundary componentName="Outline">
235
+ <Outline
236
+ visible={outlineState.visible}
237
+ rect={outlineState.rect}
238
+ isComponent={outlineState.isComponent}
239
+ componentName={outlineState.componentName}
240
+ tagName={outlineState.tagName}
241
+ element={outlineState.element}
242
+ cmsId={outlineState.cmsId}
243
+ onColorClick={handleOutlineColorClick}
244
+ onAttributeClick={handleOutlineAttributeClick}
245
+ />
246
+ </ErrorBoundary>
247
+
248
+ <ErrorBoundary componentName="ImageOverlay">
249
+ <ImageOverlay
250
+ visible={imageHoverState.visible && isEditing}
251
+ rect={imageHoverState.rect}
252
+ element={imageHoverState.element}
253
+ cmsId={imageHoverState.cmsId}
254
+ />
255
+ </ErrorBoundary>
256
+
257
+ <ErrorBoundary componentName="Toolbar">
258
+ <Toolbar
259
+ callbacks={{
260
+ onEdit: handleEditToggle,
261
+ onCompare: handleCompare,
262
+ onSave: handleSave,
263
+ onDiscard: handleDiscard,
264
+ onAIChat: handleAIChatToggle,
265
+ onMediaLibrary: handleMediaLibrary,
266
+ onDismissDeployment: handleDismissDeployment,
267
+ onNavigateChange: () => {
268
+ signals.navigateToNextChange()
269
+ },
270
+ onEditContent: handleEditContent,
271
+ onToggleHighlights: handleToggleHighlights,
272
+ onSeoEditor: hasSeoData ? handleSeoEditor : undefined,
273
+ onOpenCollection: handleOpenCollection,
274
+ }}
275
+ collectionDefinitions={Object.keys(collectionDefinitions).length > 0 ? collectionDefinitions : undefined}
276
+ />
277
+ </ErrorBoundary>
278
+
279
+ <ErrorBoundary componentName="AI Tooltip">
280
+ <AITooltip
281
+ callbacks={{
282
+ onPromptSubmit: handleTooltipPromptSubmit,
283
+ }}
284
+ visible={!!tooltipState.elementId && isEditing && !isAIProcessing && !textSelectionState.hasSelection}
285
+ elementId={tooltipState.elementId}
286
+ rect={tooltipState.rect}
287
+ processing={isAIProcessing}
288
+ />
289
+ </ErrorBoundary>
290
+
291
+ <ErrorBoundary componentName="Text Style Toolbar">
292
+ <TextStyleToolbar
293
+ visible={textSelectionState.hasSelection && isEditing && !isAIProcessing}
294
+ rect={textSelectionState.rect}
295
+ element={textSelectionState.element}
296
+ onStyleChange={updateUI}
297
+ />
298
+ </ErrorBoundary>
299
+
300
+ <ErrorBoundary componentName="Color Toolbar">
301
+ <ColorToolbar
302
+ visible={colorEditorState.isOpen && isEditing}
303
+ rect={colorEditorState.targetRect}
304
+ element={colorEditorElement}
305
+ availableColors={manifest.availableColors}
306
+ currentClasses={colorEditorCurrentClasses}
307
+ onColorChange={handleColorToolbarChange}
308
+ onClose={handleColorToolbarClose}
309
+ />
310
+ </ErrorBoundary>
311
+
312
+ <ErrorBoundary componentName="Attribute Editor">
313
+ <AttributeEditor
314
+ onClose={handleAttributeEditorClose}
315
+ />
316
+ </ErrorBoundary>
317
+
318
+ <ErrorBoundary componentName="AI Chat">
319
+ <AIChat
320
+ callbacks={{
321
+ onSend: handleChatSend,
322
+ onClose: handleChatClose,
323
+ onCancel: handleChatCancel,
324
+ onApplyToElement: handleApplyToElement,
325
+ }}
326
+ />
327
+ </ErrorBoundary>
328
+
329
+ <ErrorBoundary componentName="Block Editor">
330
+ <BlockEditor
331
+ visible={blockEditorState.isOpen && isEditing}
332
+ componentId={blockEditorState.currentComponentId}
333
+ rect={blockEditorRect}
334
+ onClose={handleBlockEditorClose}
335
+ onUpdateProps={handleUpdateProps}
336
+ onInsertComponent={handleInsertComponent}
337
+ onRemoveBlock={handleRemoveBlock}
338
+ />
339
+ </ErrorBoundary>
340
+
341
+ <ErrorBoundary componentName="SEO Editor">
342
+ <SeoEditor />
343
+ </ErrorBoundary>
344
+
345
+ <ErrorBoundary componentName="Collections Browser">
346
+ <CollectionsBrowser />
347
+ </ErrorBoundary>
348
+
349
+ <ErrorBoundary componentName="Create Page Modal">
350
+ <CreatePageModal />
351
+ </ErrorBoundary>
352
+
353
+ <ErrorBoundary componentName="Markdown Editor">
354
+ <MarkdownEditorOverlay />
355
+ </ErrorBoundary>
356
+
357
+ <ErrorBoundary componentName="Media Library">
358
+ <MediaLibrary />
359
+ </ErrorBoundary>
360
+
361
+ <ErrorBoundary componentName="Confirm Dialog">
362
+ <ConfirmDialog />
363
+ </ErrorBoundary>
364
+
365
+ <RedirectCountdown />
366
+ <ToastContainer toasts={toasts} onRemove={signals.removeToast} />
367
+ </>
368
+ )
369
+ }
370
+
371
+ class CmsEditor {
372
+ private appRoot: HTMLElement | null = null
373
+ private shadowRoot: ShadowRoot | null = null
374
+ private config = getConfig()
375
+
376
+ async init(): Promise<void> {
377
+ signals.setConfig(this.config)
378
+
379
+ logDebug(this.config.debug, 'Initializing CMS editor with config:', this.config)
380
+
381
+ this.setupUI()
382
+ this.setupKeyboardShortcuts()
383
+ }
384
+
385
+ private setupUI(): void {
386
+ const hostElement = document.createElement('div')
387
+ hostElement.id = 'cms-app-host'
388
+ hostElement.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647;'
389
+ document.body.appendChild(hostElement)
390
+
391
+ // Create shadow DOM with closed mode for better isolation
392
+ this.shadowRoot = hostElement.attachShadow({ mode: 'open' })
393
+
394
+ // Inject theme CSS variables BEFORE main styles
395
+ this.injectThemeStyles()
396
+
397
+ // Inject Tailwind CSS styles into shadow DOM
398
+ const styleElement = document.createElement('style')
399
+ styleElement.textContent = CMS_STYLES
400
+ this.shadowRoot.appendChild(styleElement)
401
+
402
+ // Create the app root container
403
+ this.appRoot = document.createElement('div')
404
+ this.appRoot.id = 'cms-app-root'
405
+ this.appRoot.className = 'cms-root'
406
+ this.shadowRoot.appendChild(this.appRoot)
407
+
408
+ // Render Preact app into the shadow DOM
409
+ render(<CmsUI />, this.appRoot)
410
+ }
411
+
412
+ private injectThemeStyles(): void {
413
+ if (!this.shadowRoot) return
414
+
415
+ const theme = resolveTheme(this.config)
416
+ const cssVars = generateCSSVariables(theme)
417
+
418
+ const styleEl = document.createElement('style')
419
+ styleEl.id = 'cms-theme-vars'
420
+ styleEl.textContent = `:host { ${cssVars} }`
421
+ this.shadowRoot.insertBefore(styleEl, this.shadowRoot.firstChild)
422
+ }
423
+
424
+ private setupKeyboardShortcuts(): void {
425
+ document.addEventListener('keydown', (ev) => {
426
+ // Cmd+Shift+E: Toggle CMS visibility
427
+ const combo = (ev.metaKey || ev.ctrlKey) && ev.shiftKey && ev.key.toLowerCase() === 'e'
428
+ if (combo) {
429
+ ev.preventDefault()
430
+ this.toggleVisibility()
431
+ return
432
+ }
433
+
434
+ // Undo/Redo only when editing
435
+ if (!signals.isEditing.value) return
436
+ if (!(ev.metaKey || ev.ctrlKey)) return
437
+
438
+ const key = ev.key.toLowerCase()
439
+
440
+ // Cmd+Shift+Z: Redo
441
+ if (key === 'z' && ev.shiftKey) {
442
+ ev.preventDefault()
443
+ performRedo()
444
+ return
445
+ }
446
+
447
+ // Cmd+Z: Undo
448
+ if (key === 'z' && !ev.shiftKey) {
449
+ ev.preventDefault()
450
+ performUndo()
451
+ return
452
+ }
453
+ })
454
+ }
455
+
456
+ private toggleVisibility(): void {
457
+ if (!this.appRoot) return
458
+
459
+ const isHidden = this.appRoot.style.display === 'none'
460
+ this.appRoot.style.display = isHidden ? 'block' : 'none'
461
+
462
+ // Use signals.showToast for consistency
463
+ signals.showToast(isHidden ? 'CMS editing enabled' : 'CMS editing disabled', 'info')
464
+ }
465
+ }
466
+
467
+ // Auto-initialize when DOM is ready
468
+ if (typeof window !== 'undefined') {
469
+ const initEditor = () => {
470
+ const editor = new CmsEditor()
471
+ editor.init()
472
+ }
473
+
474
+ if (document.readyState === 'loading') {
475
+ document.addEventListener('DOMContentLoaded', initEditor)
476
+ } else {
477
+ initEditor()
478
+ }
479
+ }
480
+
481
+ export { CmsEditor }
@@ -0,0 +1,4 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(...inputs))
@@ -0,0 +1,25 @@
1
+ import type { CmsManifest, ComponentDefinition, ComponentInstance, ManifestEntry } from './types'
2
+
3
+ type GetManifestEntry = (manifest: CmsManifest, id: string) => ManifestEntry | undefined
4
+ export const getManifestEntry: GetManifestEntry = (manifest, id) => manifest.entries[id]
5
+
6
+ type HasManifestEntry = (manifest: CmsManifest, id: string) => boolean
7
+ export const hasManifestEntry: HasManifestEntry = (manifest, id) => id in manifest.entries
8
+
9
+ type GetComponentInstances = (manifest: CmsManifest) => Record<string, ComponentInstance>
10
+ export const getComponentInstances: GetComponentInstances = manifest => manifest.components ?? {}
11
+
12
+ type GetComponentInstance = (manifest: CmsManifest, id: string) => ComponentInstance | undefined
13
+ export const getComponentInstance: GetComponentInstance = (manifest, id) => manifest.components?.[id]
14
+
15
+ type GetComponentDefinitions = (manifest: CmsManifest) => Record<string, ComponentDefinition>
16
+ export const getComponentDefinitions: GetComponentDefinitions = manifest => manifest.componentDefinitions ?? {}
17
+
18
+ type GetComponentDefinition = (manifest: CmsManifest, name: string) => ComponentDefinition | undefined
19
+ export const getComponentDefinition: GetComponentDefinition = (manifest, name) => getComponentDefinitions(manifest)[name]
20
+
21
+ type GetManifestEntryCount = (manifest: CmsManifest) => number
22
+ export const getManifestEntryCount: GetManifestEntryCount = (manifest: CmsManifest) => Object.keys(manifest.entries).length
23
+
24
+ type GetAvailableComponentNames = (manifest: CmsManifest) => string[]
25
+ export const getAvailableComponentNames: GetAvailableComponentNames = manifest => Object.keys(getComponentDefinitions(manifest))