@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,284 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
2
+ import { CSS, LAYOUT, TIMING } from '../constants'
3
+ import { getCmsElementAtPosition, getComponentAtPosition, isNearElementEdge } from '../dom'
4
+ import { getComponentInstance } from '../manifest'
5
+ import * as signals from '../signals'
6
+ import { isEventOnCmsUI, usePositionTracking } from './utils'
7
+
8
+ export interface OutlineState {
9
+ visible: boolean
10
+ rect: DOMRect | null
11
+ isComponent: boolean
12
+ componentName: string | undefined
13
+ tagName: string | undefined
14
+ element: HTMLElement | null
15
+ /** CMS ID of the detected element */
16
+ cmsId: string | null
17
+ }
18
+
19
+ const INITIAL_STATE: OutlineState = {
20
+ visible: false,
21
+ rect: null,
22
+ isComponent: false,
23
+ componentName: undefined,
24
+ tagName: undefined,
25
+ element: null,
26
+ cmsId: null,
27
+ }
28
+
29
+ /**
30
+ * Hook for detecting and tracking hovered CMS elements.
31
+ * Uses signals directly for state management.
32
+ */
33
+ export function useElementDetection(): OutlineState {
34
+ const [outlineState, setOutlineState] = useState<OutlineState>(INITIAL_STATE)
35
+
36
+ // Throttle ref for element detection
37
+ const lastDetectionTime = useRef<number>(0)
38
+ // Timeout for delayed hide (allows reaching color swatches)
39
+ const hideTimeoutRef = useRef<number | null>(null)
40
+
41
+ // Handle position updates on scroll/resize
42
+ const handlePositionChange = useCallback((rect: DOMRect | null) => {
43
+ if (rect) {
44
+ setOutlineState(prev => ({ ...prev, rect }))
45
+ } else {
46
+ setOutlineState(INITIAL_STATE)
47
+ }
48
+ }, [])
49
+
50
+ // Track element position on scroll/resize
51
+ usePositionTracking(
52
+ outlineState.element,
53
+ handlePositionChange,
54
+ outlineState.visible,
55
+ )
56
+
57
+ // Setup hover highlight for both elements and components
58
+ useEffect(() => {
59
+ const handleMouseMove = (ev: MouseEvent) => {
60
+ const isEditing = signals.isEditing.value
61
+ const chatOpen = signals.isChatOpen.value
62
+
63
+ if (!isEditing && !chatOpen) {
64
+ if (hideTimeoutRef.current) {
65
+ clearTimeout(hideTimeoutRef.current)
66
+ hideTimeoutRef.current = null
67
+ }
68
+ setOutlineState(prev => prev.visible ? INITIAL_STATE : prev)
69
+ return
70
+ }
71
+
72
+ // Check if hovering over CMS UI, swatches, or attribute button - keep current state
73
+ if (isEventOnCmsUI(ev) || signals.isHoveringOutlineUI.value) {
74
+ // Cancel any pending hide since we're on UI elements
75
+ if (hideTimeoutRef.current) {
76
+ clearTimeout(hideTimeoutRef.current)
77
+ hideTimeoutRef.current = null
78
+ }
79
+ return
80
+ }
81
+
82
+ // Throttle detection for performance
83
+ const now = Date.now()
84
+ if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
85
+ return
86
+ }
87
+ lastDetectionTime.current = now
88
+
89
+ const manifest = signals.manifest.value
90
+ const entries = manifest.entries
91
+
92
+ // When chat is open, only detect components (not text/image elements)
93
+ if (chatOpen) {
94
+ const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
95
+ if (componentEl) {
96
+ if (hideTimeoutRef.current) {
97
+ clearTimeout(hideTimeoutRef.current)
98
+ hideTimeoutRef.current = null
99
+ }
100
+ const rect = componentEl.getBoundingClientRect()
101
+ const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
102
+ const instance = componentId ? getComponentInstance(manifest, componentId) : null
103
+
104
+ setOutlineState({
105
+ visible: true,
106
+ rect,
107
+ isComponent: true,
108
+ componentName: instance?.componentName,
109
+ tagName: componentEl.tagName.toLowerCase(),
110
+ element: componentEl,
111
+ cmsId: null,
112
+ })
113
+ return
114
+ }
115
+
116
+ setOutlineState(INITIAL_STATE)
117
+ return
118
+ }
119
+
120
+ // Use the improved elementsFromPoint-based detection
121
+ const cmsEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
122
+
123
+ if (cmsEl && !cmsEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
124
+ // Found a text-editable element - cancel any pending hide
125
+ if (hideTimeoutRef.current) {
126
+ clearTimeout(hideTimeoutRef.current)
127
+ hideTimeoutRef.current = null
128
+ }
129
+ const rect = cmsEl.getBoundingClientRect()
130
+ const cmsId = cmsEl.getAttribute(CSS.ID_ATTRIBUTE)
131
+ setOutlineState({
132
+ visible: true,
133
+ rect,
134
+ isComponent: false,
135
+ componentName: undefined,
136
+ tagName: undefined,
137
+ element: cmsEl,
138
+ cmsId,
139
+ })
140
+ return
141
+ }
142
+
143
+ // Check for component at position
144
+ const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
145
+ if (componentEl) {
146
+ const rect = componentEl.getBoundingClientRect()
147
+ const nearEdge = isNearElementEdge(
148
+ ev.clientX,
149
+ ev.clientY,
150
+ rect,
151
+ LAYOUT.COMPONENT_EDGE_THRESHOLD,
152
+ )
153
+
154
+ if (ev.altKey || nearEdge) {
155
+ // Cancel any pending hide
156
+ if (hideTimeoutRef.current) {
157
+ clearTimeout(hideTimeoutRef.current)
158
+ hideTimeoutRef.current = null
159
+ }
160
+ const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
161
+ const instance = componentId ? getComponentInstance(manifest, componentId) : null
162
+
163
+ setOutlineState({
164
+ visible: true,
165
+ rect,
166
+ isComponent: true,
167
+ componentName: instance?.componentName,
168
+ tagName: componentEl.tagName.toLowerCase(),
169
+ element: componentEl,
170
+ cmsId: null,
171
+ })
172
+ return
173
+ }
174
+ }
175
+
176
+ // Check if current outline has color swatches or attribute button - if so, delay hide
177
+ setOutlineState(prev => {
178
+ if (prev.visible && prev.cmsId) {
179
+ const entry = manifest.entries[prev.cmsId]
180
+ const hasColorClasses = entry?.colorClasses?.bg?.value || entry?.colorClasses?.text?.value
181
+ const hasEditableAttributes = entry?.attributes && Object.keys(entry.attributes).length > 0
182
+
183
+ if ((hasColorClasses || hasEditableAttributes) && !hideTimeoutRef.current) {
184
+ // Schedule delayed hide to allow reaching swatches/attribute button
185
+ hideTimeoutRef.current = window.setTimeout(() => {
186
+ hideTimeoutRef.current = null
187
+ // Only hide if still not hovering over any outline UI
188
+ if (!signals.isHoveringOutlineUI.value) {
189
+ setOutlineState(INITIAL_STATE)
190
+ }
191
+ }, 400) // Delay to allow reaching buttons below element
192
+ return prev // Keep visible for now
193
+ }
194
+ }
195
+ return INITIAL_STATE
196
+ })
197
+ }
198
+
199
+ document.addEventListener('mousemove', handleMouseMove, true)
200
+ return () => {
201
+ document.removeEventListener('mousemove', handleMouseMove, true)
202
+ if (hideTimeoutRef.current) {
203
+ clearTimeout(hideTimeoutRef.current)
204
+ }
205
+ }
206
+ }, [])
207
+
208
+ return outlineState
209
+ }
210
+
211
+ export interface ComponentClickHandlerOptions {
212
+ onComponentSelect: (componentId: string, rect: DOMRect) => void
213
+ }
214
+
215
+ /**
216
+ * Hook for handling component click selection.
217
+ * Uses signals directly for state management.
218
+ */
219
+ export function useComponentClickHandler({
220
+ onComponentSelect,
221
+ }: ComponentClickHandlerOptions): void {
222
+ useEffect(() => {
223
+ const handleClick = (ev: MouseEvent) => {
224
+ const isEditing = signals.isEditing.value
225
+ const chatOpen = signals.isChatOpen.value
226
+ if (!isEditing && !chatOpen) return
227
+
228
+ // Ignore clicks on CMS UI elements
229
+ if (isEventOnCmsUI(ev)) return
230
+
231
+ const manifest = signals.manifest.value
232
+ const entries = manifest.entries
233
+
234
+ if (chatOpen) {
235
+ // When chat is open, only select components
236
+ const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
237
+ if (componentEl) {
238
+ const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
239
+ if (componentId) {
240
+ ev.preventDefault()
241
+ ev.stopPropagation()
242
+ signals.setChatContextElement(componentId)
243
+ }
244
+ }
245
+ return
246
+ }
247
+
248
+ // Normal editing mode behavior
249
+ // Check for text element first
250
+ const textEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
251
+ if (textEl && !textEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
252
+ // Let the text element handle this click
253
+ return
254
+ }
255
+
256
+ // Check for component click
257
+ const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
258
+ if (componentEl) {
259
+ const rect = componentEl.getBoundingClientRect()
260
+ const nearEdge = isNearElementEdge(
261
+ ev.clientX,
262
+ ev.clientY,
263
+ rect,
264
+ LAYOUT.COMPONENT_EDGE_THRESHOLD,
265
+ )
266
+
267
+ if (ev.altKey || nearEdge) {
268
+ const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
269
+ if (componentId) {
270
+ ev.preventDefault()
271
+ ev.stopPropagation()
272
+ onComponentSelect(componentId, rect)
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ document.addEventListener('click', handleClick, true)
279
+ return () => document.removeEventListener('click', handleClick, true)
280
+ }, [onComponentSelect])
281
+ }
282
+
283
+ // Re-export utilities for backwards compatibility
284
+ export { isEventOnCmsUI }
@@ -0,0 +1,102 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
2
+ import { CSS, TIMING } from '../constants'
3
+ import * as signals from '../signals'
4
+ import { isEventOnCmsUI, usePositionTracking } from './utils'
5
+
6
+ export interface ImageHoverState {
7
+ visible: boolean
8
+ rect: DOMRect | null
9
+ element: HTMLImageElement | null
10
+ cmsId: string | null
11
+ }
12
+
13
+ const INITIAL_STATE: ImageHoverState = {
14
+ visible: false,
15
+ rect: null,
16
+ element: null,
17
+ cmsId: null,
18
+ }
19
+
20
+ /**
21
+ * Hook for detecting and tracking hovered CMS image elements.
22
+ * Shows a visual overlay when hovering over images marked with data-cms-img.
23
+ */
24
+ export function useImageHoverDetection(): ImageHoverState {
25
+ const [imageHoverState, setImageHoverState] = useState<ImageHoverState>(INITIAL_STATE)
26
+
27
+ // Throttle ref for element detection
28
+ const lastDetectionTime = useRef<number>(0)
29
+
30
+ // Handle position updates on scroll/resize
31
+ const handlePositionChange = useCallback((rect: DOMRect | null) => {
32
+ if (rect) {
33
+ setImageHoverState(prev => ({ ...prev, rect }))
34
+ } else {
35
+ setImageHoverState(INITIAL_STATE)
36
+ }
37
+ }, [])
38
+
39
+ // Track element position on scroll/resize
40
+ usePositionTracking(
41
+ imageHoverState.element,
42
+ handlePositionChange,
43
+ imageHoverState.visible,
44
+ )
45
+
46
+ // Setup hover detection for image elements
47
+ useEffect(() => {
48
+ const handleMouseMove = (ev: MouseEvent) => {
49
+ const isEditing = signals.isEditing.value
50
+
51
+ if (!isEditing) {
52
+ setImageHoverState(prev => prev.visible ? INITIAL_STATE : prev)
53
+ return
54
+ }
55
+
56
+ // Check if hovering over CMS UI - keep current state
57
+ if (isEventOnCmsUI(ev)) {
58
+ return
59
+ }
60
+
61
+ // Throttle detection for performance
62
+ const now = Date.now()
63
+ if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
64
+ return
65
+ }
66
+ lastDetectionTime.current = now
67
+
68
+ // Check if hovering over an image element with data-cms-img attribute
69
+ const elements = document.elementsFromPoint(ev.clientX, ev.clientY)
70
+
71
+ for (const el of elements) {
72
+ // If there's a contentEditable element above the image, don't show overlay
73
+ // This allows text editing on elements positioned over images
74
+ if (el instanceof HTMLElement && el.contentEditable === 'true') {
75
+ setImageHoverState(INITIAL_STATE)
76
+ return
77
+ }
78
+
79
+ if (el instanceof HTMLImageElement && el.hasAttribute('data-cms-img')) {
80
+ const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
81
+ const rect = el.getBoundingClientRect()
82
+
83
+ setImageHoverState({
84
+ visible: true,
85
+ rect,
86
+ element: el,
87
+ cmsId,
88
+ })
89
+ return
90
+ }
91
+ }
92
+
93
+ // No image found, hide overlay
94
+ setImageHoverState(INITIAL_STATE)
95
+ }
96
+
97
+ document.addEventListener('mousemove', handleMouseMove, true)
98
+ return () => document.removeEventListener('mousemove', handleMouseMove, true)
99
+ }, [])
100
+
101
+ return imageHoverState
102
+ }
@@ -0,0 +1,187 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
2
+ import { CSS, TIMING } from '../constants'
3
+ import * as signals from '../signals'
4
+ import { getTextSelection, type TextSelection } from '../text-styling'
5
+ import { isElementInCmsUI, usePositionTracking } from './utils'
6
+
7
+ export interface TextSelectionState {
8
+ hasSelection: boolean
9
+ selection: TextSelection | null
10
+ rect: DOMRect | null
11
+ element: HTMLElement | null
12
+ }
13
+
14
+ const INITIAL_STATE: TextSelectionState = {
15
+ hasSelection: false,
16
+ selection: null,
17
+ rect: null,
18
+ element: null,
19
+ }
20
+
21
+ /**
22
+ * Hook for managing text selection state within CMS elements.
23
+ * Tracks when the user has selected text and provides the selection details
24
+ * for the text styling toolbar.
25
+ */
26
+ export function useTextSelection(): TextSelectionState {
27
+ const [state, setState] = useState<TextSelectionState>(INITIAL_STATE)
28
+
29
+ // Track the last active element to detect if user clicked on CMS UI
30
+ const lastActiveElementRef = useRef<Element | null>(null)
31
+
32
+ // Update rect on scroll - using a simpler approach since selection rect
33
+ // needs to be recalculated from the Selection API, not tracked element
34
+ useEffect(() => {
35
+ if (!state.hasSelection) return
36
+
37
+ const updateRect = () => {
38
+ const selection = window.getSelection()
39
+ if (selection && selection.rangeCount > 0) {
40
+ const range = selection.getRangeAt(0)
41
+ const rect = range.getBoundingClientRect()
42
+ setState(prev => ({ ...prev, rect }))
43
+ }
44
+ }
45
+
46
+ window.addEventListener('scroll', updateRect, true)
47
+ window.addEventListener('resize', updateRect)
48
+
49
+ return () => {
50
+ window.removeEventListener('scroll', updateRect, true)
51
+ window.removeEventListener('resize', updateRect)
52
+ }
53
+ }, [state.hasSelection])
54
+
55
+ useEffect(() => {
56
+ const processSelection = () => {
57
+ const isEditing = signals.isEditing.value
58
+ if (!isEditing) {
59
+ setState(INITIAL_STATE)
60
+ return
61
+ }
62
+
63
+ const selection = window.getSelection()
64
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
65
+ // Check if focus is on CMS UI element - if so, preserve the selection state
66
+ // This allows clicking toolbar buttons without losing selection
67
+ const activeElement = document.activeElement
68
+ if (activeElement && isElementInCmsUI(activeElement as HTMLElement)) {
69
+ return
70
+ }
71
+ setState(INITIAL_STATE)
72
+ return
73
+ }
74
+
75
+ const range = selection.getRangeAt(0)
76
+ const container = range.commonAncestorContainer
77
+
78
+ // Find the CMS element that contains the selection
79
+ let element: HTMLElement | null = container.nodeType === Node.TEXT_NODE
80
+ ? container.parentElement
81
+ : container as HTMLElement
82
+
83
+ let cmsElement: HTMLElement | null = null
84
+ while (element && element !== document.body) {
85
+ if (element.hasAttribute(CSS.ID_ATTRIBUTE) && element.contentEditable === 'true') {
86
+ cmsElement = element
87
+ break
88
+ }
89
+ element = element.parentElement
90
+ }
91
+
92
+ if (!cmsElement) {
93
+ setState(INITIAL_STATE)
94
+ return
95
+ }
96
+
97
+ const textSelection = getTextSelection(cmsElement)
98
+ if (!textSelection) {
99
+ setState(INITIAL_STATE)
100
+ return
101
+ }
102
+
103
+ // Get the bounding rect of the selection
104
+ const rect = range.getBoundingClientRect()
105
+
106
+ setState({
107
+ hasSelection: true,
108
+ selection: textSelection,
109
+ rect,
110
+ element: cmsElement,
111
+ })
112
+ }
113
+
114
+ // Debounce selection change handling
115
+ let timeoutId: number | null = null
116
+ const debouncedHandler = () => {
117
+ if (timeoutId) {
118
+ clearTimeout(timeoutId)
119
+ }
120
+ timeoutId = window.setTimeout(processSelection, TIMING.BLUR_DELAY_MS)
121
+ }
122
+
123
+ // Handle mousedown to track where user clicked
124
+ const handleMouseDown = (e: MouseEvent) => {
125
+ lastActiveElementRef.current = e.target as Element
126
+ }
127
+
128
+ // Handle mouseup for immediate feedback after selection
129
+ const handleMouseUp = (e: MouseEvent) => {
130
+ const target = e.target as HTMLElement
131
+
132
+ // Don't process if clicking on CMS UI - let the selection persist
133
+ if (isElementInCmsUI(target)) {
134
+ return
135
+ }
136
+
137
+ // Process selection after a brief delay for the browser to update selection
138
+ window.setTimeout(processSelection, TIMING.BLUR_DELAY_MS)
139
+ }
140
+
141
+ document.addEventListener('selectionchange', debouncedHandler)
142
+ document.addEventListener('mousedown', handleMouseDown, true)
143
+ document.addEventListener('mouseup', handleMouseUp)
144
+
145
+ return () => {
146
+ document.removeEventListener('selectionchange', debouncedHandler)
147
+ document.removeEventListener('mousedown', handleMouseDown, true)
148
+ document.removeEventListener('mouseup', handleMouseUp)
149
+ if (timeoutId) {
150
+ clearTimeout(timeoutId)
151
+ }
152
+ }
153
+ }, [])
154
+
155
+ return state
156
+ }
157
+
158
+ /**
159
+ * Clear the current text selection
160
+ */
161
+ export function clearTextSelection(): void {
162
+ const selection = window.getSelection()
163
+ if (selection) {
164
+ selection.removeAllRanges()
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Save and restore selection - useful when applying styles
170
+ */
171
+ export function saveSelection(): Range | null {
172
+ const selection = window.getSelection()
173
+ if (selection && selection.rangeCount > 0) {
174
+ return selection.getRangeAt(0).cloneRange()
175
+ }
176
+ return null
177
+ }
178
+
179
+ export function restoreSelection(range: Range | null): void {
180
+ if (!range) return
181
+
182
+ const selection = window.getSelection()
183
+ if (selection) {
184
+ selection.removeAllRanges()
185
+ selection.addRange(range)
186
+ }
187
+ }
@@ -0,0 +1,126 @@
1
+ import { useCallback, useEffect, useState } from 'preact/hooks'
2
+ import { CSS } from '../constants'
3
+ import * as signals from '../signals'
4
+
5
+ export interface TooltipState {
6
+ elementId: string | null
7
+ rect: DOMRect | null
8
+ element: HTMLElement | null
9
+ }
10
+
11
+ export interface UseTooltipStateOptions {
12
+ /** @deprecated No longer needed - signals are used directly */
13
+ isEditing?: boolean
14
+ }
15
+
16
+ /**
17
+ * Hook for managing tooltip visibility and positioning.
18
+ * Uses signals directly for state management.
19
+ */
20
+ export function useTooltipState(_options?: UseTooltipStateOptions) {
21
+ const [tooltipState, setTooltipState] = useState<TooltipState>({
22
+ elementId: null,
23
+ rect: null,
24
+ element: null,
25
+ })
26
+
27
+ /**
28
+ * Show tooltip for the current editing element
29
+ */
30
+ const showTooltipForElement = useCallback(() => {
31
+ const currentEditingId = signals.currentEditingId.value
32
+ const isProcessing = signals.isAIProcessing.value
33
+
34
+ if (!currentEditingId || isProcessing) {
35
+ setTooltipState({ elementId: null, rect: null, element: null })
36
+ return
37
+ }
38
+
39
+ const change = signals.getPendingChange(currentEditingId)
40
+ if (!change) {
41
+ setTooltipState({ elementId: null, rect: null, element: null })
42
+ return
43
+ }
44
+
45
+ setTooltipState({
46
+ elementId: currentEditingId,
47
+ rect: change.element.getBoundingClientRect(),
48
+ element: change.element,
49
+ })
50
+ }, [])
51
+
52
+ /**
53
+ * Hide the tooltip
54
+ */
55
+ const hideTooltip = useCallback(() => {
56
+ setTooltipState({ elementId: null, rect: null, element: null })
57
+ }, [])
58
+
59
+ // Update tooltip position on scroll
60
+ useEffect(() => {
61
+ if (!tooltipState.elementId || !tooltipState.element) return
62
+
63
+ const updateTooltipPosition = () => {
64
+ if (tooltipState.element && document.contains(tooltipState.element)) {
65
+ setTooltipState(prev => ({
66
+ ...prev,
67
+ rect: tooltipState.element!.getBoundingClientRect(),
68
+ }))
69
+ } else {
70
+ // Element no longer in DOM
71
+ setTooltipState({ elementId: null, rect: null, element: null })
72
+ }
73
+ }
74
+
75
+ // Hide tooltip when clicking outside the element and CMS UI
76
+ const handleClickOutside = (e: MouseEvent) => {
77
+ const path = e.composedPath()
78
+ const target = path[0] as HTMLElement
79
+
80
+ // Check if click is on the tooltip element itself
81
+ if (tooltipState.element?.contains(target) || tooltipState.element === target) {
82
+ return
83
+ }
84
+
85
+ // Check if any element in the path is inside CMS UI
86
+ const cmsOverlay = document.querySelector(CSS.HIGHLIGHT_ELEMENT)
87
+ for (const el of path) {
88
+ if (el === cmsOverlay) {
89
+ return // Click was inside Shadow DOM
90
+ }
91
+ if (el instanceof HTMLElement) {
92
+ if (el.tagName?.startsWith('CMS-')) {
93
+ return
94
+ }
95
+ if (el.hasAttribute?.(CSS.UI_ATTRIBUTE)) {
96
+ return
97
+ }
98
+ }
99
+ }
100
+
101
+ // Check if click is on another CMS-editable element
102
+ if (target.hasAttribute?.(CSS.ID_ATTRIBUTE)) {
103
+ return // Will be handled by element focus
104
+ }
105
+
106
+ // Click was outside, hide tooltip
107
+ setTooltipState({ elementId: null, rect: null, element: null })
108
+ }
109
+
110
+ window.addEventListener('scroll', updateTooltipPosition, true)
111
+ window.addEventListener('resize', updateTooltipPosition)
112
+ document.addEventListener('mousedown', handleClickOutside, true)
113
+
114
+ return () => {
115
+ window.removeEventListener('scroll', updateTooltipPosition, true)
116
+ window.removeEventListener('resize', updateTooltipPosition)
117
+ document.removeEventListener('mousedown', handleClickOutside, true)
118
+ }
119
+ }, [tooltipState.elementId, tooltipState.element])
120
+
121
+ return {
122
+ tooltipState,
123
+ showTooltipForElement,
124
+ hideTooltip,
125
+ }
126
+ }