@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,248 @@
1
+ import { useMemo } from 'preact/hooks'
2
+ import {
3
+ closeCollectionsBrowser,
4
+ isCollectionsBrowserOpen,
5
+ manifest,
6
+ openMarkdownEditorForNewPage,
7
+ selectBrowserCollection,
8
+ selectedBrowserCollection,
9
+ } from '../signals'
10
+ import { savePendingEntryNavigation } from '../storage'
11
+
12
+ export function CollectionsBrowser() {
13
+ const visible = isCollectionsBrowserOpen.value
14
+ const selected = selectedBrowserCollection.value
15
+
16
+ const collectionDefinitions = manifest.value.collectionDefinitions ?? {}
17
+
18
+ const collections = useMemo(() => {
19
+ return Object.values(collectionDefinitions)
20
+ }, [collectionDefinitions])
21
+
22
+ if (!visible) return null
23
+
24
+ const handleClose = () => {
25
+ closeCollectionsBrowser()
26
+ }
27
+
28
+ const handleBackdropClick = (e: Event) => {
29
+ handleClose()
30
+ }
31
+
32
+ // View 2: Entry list for selected collection
33
+ if (selected) {
34
+ const def = collectionDefinitions[selected]
35
+ if (!def) return null
36
+
37
+ const entries = def.entries ?? []
38
+
39
+ const handleEntryClick = (slug: string, sourcePath: string, pathname?: string) => {
40
+ closeCollectionsBrowser()
41
+ // Navigate to the collection detail page to edit inline.
42
+ // Use known pathname or construct one from collection/slug.
43
+ const targetPath = pathname || `/${selected}/${slug}`
44
+ savePendingEntryNavigation({ collectionName: selected, slug, sourcePath, pathname: targetPath })
45
+ window.location.href = targetPath
46
+ }
47
+
48
+ const handleAddNew = () => {
49
+ closeCollectionsBrowser()
50
+ openMarkdownEditorForNewPage(selected, def)
51
+ }
52
+
53
+ return (
54
+ <div
55
+ class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
56
+ onClick={handleBackdropClick}
57
+ data-cms-ui
58
+ >
59
+ <div
60
+ 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 flex flex-col max-h-[80vh]"
61
+ onClick={(e) => e.stopPropagation()}
62
+ data-cms-ui
63
+ >
64
+ {/* Header */}
65
+ <div class="flex items-center justify-between p-5 border-b border-white/10 shrink-0">
66
+ <div class="flex items-center gap-3">
67
+ <button
68
+ type="button"
69
+ onClick={() => selectBrowserCollection(null)}
70
+ class="text-white/50 hover:text-white p-1 hover:bg-white/10 rounded-full transition-colors"
71
+ data-cms-ui
72
+ >
73
+ <BackArrowIcon />
74
+ </button>
75
+ <h2 class="text-lg font-semibold text-white">{def.label}</h2>
76
+ </div>
77
+ <div class="flex items-center gap-2">
78
+ <button
79
+ type="button"
80
+ onClick={handleAddNew}
81
+ class="px-3 py-1.5 text-sm font-medium text-black bg-cms-primary hover:bg-cms-primary/80 rounded-cms-pill transition-colors"
82
+ data-cms-ui
83
+ >
84
+ + Add New
85
+ </button>
86
+ <CloseButton onClick={handleClose} />
87
+ </div>
88
+ </div>
89
+
90
+ {/* Entry list */}
91
+ <div class="p-5 space-y-1 overflow-y-auto flex-1 min-h-0">
92
+ {entries.length === 0 && (
93
+ <div class="text-white/50 text-sm text-center py-8">
94
+ No entries yet. Click "Add New" to create one.
95
+ </div>
96
+ )}
97
+ {entries.map((entry) => (
98
+ <button
99
+ key={entry.slug}
100
+ type="button"
101
+ onClick={() => handleEntryClick(entry.slug, entry.sourcePath, entry.pathname)}
102
+ class="w-full flex items-center gap-3 px-4 py-3 hover:bg-white/10 rounded-cms-lg transition-colors text-left group"
103
+ data-cms-ui
104
+ >
105
+ <div class="flex-1 min-w-0">
106
+ <div class={`font-medium truncate ${entry.draft ? 'text-white/40' : 'text-white'}`}>
107
+ {entry.title || entry.slug}
108
+ </div>
109
+ {entry.title && <div class="text-white/30 text-xs truncate">{entry.slug}</div>}
110
+ </div>
111
+ {entry.draft && (
112
+ <span class="shrink-0 px-2 py-0.5 text-xs font-medium text-amber-400/80 bg-amber-400/10 rounded-full border border-amber-400/20">
113
+ Draft
114
+ </span>
115
+ )}
116
+ <svg
117
+ class="w-4 h-4 text-white/20 group-hover:text-white/40 shrink-0 transition-colors"
118
+ fill="none"
119
+ stroke="currentColor"
120
+ viewBox="0 0 24 24"
121
+ >
122
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
123
+ </svg>
124
+ </button>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ // View 1: Collection list
133
+ if (collections.length === 0) {
134
+ return (
135
+ <div
136
+ class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
137
+ onClick={handleBackdropClick}
138
+ data-cms-ui
139
+ >
140
+ <div
141
+ 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"
142
+ onClick={(e) => e.stopPropagation()}
143
+ data-cms-ui
144
+ >
145
+ <div class="flex items-center justify-between p-5 border-b border-white/10">
146
+ <h2 class="text-lg font-semibold text-white">Collections</h2>
147
+ <CloseButton onClick={handleClose} />
148
+ </div>
149
+ <div class="p-8 text-center">
150
+ <div class="text-white/60 mb-4">No content collections found.</div>
151
+ <p class="text-white/40 text-sm">
152
+ Add markdown files to <code class="bg-white/10 px-1.5 py-0.5 rounded">src/content/</code> subdirectories to enable collections.
153
+ </p>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ return (
161
+ <div
162
+ class="fixed inset-0 z-2147483647 flex items-center justify-center bg-black/60 backdrop-blur-sm"
163
+ onClick={handleBackdropClick}
164
+ data-cms-ui
165
+ >
166
+ <div
167
+ 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 flex flex-col max-h-[80vh]"
168
+ onClick={(e) => e.stopPropagation()}
169
+ data-cms-ui
170
+ >
171
+ <div class="flex items-center justify-between p-5 border-b border-white/10 shrink-0">
172
+ <h2 class="text-lg font-semibold text-white">Collections</h2>
173
+ <CloseButton onClick={handleClose} />
174
+ </div>
175
+ <div class="p-5 space-y-2 overflow-y-auto flex-1 min-h-0">
176
+ {collections.map((col) => (
177
+ <button
178
+ key={col.name}
179
+ type="button"
180
+ onClick={() => selectBrowserCollection(col.name)}
181
+ 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"
182
+ data-cms-ui
183
+ >
184
+ <div class="shrink-0 w-10 h-10 bg-cms-primary/20 rounded-cms-md flex items-center justify-center">
185
+ <CollectionIcon />
186
+ </div>
187
+ <div class="flex-1 min-w-0">
188
+ <div class="text-white font-medium">{col.label}</div>
189
+ <div class="text-white/50 text-sm">
190
+ {col.entryCount} {col.entryCount === 1 ? 'entry' : 'entries'}
191
+ </div>
192
+ </div>
193
+ <ChevronRightIcon />
194
+ </button>
195
+ ))}
196
+ </div>
197
+ </div>
198
+ </div>
199
+ )
200
+ }
201
+
202
+ // ============================================================================
203
+ // Icons
204
+ // ============================================================================
205
+
206
+ function CloseButton({ onClick }: { onClick: () => void }) {
207
+ return (
208
+ <button
209
+ type="button"
210
+ onClick={onClick}
211
+ class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
212
+ data-cms-ui
213
+ >
214
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
216
+ </svg>
217
+ </button>
218
+ )
219
+ }
220
+
221
+ function CollectionIcon() {
222
+ return (
223
+ <svg class="w-5 h-5 text-cms-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
224
+ <path
225
+ stroke-linecap="round"
226
+ stroke-linejoin="round"
227
+ stroke-width="2"
228
+ 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"
229
+ />
230
+ </svg>
231
+ )
232
+ }
233
+
234
+ function ChevronRightIcon() {
235
+ return (
236
+ <svg class="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
237
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
238
+ </svg>
239
+ )
240
+ }
241
+
242
+ function BackArrowIcon() {
243
+ return (
244
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
245
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
246
+ </svg>
247
+ )
248
+ }
@@ -0,0 +1,314 @@
1
+ import { useCallback, useEffect, useState } from 'preact/hooks'
2
+ import {
3
+ applyColorChange,
4
+ COLOR_PREVIEW_MAP,
5
+ DEFAULT_TAILWIND_COLORS,
6
+ getColorPreview,
7
+ parseColorClass,
8
+ SPECIAL_COLORS,
9
+ STANDARD_SHADES,
10
+ } from '../color-utils'
11
+ import { CSS, Z_INDEX } from '../constants'
12
+ import { cn } from '../lib/cn'
13
+ import * as signals from '../signals'
14
+ import type { Attribute, AvailableColors } from '../types'
15
+
16
+ export interface ColorToolbarProps {
17
+ visible: boolean
18
+ rect: DOMRect | null
19
+ element: HTMLElement | null
20
+ availableColors: AvailableColors | undefined
21
+ currentClasses: Record<string, Attribute> | undefined
22
+ onColorChange?: (type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText', oldClass: string, newClass: string, previousClassName: string, previousStyleCssText: string) => void
23
+ onClose?: () => void
24
+ }
25
+
26
+ interface ColorSwatchProps {
27
+ colorName: string
28
+ shade?: string
29
+ isSelected: boolean
30
+ onClick: () => void
31
+ }
32
+
33
+ function ColorSwatch({ colorName, shade, isSelected, onClick }: ColorSwatchProps) {
34
+ const preview = getColorPreview(colorName, shade)
35
+ const isWhite = colorName === 'white' || (preview === '#ffffff')
36
+ const isTransparent = colorName === 'transparent'
37
+
38
+ return (
39
+ <button
40
+ type="button"
41
+ onClick={onClick}
42
+ title={shade ? `${colorName}-${shade}` : colorName}
43
+ class={cn(
44
+ 'w-7 h-7 rounded-full border-2 transition-all cursor-pointer hover:scale-110',
45
+ isSelected ? 'border-cms-primary ring-2 ring-cms-primary/30 scale-110' : 'border-transparent',
46
+ isWhite && !isSelected && 'border-white/30',
47
+ )}
48
+ style={{
49
+ backgroundColor: isTransparent ? 'transparent' : preview,
50
+ backgroundImage: isTransparent
51
+ ? 'linear-gradient(45deg, #555 25%, transparent 25%, transparent 75%, #555 75%, #555), linear-gradient(45deg, #555 25%, transparent 25%, transparent 75%, #555 75%, #555)'
52
+ : undefined,
53
+ backgroundSize: isTransparent ? '8px 8px' : undefined,
54
+ backgroundPosition: isTransparent ? '0 0, 4px 4px' : undefined,
55
+ }}
56
+ />
57
+ )
58
+ }
59
+
60
+ interface ColorSectionProps {
61
+ title: string
62
+ type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText'
63
+ currentClass: string | undefined
64
+ availableColors: AvailableColors | undefined
65
+ onSelect: (colorName: string, shade?: string) => void
66
+ }
67
+
68
+ function ColorSection({ title, type, currentClass, availableColors, onSelect }: ColorSectionProps) {
69
+ const [showAllShades, setShowAllShades] = useState(false)
70
+ const [selectedColor, setSelectedColor] = useState<string | undefined>()
71
+
72
+ // Parse current class to get selected color
73
+ const parsedCurrent = currentClass ? parseColorClass(currentClass) : undefined
74
+
75
+ // Get popular colors for quick selection
76
+ const popularColors = [
77
+ { name: 'transparent', shade: undefined },
78
+ { name: 'white', shade: undefined },
79
+ { name: 'black', shade: undefined },
80
+ { name: 'slate', shade: '500' },
81
+ { name: 'gray', shade: '500' },
82
+ { name: 'red', shade: '500' },
83
+ { name: 'orange', shade: '500' },
84
+ { name: 'amber', shade: '500' },
85
+ { name: 'yellow', shade: '500' },
86
+ { name: 'green', shade: '500' },
87
+ { name: 'blue', shade: '500' },
88
+ { name: 'indigo', shade: '500' },
89
+ { name: 'purple', shade: '500' },
90
+ ]
91
+
92
+ // Check if a color is selected
93
+ const isColorSelected = (colorName: string, shade?: string) => {
94
+ if (!parsedCurrent) return false
95
+ if (parsedCurrent.colorName !== colorName) return false
96
+ if (shade && parsedCurrent.shade !== shade) return false
97
+ if (!shade && parsedCurrent.shade) return false
98
+ return true
99
+ }
100
+
101
+ // Handle color selection
102
+ const handleSelect = (colorName: string, shade?: string) => {
103
+ onSelect(colorName, shade)
104
+ }
105
+
106
+ // Get all colors with shades
107
+ const allColors = availableColors?.colors
108
+ ? availableColors.colors.map(color => ({
109
+ name: color.name,
110
+ shades: Object.keys(color.values).filter(s => s !== '').sort((a, b) => Number(a) - Number(b)), // Extract and sort shade keys
111
+ isCustom: color.isCustom ?? false,
112
+ }))
113
+ : [
114
+ ...SPECIAL_COLORS.filter(c => c === 'white' || c === 'black').map(name => ({
115
+ name,
116
+ shades: [] as string[],
117
+ isCustom: false,
118
+ })),
119
+ ...DEFAULT_TAILWIND_COLORS.map(name => ({
120
+ name,
121
+ shades: [...STANDARD_SHADES],
122
+ isCustom: false,
123
+ })),
124
+ ]
125
+
126
+ return (
127
+ <div class="flex flex-col gap-3">
128
+ <div class="text-xs font-medium text-white/50 uppercase tracking-wide">{title}</div>
129
+
130
+ {/* Popular colors grid */}
131
+ <div class="flex flex-wrap gap-2">
132
+ {popularColors.map(({ name, shade }) => (
133
+ <ColorSwatch
134
+ key={`${name}-${shade || 'base'}`}
135
+ colorName={name}
136
+ shade={shade}
137
+ isSelected={isColorSelected(name, shade)}
138
+ onClick={() => handleSelect(name, shade)}
139
+ />
140
+ ))}
141
+ </div>
142
+
143
+ {/* Show more button */}
144
+ <button
145
+ type="button"
146
+ onClick={() => setShowAllShades(!showAllShades)}
147
+ class="text-xs text-white/50 hover:text-white cursor-pointer text-left font-medium"
148
+ >
149
+ {showAllShades ? 'Show less' : 'More colors...'}
150
+ </button>
151
+
152
+ {/* All colors with shades */}
153
+ {showAllShades && (
154
+ <div class="flex flex-col gap-3 max-h-48 overflow-y-auto pr-1">
155
+ {allColors.filter(c => c.shades.length > 0).map(color => (
156
+ <div key={color.name} class="flex flex-col gap-1.5">
157
+ <div class="text-xs text-white/40 capitalize">{color.name}</div>
158
+ <div class="flex flex-wrap gap-1.5">
159
+ {color.shades.map(shade => (
160
+ <ColorSwatch
161
+ key={`${color.name}-${shade}`}
162
+ colorName={color.name}
163
+ shade={shade}
164
+ isSelected={isColorSelected(color.name, shade)}
165
+ onClick={() => handleSelect(color.name, shade)}
166
+ />
167
+ ))}
168
+ </div>
169
+ </div>
170
+ ))}
171
+ </div>
172
+ )}
173
+ </div>
174
+ )
175
+ }
176
+
177
+ export function ColorToolbar({
178
+ visible,
179
+ rect,
180
+ element,
181
+ availableColors,
182
+ currentClasses,
183
+ onColorChange,
184
+ onClose,
185
+ }: ColorToolbarProps) {
186
+ // Handle color selection
187
+ const handleColorSelect = useCallback(
188
+ (type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText', colorName: string, shade?: string) => {
189
+ if (!element) return
190
+
191
+ // Capture className and inline styles before DOM mutation for undo support
192
+ const previousClassName = element.className
193
+ const previousStyleCssText = element.style.cssText
194
+
195
+ const result = applyColorChange(element, type, colorName, shade, availableColors)
196
+ if (result && onColorChange) {
197
+ onColorChange(type, result.oldClass, result.newClass, previousClassName, previousStyleCssText)
198
+ }
199
+ },
200
+ [element, onColorChange, availableColors],
201
+ )
202
+
203
+ // Close on click outside
204
+ useEffect(() => {
205
+ if (!visible) return
206
+
207
+ const handleClickOutside = (e: MouseEvent) => {
208
+ const target = e.target as HTMLElement
209
+ // Don't close if clicking on CMS UI elements
210
+ if (target.closest('[data-cms-ui]')) return
211
+ onClose?.()
212
+ }
213
+
214
+ // Delay adding listener to avoid immediate close
215
+ const timeout = setTimeout(() => {
216
+ document.addEventListener('click', handleClickOutside)
217
+ }, 100)
218
+
219
+ return () => {
220
+ clearTimeout(timeout)
221
+ document.removeEventListener('click', handleClickOutside)
222
+ }
223
+ }, [visible, onClose])
224
+
225
+ if (!visible || !rect) {
226
+ return null
227
+ }
228
+
229
+ return (
230
+ <div
231
+ data-cms-ui
232
+ onMouseDown={(e) => e.stopPropagation()}
233
+ onClick={(e) => e.stopPropagation()}
234
+ class="right-8 top-8 bottom-8 fixed text-xs w-2xs"
235
+ style={{
236
+ zIndex: Z_INDEX.MODAL,
237
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
238
+ }}
239
+ >
240
+ <div class="bg-cms-dark border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.4)] rounded-cms-lg p-4 flex flex-col gap-4 h-full overflow-y-auto">
241
+ {/* Header */}
242
+ <div class="flex items-center justify-between">
243
+ <span class="font-medium text-white">Element Colors</span>
244
+ <button
245
+ type="button"
246
+ onClick={onClose}
247
+ class="text-white/50 hover:text-white cursor-pointer p-1.5 hover:bg-white/10 rounded-full transition-colors"
248
+ >
249
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
250
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
251
+ </svg>
252
+ </button>
253
+ </div>
254
+
255
+ {/* Background color section */}
256
+ <ColorSection
257
+ title="Background"
258
+ type="bg"
259
+ currentClass={currentClasses?.bg?.value}
260
+ availableColors={availableColors}
261
+ onSelect={(colorName, shade) => handleColorSelect('bg', colorName, shade)}
262
+ />
263
+
264
+ {/* Divider */}
265
+ <div class="h-px bg-white/10" />
266
+
267
+ {/* Text color section */}
268
+ <ColorSection
269
+ title="Text"
270
+ type="text"
271
+ currentClass={currentClasses?.text?.value}
272
+ availableColors={availableColors}
273
+ onSelect={(colorName, shade) => handleColorSelect('text', colorName, shade)}
274
+ />
275
+
276
+ {/* Divider */}
277
+ <div class="h-px bg-white/10" />
278
+
279
+ {/* Border color section */}
280
+ <ColorSection
281
+ title="Border"
282
+ type="border"
283
+ currentClass={currentClasses?.border?.value}
284
+ availableColors={availableColors}
285
+ onSelect={(colorName, shade) => handleColorSelect('border', colorName, shade)}
286
+ />
287
+
288
+ {/* Divider */}
289
+ <div class="h-px bg-white/10" />
290
+
291
+ {/* Hover Background color section */}
292
+ <ColorSection
293
+ title="Hover Background"
294
+ type="hoverBg"
295
+ currentClass={currentClasses?.hoverBg?.value}
296
+ availableColors={availableColors}
297
+ onSelect={(colorName, shade) => handleColorSelect('hoverBg', colorName, shade)}
298
+ />
299
+
300
+ {/* Divider */}
301
+ <div class="h-px bg-white/10" />
302
+
303
+ {/* Hover Text color section */}
304
+ <ColorSection
305
+ title="Hover Text"
306
+ type="hoverText"
307
+ currentClass={currentClasses?.hoverText?.value}
308
+ availableColors={availableColors}
309
+ onSelect={(colorName, shade) => handleColorSelect('hoverText', colorName, shade)}
310
+ />
311
+ </div>
312
+ </div>
313
+ )
314
+ }
@@ -0,0 +1,69 @@
1
+ import { cn } from '../lib/cn'
2
+ import { confirmDialogState } from '../signals'
3
+
4
+ export function ConfirmDialog() {
5
+ const state = confirmDialogState.value
6
+
7
+ if (!state.isOpen) return null
8
+
9
+ const handleConfirm = () => {
10
+ state.onConfirm?.()
11
+ }
12
+
13
+ const handleCancel = () => {
14
+ state.onCancel?.()
15
+ }
16
+
17
+ const handleBackdropClick = () => {
18
+ handleCancel()
19
+ }
20
+
21
+ return (
22
+ <div
23
+ class="fixed inset-0 z-[2147483647] flex items-center justify-center bg-black/60 backdrop-blur-sm"
24
+ onClick={handleBackdropClick}
25
+ data-cms-ui
26
+ >
27
+ <div
28
+ class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] max-w-sm w-full border border-white/10 mx-4"
29
+ onClick={(e) => e.stopPropagation()}
30
+ data-cms-ui
31
+ >
32
+ {/* Header */}
33
+ <div class="p-5 pb-3">
34
+ <h2 class="text-lg font-semibold text-white">{state.title}</h2>
35
+ </div>
36
+
37
+ {/* Body */}
38
+ <div class="px-5 pb-5">
39
+ <p class="text-sm text-white/70 leading-relaxed">{state.message}</p>
40
+ </div>
41
+
42
+ {/* Footer */}
43
+ <div class="flex items-center justify-end gap-3 p-5 pt-4 border-t border-white/10 bg-white/5 rounded-b-cms-xl">
44
+ <button
45
+ type="button"
46
+ onClick={handleCancel}
47
+ 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 cursor-pointer"
48
+ data-cms-ui
49
+ >
50
+ {state.cancelLabel}
51
+ </button>
52
+ <button
53
+ type="button"
54
+ onClick={handleConfirm}
55
+ class={cn(
56
+ 'px-5 py-2.5 rounded-cms-pill text-sm font-medium transition-colors cursor-pointer',
57
+ state.variant === 'danger' && 'bg-cms-error text-white hover:bg-red-600',
58
+ state.variant === 'warning' && 'bg-amber-500 text-white hover:bg-amber-600',
59
+ state.variant === 'info' && 'bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover',
60
+ )}
61
+ data-cms-ui
62
+ >
63
+ {state.confirmLabel}
64
+ </button>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ )
69
+ }