@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,469 @@
1
+ import { markdownEditorState, openMediaLibraryWithCallback, updateMarkdownFrontmatter } from '../signals'
2
+ import type { CollectionDefinition, FieldDefinition, MarkdownPageEntry } from '../types'
3
+ import { ComboBoxField, ImageField, NumberField, TextField, ToggleField } from './fields'
4
+
5
+ // ============================================================================
6
+ // Generic Frontmatter Field (auto-detect by value type)
7
+ // ============================================================================
8
+
9
+ interface FrontmatterFieldProps {
10
+ fieldKey: string
11
+ value: unknown
12
+ onChange: (value: unknown) => void
13
+ }
14
+
15
+ export function FrontmatterField({
16
+ fieldKey,
17
+ value,
18
+ onChange,
19
+ }: FrontmatterFieldProps) {
20
+ // Format field key as label (e.g., "featuredImage" -> "Featured Image")
21
+ const label = fieldKey
22
+ .replace(/([A-Z])/g, ' $1')
23
+ .replace(/^./, (str) => str.toUpperCase())
24
+ .trim()
25
+
26
+ // Detect field type based on value
27
+ const isBoolean = typeof value === 'boolean'
28
+ const isDate = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)
29
+ const isArray = Array.isArray(value)
30
+
31
+ // Boolean field - checkbox
32
+ if (isBoolean) {
33
+ return (
34
+ <label
35
+ class="flex items-center gap-2 text-sm text-white/80 cursor-pointer"
36
+ data-cms-ui
37
+ >
38
+ <input
39
+ type="checkbox"
40
+ checked={value}
41
+ onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
42
+ class="w-4 h-4 rounded border-white/20 bg-white/10 text-cms-primary focus:ring-cms-primary focus:ring-offset-0 cursor-pointer"
43
+ data-cms-ui
44
+ />
45
+ {label}
46
+ </label>
47
+ )
48
+ }
49
+
50
+ // Date field
51
+ if (isDate) {
52
+ return (
53
+ <div class="flex flex-col gap-1" data-cms-ui>
54
+ <label class="text-xs text-white/60 font-medium">{label}</label>
55
+ <input
56
+ type="date"
57
+ value={typeof value === 'string' ? value.split('T')[0] : ''}
58
+ onChange={(e) => onChange((e.target as HTMLInputElement).value)}
59
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-cms-primary"
60
+ data-cms-ui
61
+ />
62
+ </div>
63
+ )
64
+ }
65
+
66
+ // Array field (e.g., categories) - comma-separated input
67
+ if (isArray) {
68
+ return (
69
+ <div class="flex flex-col gap-1 col-span-2" data-cms-ui>
70
+ <label class="text-xs text-white/60 font-medium">{label}</label>
71
+ <input
72
+ type="text"
73
+ value={(value as unknown[]).join(', ')}
74
+ onChange={(e) => {
75
+ const inputValue = (e.target as HTMLInputElement).value
76
+ const arrayValue = inputValue
77
+ .split(',')
78
+ .map((s) => s.trim())
79
+ .filter(Boolean)
80
+ onChange(arrayValue)
81
+ }}
82
+ placeholder={`Enter ${label.toLowerCase()} separated by commas`}
83
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary"
84
+ data-cms-ui
85
+ />
86
+ </div>
87
+ )
88
+ }
89
+
90
+ // String field (default) - check if it's a long text (excerpt, etc.)
91
+ const isLongText = fieldKey.toLowerCase().includes('excerpt')
92
+ || fieldKey.toLowerCase().includes('description')
93
+ || (typeof value === 'string' && value.length > 100)
94
+
95
+ if (isLongText) {
96
+ return (
97
+ <div class="flex flex-col gap-1 col-span-2" data-cms-ui>
98
+ <label class="text-xs text-white/60 font-medium">{label}</label>
99
+ <textarea
100
+ value={typeof value === 'string' ? value : ''}
101
+ onChange={(e) => onChange((e.target as HTMLTextAreaElement).value)}
102
+ rows={3}
103
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary resize-none"
104
+ data-cms-ui
105
+ />
106
+ </div>
107
+ )
108
+ }
109
+
110
+ // Default text input
111
+ return (
112
+ <div class="flex flex-col gap-1" data-cms-ui>
113
+ <label class="text-xs text-white/60 font-medium">{label}</label>
114
+ <input
115
+ type="text"
116
+ value={typeof value === 'string' ? value : String(value ?? '')}
117
+ onChange={(e) => onChange((e.target as HTMLInputElement).value)}
118
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-cms-primary"
119
+ data-cms-ui
120
+ />
121
+ </div>
122
+ )
123
+ }
124
+
125
+ // ============================================================================
126
+ // Create Mode Frontmatter — schema-aware field rendering
127
+ // ============================================================================
128
+
129
+ interface CreateModeFrontmatterProps {
130
+ page: MarkdownPageEntry
131
+ collectionDefinition: CollectionDefinition
132
+ onSlugManualEdit: () => void
133
+ }
134
+
135
+ export function CreateModeFrontmatter({
136
+ page,
137
+ collectionDefinition,
138
+ onSlugManualEdit,
139
+ }: CreateModeFrontmatterProps) {
140
+ return (
141
+ <div class="space-y-4">
142
+ {/* Slug field */}
143
+ <div>
144
+ <label class="block text-xs font-medium text-white/70 mb-1.5">
145
+ URL Slug
146
+ </label>
147
+ <input
148
+ type="text"
149
+ value={page.slug}
150
+ onInput={(e) => {
151
+ onSlugManualEdit()
152
+ const slug = (e.target as HTMLInputElement).value
153
+ markdownEditorState.value = {
154
+ ...markdownEditorState.value,
155
+ currentPage: markdownEditorState.value.currentPage
156
+ ? { ...markdownEditorState.value.currentPage, slug }
157
+ : null,
158
+ }
159
+ }}
160
+ placeholder="url-friendly-slug"
161
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-cms-md text-sm text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
162
+ data-cms-ui
163
+ />
164
+ <p class="mt-1 text-xs text-white/40">
165
+ Will be saved to: src/content/{collectionDefinition.name}/
166
+ {page.slug || 'your-slug'}.{collectionDefinition.fileExtension}
167
+ </p>
168
+ </div>
169
+
170
+ {/* Schema fields */}
171
+ <div class="grid grid-cols-2 gap-4">
172
+ {collectionDefinition.fields.map((field) => (
173
+ <SchemaFrontmatterField
174
+ key={field.name}
175
+ field={field}
176
+ value={page.frontmatter[field.name]}
177
+ onChange={(newValue) => updateMarkdownFrontmatter({ [field.name]: newValue })}
178
+ />
179
+ ))}
180
+ </div>
181
+ </div>
182
+ )
183
+ }
184
+
185
+ // ============================================================================
186
+ // Edit Mode Frontmatter — uses schema fields when available, falls back to generic
187
+ // ============================================================================
188
+
189
+ interface EditModeFrontmatterProps {
190
+ page: MarkdownPageEntry
191
+ collectionDefinition?: CollectionDefinition
192
+ }
193
+
194
+ export function EditModeFrontmatter({
195
+ page,
196
+ collectionDefinition,
197
+ }: EditModeFrontmatterProps) {
198
+ // Collect schema field names for filtering extra keys
199
+ const schemaFieldNames = new Set(
200
+ collectionDefinition?.fields.map((f) => f.name) ?? [],
201
+ )
202
+ // Frontmatter keys not covered by the schema (user-added fields)
203
+ const extraKeys = Object.keys(page.frontmatter).filter(
204
+ (key) => !schemaFieldNames.has(key),
205
+ )
206
+
207
+ return (
208
+ <div class="space-y-4">
209
+ {/* Slug field (always disabled in edit mode) */}
210
+ <div>
211
+ <label class="block text-xs font-medium text-white/70 mb-1.5">
212
+ URL Slug
213
+ </label>
214
+ <input
215
+ type="text"
216
+ value={page.slug}
217
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-cms-md text-sm text-white/50 focus:outline-none cursor-not-allowed"
218
+ disabled
219
+ data-cms-ui
220
+ />
221
+ </div>
222
+ <div class="grid grid-cols-2 gap-4">
223
+ {collectionDefinition
224
+ ? (
225
+ <>
226
+ {/* Schema-aware fields */}
227
+ {collectionDefinition.fields.map((field) => (
228
+ <SchemaFrontmatterField
229
+ key={field.name}
230
+ field={field}
231
+ value={page.frontmatter[field.name]}
232
+ onChange={(newValue) => updateMarkdownFrontmatter({ [field.name]: newValue })}
233
+ />
234
+ ))}
235
+ {/* Extra fields not in schema */}
236
+ {extraKeys.map((key) => (
237
+ <FrontmatterField
238
+ key={key}
239
+ fieldKey={key}
240
+ value={page.frontmatter[key]}
241
+ onChange={(newValue) => updateMarkdownFrontmatter({ [key]: newValue })}
242
+ />
243
+ ))}
244
+ </>
245
+ )
246
+ : (
247
+ /* Generic fallback when no schema is available */
248
+ Object.entries(page.frontmatter).map(([key, value]) => (
249
+ <FrontmatterField
250
+ key={key}
251
+ fieldKey={key}
252
+ value={value}
253
+ onChange={(newValue) => updateMarkdownFrontmatter({ [key]: newValue })}
254
+ />
255
+ ))
256
+ )}
257
+ </div>
258
+ </div>
259
+ )
260
+ }
261
+
262
+ // ============================================================================
263
+ // Schema-aware Frontmatter Field
264
+ // ============================================================================
265
+
266
+ interface SchemaFrontmatterFieldProps {
267
+ field: FieldDefinition
268
+ value: unknown
269
+ onChange: (value: unknown) => void
270
+ }
271
+
272
+ export function SchemaFrontmatterField({
273
+ field,
274
+ value,
275
+ onChange,
276
+ }: SchemaFrontmatterFieldProps) {
277
+ const label = formatFieldLabel(field.name)
278
+
279
+ switch (field.type) {
280
+ case 'text':
281
+ case 'url':
282
+ return (
283
+ <TextField
284
+ label={label}
285
+ value={(value as string) ?? ''}
286
+ placeholder={getPlaceholder(field)}
287
+ onChange={(v) => onChange(v)}
288
+ />
289
+ )
290
+
291
+ case 'image':
292
+ return (
293
+ <ImageField
294
+ label={label}
295
+ value={(value as string) ?? ''}
296
+ placeholder={getPlaceholder(field)}
297
+ onChange={(v) => onChange(v)}
298
+ onBrowse={() => {
299
+ openMediaLibraryWithCallback((url: string) => {
300
+ onChange(url)
301
+ })
302
+ }}
303
+ />
304
+ )
305
+
306
+ case 'textarea':
307
+ return (
308
+ <div class="flex flex-col gap-1 col-span-2" data-cms-ui>
309
+ <label class="text-xs text-white/60 font-medium">{label}</label>
310
+ <textarea
311
+ value={(value as string) ?? ''}
312
+ onInput={(e) => onChange((e.target as HTMLTextAreaElement).value)}
313
+ placeholder={getPlaceholder(field)}
314
+ rows={3}
315
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40 resize-none"
316
+ data-cms-ui
317
+ />
318
+ </div>
319
+ )
320
+
321
+ case 'date':
322
+ return (
323
+ <div class="flex flex-col gap-1" data-cms-ui>
324
+ <label class="text-xs text-white/60 font-medium">{label}</label>
325
+ <input
326
+ type="date"
327
+ value={(value as string) ?? ''}
328
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
329
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-white/40"
330
+ data-cms-ui
331
+ />
332
+ </div>
333
+ )
334
+
335
+ case 'number':
336
+ return (
337
+ <NumberField
338
+ label={label}
339
+ value={(value as number) ?? undefined}
340
+ onChange={(v) => onChange(v ?? 0)}
341
+ />
342
+ )
343
+
344
+ case 'boolean':
345
+ return (
346
+ <ToggleField
347
+ label={label}
348
+ value={!!value}
349
+ onChange={(v) => onChange(v)}
350
+ />
351
+ )
352
+
353
+ case 'select':
354
+ return (
355
+ <ComboBoxField
356
+ label={label}
357
+ value={(value as string) ?? ''}
358
+ placeholder={getPlaceholder(field)}
359
+ options={(field.options ?? []).map((opt) => ({
360
+ value: opt,
361
+ label: opt,
362
+ }))}
363
+ onChange={(v) => onChange(v)}
364
+ />
365
+ )
366
+
367
+ case 'array': {
368
+ const items = Array.isArray(value) ? value : []
369
+ if (field.options && field.options.length > 0) {
370
+ return (
371
+ <div class="col-span-2 space-y-1.5" data-cms-ui>
372
+ <label class="text-xs text-white/60 font-medium">{label}</label>
373
+ <div class="space-y-2">
374
+ {field.options.map((opt) => (
375
+ <label key={opt} class="flex items-center gap-2 cursor-pointer">
376
+ <input
377
+ type="checkbox"
378
+ checked={items.includes(opt)}
379
+ onChange={(e) => {
380
+ if ((e.target as HTMLInputElement).checked) {
381
+ onChange([...items, opt])
382
+ } else {
383
+ onChange(items.filter((i: unknown) => i !== opt))
384
+ }
385
+ }}
386
+ class="rounded border-white/20 bg-white/10 text-cms-primary focus:ring-cms-primary"
387
+ data-cms-ui
388
+ />
389
+ <span class="text-sm text-white/80">{opt}</span>
390
+ </label>
391
+ ))}
392
+ </div>
393
+ </div>
394
+ )
395
+ }
396
+ return (
397
+ <div class="col-span-2 flex flex-col gap-1" data-cms-ui>
398
+ <label class="text-xs text-white/60 font-medium">{label}</label>
399
+ <input
400
+ type="text"
401
+ value={(items as unknown[]).join(', ')}
402
+ onInput={(e) => {
403
+ const inputValue = (e.target as HTMLInputElement).value
404
+ const arrayValue = inputValue
405
+ .split(',')
406
+ .map((s) => s.trim())
407
+ .filter(Boolean)
408
+ onChange(arrayValue)
409
+ }}
410
+ placeholder={`Enter ${label.toLowerCase()} separated by commas`}
411
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
412
+ data-cms-ui
413
+ />
414
+ </div>
415
+ )
416
+ }
417
+
418
+ default:
419
+ return (
420
+ <div class="flex flex-col gap-1" data-cms-ui>
421
+ <label class="text-xs text-white/60 font-medium">{label}</label>
422
+ <input
423
+ type="text"
424
+ value={String(value ?? '')}
425
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
426
+ class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white placeholder-white/30 focus:outline-none focus:border-white/40"
427
+ data-cms-ui
428
+ />
429
+ </div>
430
+ )
431
+ }
432
+ }
433
+
434
+ // ============================================================================
435
+ // Helper Functions
436
+ // ============================================================================
437
+
438
+ export function formatFieldLabel(name: string): string {
439
+ return name
440
+ .replace(/([A-Z])/g, ' $1')
441
+ .replace(/[-_]/g, ' ')
442
+ .replace(/\b\w/g, (c) => c.toUpperCase())
443
+ .trim()
444
+ }
445
+
446
+ export function getPlaceholder(field: FieldDefinition): string {
447
+ if (field.examples && field.examples.length > 0) {
448
+ return String(field.examples[0])
449
+ }
450
+ switch (field.type) {
451
+ case 'url':
452
+ return 'https://...'
453
+ case 'image':
454
+ return '/images/...'
455
+ case 'date':
456
+ return 'YYYY-MM-DD'
457
+ default:
458
+ return `Enter ${formatFieldLabel(field.name).toLowerCase()}...`
459
+ }
460
+ }
461
+
462
+ export function slugify(text: string): string {
463
+ return text
464
+ .toLowerCase()
465
+ .trim()
466
+ .replace(/[^\w\s-]/g, '')
467
+ .replace(/[\s_-]+/g, '-')
468
+ .replace(/^-+|-+$/g, '')
469
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Shadow DOM-based highlight overlay for CMS elements.
3
+ * This component renders highlights without modifying the target element's styles.
4
+ */
5
+
6
+ export interface HighlightState {
7
+ color: string
8
+ style: 'solid' | 'dashed'
9
+ visible: boolean
10
+ }
11
+
12
+ // Map to track highlights by element
13
+ const highlightMap = new WeakMap<HTMLElement, HTMLElement>()
14
+
15
+ // Container for all highlight overlays
16
+ let highlightContainer: HTMLElement | null = null
17
+
18
+ // Flag to track if custom element is registered
19
+ let customElementRegistered = false
20
+
21
+ /**
22
+ * Custom element that renders a highlight overlay using Shadow DOM
23
+ */
24
+ class CmsHighlightOverlay extends HTMLElement {
25
+ private shadow: ShadowRoot
26
+ private overlayElement: HTMLDivElement
27
+ private resizeObserver: ResizeObserver | null = null
28
+ private targetElement: HTMLElement | null = null
29
+ private animationFrameId: number | null = null
30
+
31
+ constructor() {
32
+ super()
33
+ this.shadow = this.attachShadow({ mode: 'open' })
34
+
35
+ // Create styles
36
+ const style = document.createElement('style')
37
+ style.textContent = `
38
+ :host {
39
+ position: absolute;
40
+ pointer-events: none;
41
+ z-index: 2147483645;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ .overlay {
46
+ position: absolute;
47
+ inset: 0;
48
+ border-radius: 4px;
49
+ box-sizing: border-box;
50
+ transition: border-color 150ms ease, border-style 150ms ease;
51
+ }
52
+ `
53
+
54
+ this.overlayElement = document.createElement('div')
55
+ this.overlayElement.className = 'overlay'
56
+
57
+ this.shadow.appendChild(style)
58
+ this.shadow.appendChild(this.overlayElement)
59
+ }
60
+
61
+ connectedCallback() {
62
+ this.startPositionTracking()
63
+ }
64
+
65
+ disconnectedCallback() {
66
+ this.stopPositionTracking()
67
+ }
68
+
69
+ /**
70
+ * Set the target element to highlight
71
+ */
72
+ setTarget(element: HTMLElement) {
73
+ this.targetElement = element
74
+ this.updatePosition()
75
+
76
+ // Set up resize observer
77
+ if (this.resizeObserver) {
78
+ this.resizeObserver.disconnect()
79
+ }
80
+
81
+ this.resizeObserver = new ResizeObserver(() => {
82
+ this.updatePosition()
83
+ })
84
+ this.resizeObserver.observe(element)
85
+ }
86
+
87
+ /**
88
+ * Set the highlight style
89
+ */
90
+ setHighlightStyle(color: string, style: 'solid' | 'dashed') {
91
+ this.overlayElement.style.borderWidth = '2px'
92
+ this.overlayElement.style.borderColor = color
93
+ this.overlayElement.style.borderStyle = style
94
+ }
95
+
96
+ /**
97
+ * Update position to match target element
98
+ */
99
+ private updatePosition() {
100
+ if (!this.targetElement) return
101
+
102
+ const rect = this.targetElement.getBoundingClientRect()
103
+ const scrollX = window.scrollX
104
+ const scrollY = window.scrollY
105
+
106
+ // Position with offset to give breathing room from content
107
+ this.style.left = `${rect.left + scrollX - 6}px`
108
+ this.style.top = `${rect.top + scrollY - 6}px`
109
+ this.style.width = `${rect.width + 12}px`
110
+ this.style.height = `${rect.height + 12}px`
111
+ }
112
+
113
+ /**
114
+ * Start continuous position tracking for scroll/resize
115
+ */
116
+ private startPositionTracking() {
117
+ const track = () => {
118
+ this.updatePosition()
119
+ this.animationFrameId = requestAnimationFrame(track)
120
+ }
121
+ this.animationFrameId = requestAnimationFrame(track)
122
+ }
123
+
124
+ /**
125
+ * Stop position tracking
126
+ */
127
+ private stopPositionTracking() {
128
+ if (this.animationFrameId !== null) {
129
+ cancelAnimationFrame(this.animationFrameId)
130
+ this.animationFrameId = null
131
+ }
132
+ if (this.resizeObserver) {
133
+ this.resizeObserver.disconnect()
134
+ this.resizeObserver = null
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Register the custom element (safe to call multiple times)
141
+ */
142
+ function ensureCustomElementRegistered(): void {
143
+ if (customElementRegistered) return
144
+ if (typeof window === 'undefined' || typeof customElements === 'undefined') return
145
+
146
+ if (!customElements.get('cms-highlight-overlay')) {
147
+ customElements.define('cms-highlight-overlay', CmsHighlightOverlay)
148
+ }
149
+ customElementRegistered = true
150
+ }
151
+
152
+ /**
153
+ * Initialize the highlight container
154
+ */
155
+ export function initHighlightContainer(): void {
156
+ if (typeof document === 'undefined') return
157
+ if (highlightContainer) return
158
+
159
+ ensureCustomElementRegistered()
160
+
161
+ highlightContainer = document.createElement('div')
162
+ highlightContainer.id = 'cms-highlight-container'
163
+ highlightContainer.style.cssText = `
164
+ position: absolute;
165
+ top: 0;
166
+ left: 0;
167
+ width: 0;
168
+ height: 0;
169
+ pointer-events: none;
170
+ z-index: 2147483645;
171
+ `
172
+ document.body.appendChild(highlightContainer)
173
+ }
174
+
175
+ /**
176
+ * Clean up the highlight container
177
+ */
178
+ export function destroyHighlightContainer(): void {
179
+ if (highlightContainer) {
180
+ highlightContainer.remove()
181
+ highlightContainer = null
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Set highlight outline on an element using Shadow DOM overlay
187
+ */
188
+ export function setElementHighlight(
189
+ el: HTMLElement,
190
+ color: string,
191
+ style: 'solid' | 'dashed' = 'solid',
192
+ ): void {
193
+ initHighlightContainer()
194
+
195
+ let overlay = highlightMap.get(el)
196
+
197
+ if (!overlay) {
198
+ overlay = document.createElement('cms-highlight-overlay') as HTMLElement
199
+ highlightMap.set(el, overlay)
200
+ highlightContainer?.appendChild(overlay) // Set target after adding to DOM
201
+ ;(overlay as CmsHighlightOverlay).setTarget(el)
202
+ }
203
+
204
+ ;(overlay as CmsHighlightOverlay).setHighlightStyle(color, style)
205
+ }
206
+
207
+ /**
208
+ * Clear highlight from an element
209
+ */
210
+ export function clearElementHighlight(el: HTMLElement): void {
211
+ const overlay = highlightMap.get(el)
212
+ if (overlay) {
213
+ overlay.remove()
214
+ highlightMap.delete(el)
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Clear all highlights
220
+ */
221
+ export function clearAllHighlights(): void {
222
+ if (highlightContainer) {
223
+ highlightContainer.innerHTML = ''
224
+ }
225
+ // WeakMap entries will be garbage collected
226
+ }
227
+
228
+ // Export the class for type checking
229
+ export { CmsHighlightOverlay }