@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,163 @@
1
+ import { useEffect, useMemo } from 'preact/hooks'
2
+ import { isCreatePageOpen, manifest, openMarkdownEditorForNewPage, resetCreatePageState } from '../signals'
3
+
4
+ export function CreatePageModal() {
5
+ const visible = isCreatePageOpen.value
6
+
7
+ // Get collection definitions from manifest (read signal directly for reactivity)
8
+ const collectionDefinitions = manifest.value.collectionDefinitions ?? {}
9
+
10
+ const collections = useMemo(() => {
11
+ return Object.values(collectionDefinitions)
12
+ }, [collectionDefinitions])
13
+
14
+ // Single collection — skip picker and go straight to editor
15
+ useEffect(() => {
16
+ if (visible && collections.length === 1) {
17
+ const col = collections[0]
18
+ resetCreatePageState()
19
+ if (col) {
20
+ openMarkdownEditorForNewPage(col?.name, col)
21
+ }
22
+ }
23
+ }, [visible, collections])
24
+
25
+ const handleClose = () => {
26
+ resetCreatePageState()
27
+ }
28
+
29
+ const handleSelectCollection = (name: string) => {
30
+ const def = collectionDefinitions[name]
31
+ if (def) {
32
+ resetCreatePageState()
33
+ openMarkdownEditorForNewPage(name, def)
34
+ }
35
+ }
36
+
37
+ if (!visible) return null
38
+
39
+ // No collections available
40
+ if (collections.length === 0) {
41
+ return (
42
+ <div
43
+ class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
44
+ onClick={handleClose}
45
+ data-cms-ui
46
+ >
47
+ <div
48
+ class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-md w-full border border-white/10"
49
+ onClick={(e) => e.stopPropagation()}
50
+ data-cms-ui
51
+ >
52
+ <div class="flex items-center justify-between p-5 border-b border-white/10">
53
+ <h2 class="text-lg font-semibold text-white">Create New Page</h2>
54
+ <CloseButton onClick={handleClose} />
55
+ </div>
56
+ <div class="p-8 text-center">
57
+ <div class="text-white/60 mb-4">
58
+ No content collections found.
59
+ </div>
60
+ <p class="text-white/40 text-sm">
61
+ Add markdown files to <code class="bg-white/10 px-1.5 py-0.5 rounded">src/content/</code> subdirectories to enable page creation.
62
+ </p>
63
+ </div>
64
+ <div class="flex items-center justify-end p-5 border-t border-white/10 bg-white/5 rounded-b-cms-xl">
65
+ <button
66
+ type="button"
67
+ onClick={handleClose}
68
+ class="px-4 py-2.5 text-sm text-white/80 font-medium rounded-cms-pill hover:bg-white/10 hover:text-white transition-colors"
69
+ data-cms-ui
70
+ >
71
+ Close
72
+ </button>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ // Single collection auto-selected via useEffect above
80
+ if (collections.length === 1) return null
81
+
82
+ // Collection picker
83
+ return (
84
+ <div
85
+ class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
86
+ onClick={handleClose}
87
+ data-cms-ui
88
+ >
89
+ <div
90
+ class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-md w-full border border-white/10"
91
+ onClick={(e) => e.stopPropagation()}
92
+ data-cms-ui
93
+ >
94
+ <div class="flex items-center justify-between p-5 border-b border-white/10">
95
+ <h2 class="text-lg font-semibold text-white">Choose Collection</h2>
96
+ <CloseButton onClick={handleClose} />
97
+ </div>
98
+ <div class="p-5 space-y-2">
99
+ {collections.map((col) => (
100
+ <button
101
+ key={col.name}
102
+ type="button"
103
+ onClick={() => handleSelectCollection(col.name)}
104
+ class="w-full flex items-center gap-4 p-4 bg-white/5 hover:bg-white/10 rounded-cms-lg border border-white/10 hover:border-white/20 transition-colors text-left"
105
+ data-cms-ui
106
+ >
107
+ <div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-md flex items-center justify-center">
108
+ <CollectionIcon />
109
+ </div>
110
+ <div class="flex-1 min-w-0">
111
+ <div class="text-white font-medium">{col.label}</div>
112
+ <div class="text-white/50 text-sm">
113
+ {col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'} &middot; {col.fields.length} fields
114
+ </div>
115
+ </div>
116
+ <ChevronRightIcon />
117
+ </button>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ // ============================================================================
126
+ // Icons
127
+ // ============================================================================
128
+
129
+ function CloseButton({ onClick }: { onClick: () => void }) {
130
+ return (
131
+ <button
132
+ type="button"
133
+ onClick={onClick}
134
+ class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
135
+ data-cms-ui
136
+ >
137
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
139
+ </svg>
140
+ </button>
141
+ )
142
+ }
143
+
144
+ function CollectionIcon() {
145
+ return (
146
+ <svg class="w-5 h-5 text-cms-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147
+ <path
148
+ stroke-linecap="round"
149
+ stroke-linejoin="round"
150
+ stroke-width="2"
151
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
152
+ />
153
+ </svg>
154
+ )
155
+ }
156
+
157
+ function ChevronRightIcon() {
158
+ return (
159
+ <svg class="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
160
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
161
+ </svg>
162
+ )
163
+ }
@@ -0,0 +1,260 @@
1
+ import { useEffect, useRef } from 'preact/hooks'
2
+ import { Z_INDEX } from '../constants'
3
+ import * as signals from '../signals'
4
+
5
+ export interface EditableHighlightsProps {
6
+ visible: boolean
7
+ }
8
+
9
+ interface HighlightRect {
10
+ cmsId: string
11
+ type: 'text' | 'component' | 'image'
12
+ rect: DOMRect
13
+ }
14
+
15
+ /**
16
+ * Renders lightweight dashed outlines around all editable elements.
17
+ * Uses a single canvas-like approach with Shadow DOM for performance.
18
+ */
19
+ export function EditableHighlights({ visible }: EditableHighlightsProps) {
20
+ const containerRef = useRef<HTMLDivElement>(null)
21
+ const shadowRootRef = useRef<ShadowRoot | null>(null)
22
+ const overlaysContainerRef = useRef<HTMLDivElement | null>(null)
23
+ const rafRef = useRef<number | null>(null)
24
+
25
+ // Initialize Shadow DOM once
26
+ useEffect(() => {
27
+ if (containerRef.current && !shadowRootRef.current) {
28
+ shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' })
29
+
30
+ const style = document.createElement('style')
31
+ style.textContent = `
32
+ :host {
33
+ position: fixed;
34
+ top: 0;
35
+ left: 0;
36
+ width: 100%;
37
+ height: 100%;
38
+ pointer-events: none;
39
+ z-index: ${Z_INDEX.HIGHLIGHT};
40
+ }
41
+
42
+ .highlights-container {
43
+ position: fixed;
44
+ top: 0;
45
+ left: 0;
46
+ width: 100%;
47
+ height: 100%;
48
+ pointer-events: none;
49
+ }
50
+
51
+ .highlight-overlay {
52
+ position: fixed;
53
+ box-sizing: border-box;
54
+ border: 2px dashed #1A1A1A;
55
+ border-radius: 4px;
56
+ pointer-events: none;
57
+ opacity: 0.5;
58
+ transition: opacity 100ms ease;
59
+ }
60
+
61
+ .highlight-overlay.component {
62
+ border-style: solid;
63
+ }
64
+
65
+ .highlight-overlay.image {
66
+ border-style: dotted;
67
+ }
68
+
69
+ .highlights-container.hidden {
70
+ display: none;
71
+ }
72
+ `
73
+
74
+ overlaysContainerRef.current = document.createElement('div')
75
+ overlaysContainerRef.current.className = 'highlights-container hidden'
76
+
77
+ shadowRootRef.current.appendChild(style)
78
+ shadowRootRef.current.appendChild(overlaysContainerRef.current)
79
+ }
80
+ }, [])
81
+
82
+ // Update highlights when visible changes or on scroll/resize
83
+ useEffect(() => {
84
+ if (!overlaysContainerRef.current) return
85
+
86
+ if (!visible) {
87
+ overlaysContainerRef.current.className = 'highlights-container hidden'
88
+ if (rafRef.current) {
89
+ cancelAnimationFrame(rafRef.current)
90
+ rafRef.current = null
91
+ }
92
+ return
93
+ }
94
+
95
+ overlaysContainerRef.current.className = 'highlights-container'
96
+
97
+ const updateHighlights = () => {
98
+ if (!overlaysContainerRef.current || !visible) return
99
+
100
+ const highlights = collectEditableElements()
101
+ renderHighlights(overlaysContainerRef.current, highlights)
102
+ }
103
+
104
+ // Initial render
105
+ updateHighlights()
106
+
107
+ // Use RAF loop for smooth updates during scroll
108
+ let lastScrollY = window.scrollY
109
+ let lastScrollX = window.scrollX
110
+
111
+ const rafLoop = () => {
112
+ if (!visible) return
113
+
114
+ // Only update if scroll position changed
115
+ if (window.scrollY !== lastScrollY || window.scrollX !== lastScrollX) {
116
+ lastScrollY = window.scrollY
117
+ lastScrollX = window.scrollX
118
+ updateHighlights()
119
+ }
120
+
121
+ rafRef.current = requestAnimationFrame(rafLoop)
122
+ }
123
+
124
+ rafRef.current = requestAnimationFrame(rafLoop)
125
+
126
+ // Listen for resize
127
+ const handleResize = () => {
128
+ updateHighlights()
129
+ }
130
+
131
+ window.addEventListener('resize', handleResize)
132
+
133
+ return () => {
134
+ window.removeEventListener('resize', handleResize)
135
+ if (rafRef.current) {
136
+ cancelAnimationFrame(rafRef.current)
137
+ rafRef.current = null
138
+ }
139
+ }
140
+ }, [visible])
141
+
142
+ return (
143
+ <div
144
+ ref={containerRef}
145
+ data-cms-ui
146
+ style={{
147
+ position: 'fixed',
148
+ top: 0,
149
+ left: 0,
150
+ width: 0,
151
+ height: 0,
152
+ pointerEvents: 'none',
153
+ zIndex: Z_INDEX.HIGHLIGHT,
154
+ }}
155
+ />
156
+ )
157
+ }
158
+
159
+ /**
160
+ * Collect all editable elements from the DOM
161
+ */
162
+ function collectEditableElements(): HighlightRect[] {
163
+ const highlights: HighlightRect[] = []
164
+ const manifest = signals.manifest.value
165
+
166
+ // Query all elements with CMS data attributes
167
+ const textElements = document.querySelectorAll('[data-cms-id]')
168
+ const componentElements = document.querySelectorAll('[data-cms-component-id]')
169
+ const imageElements = document.querySelectorAll('img[data-cms-img]')
170
+
171
+ // Process text elements
172
+ textElements.forEach((el) => {
173
+ const cmsId = el.getAttribute('data-cms-id')
174
+ if (!cmsId) return
175
+
176
+ // Skip if this is also a component or image
177
+ if (el.hasAttribute('data-cms-component-id') || el.tagName === 'IMG') return
178
+
179
+ // Skip if not in manifest (invalid element)
180
+ if (!manifest.entries[cmsId]) return
181
+
182
+ const rect = el.getBoundingClientRect()
183
+ // Skip elements not in viewport or too small
184
+ if (rect.width < 10 || rect.height < 10) return
185
+ if (rect.bottom < 0 || rect.top > window.innerHeight) return
186
+ if (rect.right < 0 || rect.left > window.innerWidth) return
187
+
188
+ highlights.push({ cmsId, type: 'text', rect })
189
+ })
190
+
191
+ // Process component elements
192
+ componentElements.forEach((el) => {
193
+ const componentId = el.getAttribute('data-cms-component-id')
194
+ if (!componentId) return
195
+
196
+ // Skip if not in manifest
197
+ if (!manifest.components[componentId]) return
198
+
199
+ const rect = el.getBoundingClientRect()
200
+ if (rect.width < 10 || rect.height < 10) return
201
+ if (rect.bottom < 0 || rect.top > window.innerHeight) return
202
+ if (rect.right < 0 || rect.left > window.innerWidth) return
203
+
204
+ highlights.push({ cmsId: componentId, type: 'component', rect })
205
+ })
206
+
207
+ // Process image elements
208
+ imageElements.forEach((el) => {
209
+ const cmsId = el.getAttribute('data-cms-img')
210
+ if (!cmsId) return
211
+
212
+ const rect = el.getBoundingClientRect()
213
+ if (rect.width < 10 || rect.height < 10) return
214
+ if (rect.bottom < 0 || rect.top > window.innerHeight) return
215
+ if (rect.right < 0 || rect.left > window.innerWidth) return
216
+
217
+ highlights.push({ cmsId, type: 'image', rect })
218
+ })
219
+
220
+ return highlights
221
+ }
222
+
223
+ /**
224
+ * Render highlight overlays efficiently by reusing DOM elements
225
+ */
226
+ function renderHighlights(container: HTMLDivElement, highlights: HighlightRect[]): void {
227
+ // Get existing overlay elements
228
+ const existingOverlays = container.querySelectorAll('.highlight-overlay')
229
+ const existingCount = existingOverlays.length
230
+ const neededCount = highlights.length
231
+
232
+ // Update or create overlays
233
+ highlights.forEach((highlight, index) => {
234
+ let overlay: HTMLDivElement
235
+
236
+ if (index < existingCount) {
237
+ // Reuse existing overlay
238
+ overlay = existingOverlays[index] as HTMLDivElement
239
+ } else {
240
+ // Create new overlay
241
+ overlay = document.createElement('div')
242
+ overlay.className = 'highlight-overlay'
243
+ container.appendChild(overlay)
244
+ }
245
+
246
+ // Update class based on type
247
+ overlay.className = `highlight-overlay ${highlight.type}`
248
+
249
+ // Update position
250
+ overlay.style.left = `${highlight.rect.left - 6}px`
251
+ overlay.style.top = `${highlight.rect.top - 6}px`
252
+ overlay.style.width = `${highlight.rect.width + 12}px`
253
+ overlay.style.height = `${highlight.rect.height + 12}px`
254
+ })
255
+
256
+ // Remove extra overlays
257
+ for (let i = neededCount; i < existingCount; i++) {
258
+ existingOverlays[i]!.remove()
259
+ }
260
+ }
@@ -0,0 +1,87 @@
1
+ import { Component, type ComponentChildren } from 'preact'
2
+
3
+ interface ErrorBoundaryProps {
4
+ children: ComponentChildren
5
+ fallback?: ComponentChildren
6
+ onError?: (error: Error, errorInfo: { componentStack: string }) => void
7
+ componentName?: string
8
+ }
9
+
10
+ interface ErrorBoundaryState {
11
+ hasError: boolean
12
+ error: Error | null
13
+ }
14
+
15
+ /**
16
+ * Error boundary component to catch and handle errors in CMS UI components.
17
+ * Prevents the entire CMS overlay from crashing when a component fails.
18
+ */
19
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
20
+ constructor(props: ErrorBoundaryProps) {
21
+ super(props)
22
+ this.state = { hasError: false, error: null }
23
+ }
24
+
25
+ static override getDerivedStateFromError(error: Error): ErrorBoundaryState {
26
+ return { hasError: true, error }
27
+ }
28
+
29
+ override componentDidCatch(error: Error, errorInfo: { componentStack: string }): void {
30
+ console.error('[CMS] Component error:', error)
31
+ console.error('[CMS] Component stack:', errorInfo.componentStack)
32
+ this.props.onError?.(error, errorInfo)
33
+ }
34
+
35
+ private handleRetry = (): void => {
36
+ this.setState({ hasError: false, error: null })
37
+ }
38
+
39
+ render() {
40
+ if (this.state.hasError) {
41
+ if (this.props.fallback) {
42
+ return this.props.fallback
43
+ }
44
+
45
+ const componentName = this.props.componentName || 'Component'
46
+
47
+ return (
48
+ <div
49
+ data-cms-ui
50
+ class="p-4 bg-red-50 border-2 border-red-500 text-red-800 font-sans text-sm"
51
+ style={{
52
+ fontFamily: 'system-ui, -apple-system, sans-serif',
53
+ }}
54
+ >
55
+ <div class="font-bold mb-2 flex items-center gap-2">
56
+ <span class="text-red-600">⚠</span>
57
+ {componentName} Error
58
+ </div>
59
+ <div class="text-xs text-red-600 mb-3 font-mono">
60
+ {this.state.error?.message || 'An unexpected error occurred'}
61
+ </div>
62
+ <button
63
+ onClick={this.handleRetry}
64
+ class="px-3 py-1.5 bg-red-600 text-white border-2 border-red-800 text-xs font-bold cursor-pointer hover:bg-red-700 transition-colors"
65
+ >
66
+ Retry
67
+ </button>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ return this.props.children
73
+ }
74
+ }
75
+
76
+ export const SilentErrorFallback = () => null
77
+
78
+ export const CompactErrorFallback = ({ message }: { message?: string }) => {
79
+ return (
80
+ <div
81
+ data-cms-ui
82
+ class="px-2 py-1 bg-red-100 text-red-700 text-xs font-sans border border-red-300 inline-block"
83
+ >
84
+ ⚠ {message || 'Error'}
85
+ </div>
86
+ )
87
+ }