@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,1351 @@
1
+ import { batch, computed, type Signal, signal } from '@preact/signals'
2
+ import { fetchManifest, getMarkdownContent } from './api'
3
+ import type { ToastMessage, ToastType } from './components/toast/types'
4
+ import { getConfig } from './config'
5
+ import type {
6
+ AIState,
7
+ AIStatusType,
8
+ AttributeEditorState,
9
+ BlockEditorState,
10
+ ChatMessage,
11
+ CmsConfig,
12
+ CmsManifest,
13
+ CmsSettings,
14
+ CollectionDefinition,
15
+ CollectionEntry,
16
+ CollectionsBrowserState,
17
+ ColorEditorState,
18
+ ComponentInstance,
19
+ ConfirmDialogState,
20
+ CreatePageState,
21
+ DeploymentState,
22
+ DeploymentStatusType,
23
+ EditorState,
24
+ FieldDefinition,
25
+ MarkdownEditorState,
26
+ MarkdownPageEntry,
27
+ MediaItem,
28
+ MediaLibraryState,
29
+ PendingAttributeChange,
30
+ PendingChange,
31
+ PendingColorChange,
32
+ PendingComponentInsert,
33
+ PendingImageChange,
34
+ PendingSeoChange,
35
+ SeoEditorState,
36
+ } from './types'
37
+
38
+ // ============================================================================
39
+ // Map Signal Helpers - reduces boilerplate for Map-based signals
40
+ // ============================================================================
41
+
42
+ interface MapSignalHelpers<T> {
43
+ set: (id: string, value: T) => void
44
+ update: (id: string, updater: (value: T) => T) => void
45
+ delete: (id: string) => void
46
+ clear: () => void
47
+ get: (id: string) => T | undefined
48
+ }
49
+
50
+ /**
51
+ * Creates helper functions for a Map-based signal.
52
+ * Reduces boilerplate for common Map operations.
53
+ */
54
+ function createMapHelpers<T>(mapSignal: Signal<Map<string, T>>): MapSignalHelpers<T> {
55
+ return {
56
+ set(id: string, value: T): void {
57
+ const newMap = new Map(mapSignal.value)
58
+ newMap.set(id, value)
59
+ mapSignal.value = newMap
60
+ },
61
+ update(id: string, updater: (value: T) => T): void {
62
+ const current = mapSignal.value.get(id)
63
+ if (current) {
64
+ const newMap = new Map(mapSignal.value)
65
+ newMap.set(id, updater(current))
66
+ mapSignal.value = newMap
67
+ }
68
+ },
69
+ delete(id: string): void {
70
+ const newMap = new Map(mapSignal.value)
71
+ newMap.delete(id)
72
+ mapSignal.value = newMap
73
+ },
74
+ clear(): void {
75
+ mapSignal.value = new Map()
76
+ },
77
+ get(id: string): T | undefined {
78
+ return mapSignal.value.get(id)
79
+ },
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Creates computed signals for tracking dirty items in a Map signal.
85
+ * Works with any type that has an `isDirty` property.
86
+ */
87
+ function createDirtyTracking<T extends { isDirty: boolean }>(
88
+ mapSignal: Signal<Map<string, T>>,
89
+ ) {
90
+ const dirtyCount = computed(() => {
91
+ return Array.from(mapSignal.value.values()).filter((c) => c.isDirty).length
92
+ })
93
+ const dirtyItems = computed(() => {
94
+ return Array.from(mapSignal.value.entries()).filter(([_, item]) => item.isDirty)
95
+ })
96
+ const hasDirty = computed(() => dirtyCount.value > 0)
97
+
98
+ return { dirtyCount, dirtyItems, hasDirty }
99
+ }
100
+
101
+ // Initial state factories
102
+ function createInitialAIState(): AIState {
103
+ return {
104
+ isPromptVisible: false,
105
+ isProcessing: false,
106
+ targetElementId: null,
107
+ streamingContent: null,
108
+ error: null,
109
+ isChatOpen: false,
110
+ chatMessages: [],
111
+ chatContextElementId: null,
112
+ currentStatus: null,
113
+ statusMessage: null,
114
+ }
115
+ }
116
+
117
+ function createInitialBlockEditorState(): BlockEditorState {
118
+ return {
119
+ isOpen: false,
120
+ currentComponentId: null,
121
+ mode: 'edit',
122
+ }
123
+ }
124
+
125
+ function createInitialMarkdownEditorState(): MarkdownEditorState {
126
+ return {
127
+ isOpen: false,
128
+ currentPage: null,
129
+ activeElementId: null,
130
+ mode: 'edit',
131
+ }
132
+ }
133
+
134
+ function createInitialMediaLibraryState(): MediaLibraryState {
135
+ return {
136
+ isOpen: false,
137
+ items: [],
138
+ isLoading: false,
139
+ selectedItem: null,
140
+ insertCallback: null,
141
+ }
142
+ }
143
+
144
+ function createInitialCreatePageState(): CreatePageState {
145
+ return {
146
+ isOpen: false,
147
+ isCreating: false,
148
+ selectedCollection: null,
149
+ }
150
+ }
151
+
152
+ function createInitialCollectionsBrowserState(): CollectionsBrowserState {
153
+ return {
154
+ isOpen: false,
155
+ selectedCollection: null,
156
+ }
157
+ }
158
+
159
+ function createInitialDeploymentState(): DeploymentState {
160
+ return {
161
+ status: null,
162
+ lastDeployedAt: null,
163
+ isPolling: false,
164
+ error: null,
165
+ }
166
+ }
167
+
168
+ function createInitialColorEditorState(): ColorEditorState {
169
+ return {
170
+ isOpen: false,
171
+ targetElementId: null,
172
+ targetRect: null,
173
+ }
174
+ }
175
+
176
+ function createInitialConfirmDialogState(): ConfirmDialogState {
177
+ return {
178
+ isOpen: false,
179
+ title: '',
180
+ message: '',
181
+ confirmLabel: 'Confirm',
182
+ cancelLabel: 'Cancel',
183
+ variant: 'info',
184
+ onConfirm: null,
185
+ onCancel: null,
186
+ }
187
+ }
188
+
189
+ function createInitialSettings(): CmsSettings {
190
+ return {
191
+ showEditableHighlights: false,
192
+ }
193
+ }
194
+
195
+ function createInitialSeoEditorState(): SeoEditorState {
196
+ return {
197
+ isOpen: false,
198
+ }
199
+ }
200
+
201
+ function createInitialAttributeEditorState(): AttributeEditorState {
202
+ return {
203
+ isOpen: false,
204
+ targetElementId: null,
205
+ targetRect: null,
206
+ }
207
+ }
208
+
209
+ // ============================================================================
210
+ // Core Editor State Signals
211
+ // ============================================================================
212
+
213
+ export const isEnabled = signal(false)
214
+ export const isEditing = signal(false)
215
+ export const isSaving = signal(false)
216
+ export const showingOriginal = signal(false)
217
+ export const currentEditingId = signal<string | null>(null)
218
+ export const currentComponentId = signal<string | null>(null)
219
+
220
+ // Complex state - use signals wrapping the full object for atomicity
221
+ export const pendingChanges = signal<Map<string, PendingChange>>(new Map())
222
+ export const pendingComponentChanges = signal<Map<string, ComponentInstance>>(
223
+ new Map(),
224
+ )
225
+ export const pendingInserts = signal<Map<string, PendingComponentInsert>>(
226
+ new Map(),
227
+ )
228
+ export const pendingImageChanges = signal<Map<string, PendingImageChange>>(
229
+ new Map(),
230
+ )
231
+ export const pendingColorChanges = signal<Map<string, PendingColorChange>>(
232
+ new Map(),
233
+ )
234
+ export const manifest = signal<CmsManifest>({
235
+ entries: {},
236
+ components: {},
237
+ componentDefinitions: {},
238
+ })
239
+
240
+ // Computed signal to get the current page's collection entry (if any)
241
+ export const currentPageCollection = computed((): CollectionEntry | null => {
242
+ const collections = manifest.value.collections
243
+ if (!collections || Object.keys(collections).length === 0) return null
244
+ // Return the first (and typically only) collection entry for the current page
245
+ const entries = Object.values(collections)
246
+ return entries.length > 0 ? entries[0]! : null
247
+ })
248
+
249
+ // Create helpers for Map signals (internal use)
250
+ const _pendingChangesHelpers = createMapHelpers(pendingChanges)
251
+ const _pendingComponentChangesHelpers = createMapHelpers(pendingComponentChanges)
252
+ const _pendingInsertsHelpers = createMapHelpers(pendingInserts)
253
+ const _pendingImageChangesHelpers = createMapHelpers(pendingImageChanges)
254
+ const _pendingColorChangesHelpers = createMapHelpers(pendingColorChanges)
255
+
256
+ // ============================================================================
257
+ // AI State Signals
258
+ // ============================================================================
259
+
260
+ export const aiState = signal<AIState>(createInitialAIState())
261
+
262
+ // Convenience computed signals for AI state
263
+ export const isAIProcessing = computed(() => aiState.value.isProcessing)
264
+ export const isChatOpen = computed(() => aiState.value.isChatOpen)
265
+ export const chatMessages = computed(() => aiState.value.chatMessages)
266
+ export const chatContextElementId = computed(
267
+ () => aiState.value.chatContextElementId,
268
+ )
269
+ export const currentStatus = computed(() => aiState.value.currentStatus)
270
+ export const statusMessage = computed(() => aiState.value.statusMessage)
271
+
272
+ // ============================================================================
273
+ // Block Editor State Signals
274
+ // ============================================================================
275
+
276
+ export const blockEditorState = signal<BlockEditorState>(
277
+ createInitialBlockEditorState(),
278
+ )
279
+
280
+ // Convenience computed signals for block editor
281
+ export const isBlockEditorOpen = computed(() => blockEditorState.value.isOpen)
282
+ export const blockEditorMode = computed(() => blockEditorState.value.mode)
283
+
284
+ // ============================================================================
285
+ // Markdown Editor State Signals
286
+ // ============================================================================
287
+
288
+ export const markdownEditorState = signal<MarkdownEditorState>(
289
+ createInitialMarkdownEditorState(),
290
+ )
291
+
292
+ // Convenience computed signals for markdown editor
293
+ export const isMarkdownEditorOpen = computed(
294
+ () => markdownEditorState.value.isOpen,
295
+ )
296
+ export const currentMarkdownPage = computed(
297
+ () => markdownEditorState.value.currentPage,
298
+ )
299
+ export const isMarkdownPreview = signal(false)
300
+
301
+ // ============================================================================
302
+ // Media Library State Signals
303
+ // ============================================================================
304
+
305
+ export const mediaLibraryState = signal<MediaLibraryState>(
306
+ createInitialMediaLibraryState(),
307
+ )
308
+
309
+ // Convenience computed signals for media library
310
+ export const isMediaLibraryOpen = computed(() => mediaLibraryState.value.isOpen)
311
+ export const mediaLibraryItems = computed(() => mediaLibraryState.value.items)
312
+ export const isMediaLibraryLoading = computed(
313
+ () => mediaLibraryState.value.isLoading,
314
+ )
315
+
316
+ // ============================================================================
317
+ // Create Page State Signals
318
+ // ============================================================================
319
+
320
+ export const createPageState = signal<CreatePageState>(
321
+ createInitialCreatePageState(),
322
+ )
323
+
324
+ // Convenience computed signals for create page
325
+ export const isCreatePageOpen = computed(() => createPageState.value.isOpen)
326
+ export const isCreatingPage = computed(() => createPageState.value.isCreating)
327
+ export const selectedCollection = computed(() => createPageState.value.selectedCollection)
328
+
329
+ // ============================================================================
330
+ // Collections Browser State Signals
331
+ // ============================================================================
332
+
333
+ export const collectionsBrowserState = signal<CollectionsBrowserState>(
334
+ createInitialCollectionsBrowserState(),
335
+ )
336
+
337
+ // Convenience computed signals for collections browser
338
+ export const isCollectionsBrowserOpen = computed(() => collectionsBrowserState.value.isOpen)
339
+ export const selectedBrowserCollection = computed(() => collectionsBrowserState.value.selectedCollection)
340
+
341
+ // ============================================================================
342
+ // Deployment State Signals
343
+ // ============================================================================
344
+
345
+ export const deploymentState = signal<DeploymentState>(
346
+ createInitialDeploymentState(),
347
+ )
348
+
349
+ // Convenience computed signals for deployment
350
+ export const deploymentStatus = computed(() => deploymentState.value.status)
351
+ export const isDeploymentPolling = computed(() => deploymentState.value.isPolling)
352
+ export const lastDeployedAt = computed(() => deploymentState.value.lastDeployedAt)
353
+
354
+ // ============================================================================
355
+ // Redirect Countdown State
356
+ // ============================================================================
357
+
358
+ export interface RedirectCountdownState {
359
+ url: string
360
+ label: string
361
+ secondsLeft: number
362
+ }
363
+
364
+ export const redirectCountdown = signal<RedirectCountdownState | null>(null)
365
+
366
+ let redirectTimer: ReturnType<typeof setInterval> | null = null
367
+
368
+ export function startRedirectCountdown(url: string, label: string, seconds = 10): void {
369
+ stopRedirectCountdown()
370
+ redirectCountdown.value = { url, label, secondsLeft: seconds }
371
+ redirectTimer = setInterval(() => {
372
+ const current = redirectCountdown.value
373
+ if (!current) {
374
+ stopRedirectCountdown()
375
+ return
376
+ }
377
+ if (current.secondsLeft <= 1) {
378
+ const targetUrl = current.url
379
+ stopRedirectCountdown()
380
+ window.location.href = targetUrl
381
+ } else {
382
+ redirectCountdown.value = { ...current, secondsLeft: current.secondsLeft - 1 }
383
+ }
384
+ }, 1000)
385
+ }
386
+
387
+ export function stopRedirectCountdown(): void {
388
+ if (redirectTimer) {
389
+ clearInterval(redirectTimer)
390
+ redirectTimer = null
391
+ }
392
+ redirectCountdown.value = null
393
+ }
394
+
395
+ // ============================================================================
396
+ // Color Editor State Signals
397
+ // ============================================================================
398
+
399
+ export const colorEditorState = signal<ColorEditorState>(
400
+ createInitialColorEditorState(),
401
+ )
402
+
403
+ // Convenience computed signals for color editor
404
+ export const isColorEditorOpen = computed(() => colorEditorState.value.isOpen)
405
+ export const colorEditorTargetId = computed(() => colorEditorState.value.targetElementId)
406
+
407
+ // ============================================================================
408
+ // Confirm Dialog State Signals
409
+ // ============================================================================
410
+
411
+ export const confirmDialogState = signal<ConfirmDialogState>(
412
+ createInitialConfirmDialogState(),
413
+ )
414
+
415
+ // Convenience computed signals for confirm dialog
416
+ export const isConfirmDialogOpen = computed(() => confirmDialogState.value.isOpen)
417
+
418
+ // ============================================================================
419
+ // Settings State Signals
420
+ // ============================================================================
421
+
422
+ export const settings = signal<CmsSettings>(createInitialSettings())
423
+
424
+ // Convenience computed signals for settings
425
+ export const showEditableHighlights = computed(() => settings.value.showEditableHighlights)
426
+
427
+ // ============================================================================
428
+ // SEO Editor State Signals
429
+ // ============================================================================
430
+
431
+ export const seoEditorState = signal<SeoEditorState>(createInitialSeoEditorState())
432
+ export const pendingSeoChanges = signal<Map<string, PendingSeoChange>>(new Map())
433
+
434
+ // Convenience computed signals for SEO editor
435
+ export const isSeoEditorOpen = computed(() => seoEditorState.value.isOpen)
436
+
437
+ // Create helpers for pending SEO changes
438
+ const _pendingSeoChangesHelpers = createMapHelpers(pendingSeoChanges)
439
+
440
+ // ============================================================================
441
+ // Attribute Editor State Signals
442
+ // ============================================================================
443
+
444
+ export const attributeEditorState = signal<AttributeEditorState>(createInitialAttributeEditorState())
445
+ export const pendingAttributeChanges = signal<Map<string, PendingAttributeChange>>(new Map())
446
+
447
+ // Convenience computed signals for attribute editor
448
+ export const isAttributeEditorOpen = computed(() => attributeEditorState.value.isOpen)
449
+ export const attributeEditorTargetId = computed(() => attributeEditorState.value.targetElementId)
450
+
451
+ // Create helpers for pending attribute changes
452
+ const _pendingAttributeChangesHelpers = createMapHelpers(pendingAttributeChanges)
453
+
454
+ // ============================================================================
455
+ // Swatch/Attribute Button Hover State Signals
456
+ // ============================================================================
457
+
458
+ /** True when user is hovering over color swatches */
459
+ export const isHoveringSwatches = signal(false)
460
+
461
+ /** True when user is hovering over attribute edit button */
462
+ export const isHoveringAttributeButton = signal(false)
463
+
464
+ /** Computed: true when hovering over any outline UI element (swatches, attr button) */
465
+ export const isHoveringOutlineUI = computed(() => isHoveringSwatches.value || isHoveringAttributeButton.value)
466
+
467
+ export function setHoveringSwatches(hovering: boolean): void {
468
+ isHoveringSwatches.value = hovering
469
+ }
470
+
471
+ export function setHoveringAttributeButton(hovering: boolean): void {
472
+ isHoveringAttributeButton.value = hovering
473
+ }
474
+
475
+ // ============================================================================
476
+ // Config Signal
477
+ // ============================================================================
478
+
479
+ export const config = signal<CmsConfig>(getConfig())
480
+
481
+ // ============================================================================
482
+ // Toast State
483
+ // ============================================================================
484
+
485
+ export const toasts = signal<ToastMessage[]>([])
486
+
487
+ // Counter for unique toast IDs (more reliable than Date.now())
488
+ let toastIdCounter = 0
489
+
490
+ // ============================================================================
491
+ // Computed Values - Dirty Tracking
492
+ // ============================================================================
493
+
494
+ // Use factory for dirty tracking to reduce duplication
495
+ const _pendingChangesDirty = createDirtyTracking(pendingChanges)
496
+ const _pendingImageChangesDirty = createDirtyTracking(pendingImageChanges)
497
+ const _pendingColorChangesDirty = createDirtyTracking(pendingColorChanges)
498
+ const _pendingSeoChangesDirty = createDirtyTracking(pendingSeoChanges)
499
+ const _pendingAttributeChangesDirty = createDirtyTracking(pendingAttributeChanges)
500
+
501
+ export const dirtyChangesCount = _pendingChangesDirty.dirtyCount
502
+ export const dirtyChanges = _pendingChangesDirty.dirtyItems
503
+ export const hasDirtyChanges = _pendingChangesDirty.hasDirty
504
+
505
+ export const dirtyImageChangesCount = _pendingImageChangesDirty.dirtyCount
506
+ export const dirtyImageChanges = _pendingImageChangesDirty.dirtyItems
507
+ export const hasDirtyImageChanges = _pendingImageChangesDirty.hasDirty
508
+
509
+ export const dirtyColorChangesCount = _pendingColorChangesDirty.dirtyCount
510
+ export const dirtyColorChanges = _pendingColorChangesDirty.dirtyItems
511
+ export const hasDirtyColorChanges = _pendingColorChangesDirty.hasDirty
512
+
513
+ export const dirtySeoChangesCount = _pendingSeoChangesDirty.dirtyCount
514
+ export const dirtySeoChanges = _pendingSeoChangesDirty.dirtyItems
515
+ export const hasDirtySeoChanges = _pendingSeoChangesDirty.hasDirty
516
+
517
+ export const dirtyAttributeChangesCount = _pendingAttributeChangesDirty.dirtyCount
518
+ export const dirtyAttributeChanges = _pendingAttributeChangesDirty.dirtyItems
519
+ export const hasDirtyAttributeChanges = _pendingAttributeChangesDirty.hasDirty
520
+
521
+ export const totalDirtyCount = computed(
522
+ () =>
523
+ dirtyChangesCount.value + dirtyImageChangesCount.value + dirtyColorChangesCount.value + dirtySeoChangesCount.value
524
+ + dirtyAttributeChangesCount.value,
525
+ )
526
+
527
+ export const hasAnyDirtyChanges = computed(
528
+ () =>
529
+ hasDirtyChanges.value || hasDirtyImageChanges.value || hasDirtyColorChanges.value || hasDirtySeoChanges.value || hasDirtyAttributeChanges.value,
530
+ )
531
+
532
+ // Navigation index for cycling through dirty elements
533
+ export const changeNavigationIndex = signal<number>(0)
534
+
535
+ // Combined list of all dirty elements for navigation
536
+ export const allDirtyElements = computed(() => {
537
+ const elements: Array<{ cmsId: string; element: HTMLElement; type: 'text' | 'image' | 'color' }> = []
538
+
539
+ dirtyChanges.value.forEach(([cmsId, change]) => {
540
+ elements.push({ cmsId, element: change.element, type: 'text' })
541
+ })
542
+ dirtyImageChanges.value.forEach(([cmsId, change]) => {
543
+ elements.push({ cmsId, element: change.element, type: 'image' })
544
+ })
545
+ dirtyColorChanges.value.forEach(([cmsId, change]) => {
546
+ elements.push({ cmsId, element: change.element, type: 'color' })
547
+ })
548
+
549
+ return elements
550
+ })
551
+
552
+ // ============================================================================
553
+ // State Mutation Functions
554
+ // ============================================================================
555
+
556
+ // Editor state mutations
557
+ export function setManifest(newManifest: CmsManifest): void {
558
+ manifest.value = newManifest
559
+ }
560
+
561
+ export function setEnabled(enabled: boolean): void {
562
+ isEnabled.value = enabled
563
+ }
564
+
565
+ export function setEditing(editing: boolean): void {
566
+ isEditing.value = editing
567
+ }
568
+
569
+ export function setShowingOriginal(showing: boolean): void {
570
+ showingOriginal.value = showing
571
+ }
572
+
573
+ export function setCurrentEditingId(id: string | null): void {
574
+ currentEditingId.value = id
575
+ }
576
+
577
+ export function setCurrentComponentId(componentId: string | null): void {
578
+ batch(() => {
579
+ currentComponentId.value = componentId
580
+ blockEditorState.value = {
581
+ ...blockEditorState.value,
582
+ currentComponentId: componentId,
583
+ }
584
+ })
585
+ }
586
+
587
+ // Pending changes mutations - using helpers
588
+ export const setPendingChange = _pendingChangesHelpers.set
589
+ export const updatePendingChange = _pendingChangesHelpers.update
590
+ export const deletePendingChange = _pendingChangesHelpers.delete
591
+ export const clearPendingChanges = _pendingChangesHelpers.clear
592
+ export const getPendingChange = _pendingChangesHelpers.get
593
+
594
+ // Component changes mutations - using helpers
595
+ export const setPendingComponentChange = _pendingComponentChangesHelpers.set
596
+ export const deletePendingComponentChange = _pendingComponentChangesHelpers.delete
597
+ export const clearPendingComponentChanges = _pendingComponentChangesHelpers.clear
598
+
599
+ // Insert mutations - using helpers
600
+ export const setPendingInsert = _pendingInsertsHelpers.set
601
+ export const deletePendingInsert = _pendingInsertsHelpers.delete
602
+ export const clearPendingInserts = _pendingInsertsHelpers.clear
603
+
604
+ // Image changes mutations - using helpers
605
+ export const setPendingImageChange = _pendingImageChangesHelpers.set
606
+ export const updatePendingImageChange = _pendingImageChangesHelpers.update
607
+ export const deletePendingImageChange = _pendingImageChangesHelpers.delete
608
+ export const clearPendingImageChanges = _pendingImageChangesHelpers.clear
609
+ export const getPendingImageChange = _pendingImageChangesHelpers.get
610
+
611
+ // Color changes mutations - using helpers
612
+ export const setPendingColorChange = _pendingColorChangesHelpers.set
613
+ export const updatePendingColorChange = _pendingColorChangesHelpers.update
614
+ export const deletePendingColorChange = _pendingColorChangesHelpers.delete
615
+ export const clearPendingColorChanges = _pendingColorChangesHelpers.clear
616
+ export const getPendingColorChange = _pendingColorChangesHelpers.get
617
+
618
+ // SEO changes mutations - using helpers
619
+ export const setPendingSeoChange = _pendingSeoChangesHelpers.set
620
+ export const updatePendingSeoChange = _pendingSeoChangesHelpers.update
621
+ export const deletePendingSeoChange = _pendingSeoChangesHelpers.delete
622
+ export const clearPendingSeoChanges = _pendingSeoChangesHelpers.clear
623
+ export const getPendingSeoChange = _pendingSeoChangesHelpers.get
624
+
625
+ // Attribute changes mutations - using helpers
626
+ export const setPendingAttributeChange = _pendingAttributeChangesHelpers.set
627
+ export const updatePendingAttributeChange = _pendingAttributeChangesHelpers.update
628
+ export const deletePendingAttributeChange = _pendingAttributeChangesHelpers.delete
629
+ export const clearPendingAttributeChanges = _pendingAttributeChangesHelpers.clear
630
+ export const getPendingAttributeChange = _pendingAttributeChangesHelpers.get
631
+
632
+ // ============================================================================
633
+ // AI State Mutations
634
+ // ============================================================================
635
+
636
+ export function setAIPromptVisible(visible: boolean): void {
637
+ aiState.value = { ...aiState.value, isPromptVisible: visible }
638
+ }
639
+
640
+ export function setAIProcessing(processing: boolean): void {
641
+ aiState.value = { ...aiState.value, isProcessing: processing }
642
+ }
643
+
644
+ export function setAIStatus(status: AIStatusType, message?: string): void {
645
+ aiState.value = {
646
+ ...aiState.value,
647
+ currentStatus: status,
648
+ statusMessage: message ?? null,
649
+ }
650
+ }
651
+
652
+ export function clearAIStatus(): void {
653
+ aiState.value = {
654
+ ...aiState.value,
655
+ currentStatus: null,
656
+ statusMessage: null,
657
+ }
658
+ }
659
+
660
+ export function setAITargetElement(elementId: string | null): void {
661
+ aiState.value = { ...aiState.value, targetElementId: elementId }
662
+ }
663
+
664
+ export function setAIStreamingContent(content: string | null): void {
665
+ aiState.value = { ...aiState.value, streamingContent: content }
666
+ }
667
+
668
+ export function setAIError(error: string | null): void {
669
+ aiState.value = { ...aiState.value, error: error }
670
+ }
671
+
672
+ export function resetAIState(): void {
673
+ aiState.value = createInitialAIState()
674
+ }
675
+
676
+ export function setAIChatOpen(open: boolean): void {
677
+ aiState.value = { ...aiState.value, isChatOpen: open }
678
+ }
679
+
680
+ export function addChatMessage(message: ChatMessage): void {
681
+ aiState.value = {
682
+ ...aiState.value,
683
+ chatMessages: [...aiState.value.chatMessages, message],
684
+ }
685
+ }
686
+
687
+ export function setChatMessages(messages: ChatMessage[]): void {
688
+ aiState.value = {
689
+ ...aiState.value,
690
+ chatMessages: messages,
691
+ }
692
+ }
693
+
694
+ export function updateChatMessage(messageId: string, content: string): void {
695
+ aiState.value = {
696
+ ...aiState.value,
697
+ chatMessages: aiState.value.chatMessages.map((msg) => msg.id === messageId ? { ...msg, content } : msg),
698
+ }
699
+ }
700
+
701
+ export function setChatContextElement(elementId: string | null): void {
702
+ aiState.value = { ...aiState.value, chatContextElementId: elementId }
703
+ }
704
+
705
+ export function clearChatMessages(): void {
706
+ aiState.value = { ...aiState.value, chatMessages: [] }
707
+ }
708
+
709
+ // ============================================================================
710
+ // Block Editor State Mutations
711
+ // ============================================================================
712
+
713
+ export function setBlockEditorOpen(open: boolean): void {
714
+ blockEditorState.value = { ...blockEditorState.value, isOpen: open }
715
+ }
716
+
717
+ export function setBlockEditorMode(mode: 'edit' | 'add' | 'picker'): void {
718
+ blockEditorState.value = { ...blockEditorState.value, mode }
719
+ }
720
+
721
+ export function resetBlockEditorState(): void {
722
+ blockEditorState.value = createInitialBlockEditorState()
723
+ }
724
+
725
+ // ============================================================================
726
+ // Markdown Editor State Mutations
727
+ // ============================================================================
728
+
729
+ export function setMarkdownEditorOpen(open: boolean): void {
730
+ markdownEditorState.value = { ...markdownEditorState.value, isOpen: open }
731
+ }
732
+
733
+ export function setMarkdownPage(page: MarkdownPageEntry | null): void {
734
+ markdownEditorState.value = { ...markdownEditorState.value, currentPage: page }
735
+ }
736
+
737
+ export function updateMarkdownContent(content: string): void {
738
+ if (markdownEditorState.value.currentPage) {
739
+ markdownEditorState.value = {
740
+ ...markdownEditorState.value,
741
+ currentPage: {
742
+ ...markdownEditorState.value.currentPage,
743
+ content,
744
+ isDirty: true,
745
+ },
746
+ }
747
+ }
748
+ }
749
+
750
+ export function setMarkdownActiveElement(elementId: string | null): void {
751
+ markdownEditorState.value = {
752
+ ...markdownEditorState.value,
753
+ activeElementId: elementId,
754
+ }
755
+ }
756
+
757
+ export function updateMarkdownFrontmatter(updates: Partial<import('./types').BlogFrontmatter>): void {
758
+ if (markdownEditorState.value.currentPage) {
759
+ markdownEditorState.value = {
760
+ ...markdownEditorState.value,
761
+ currentPage: {
762
+ ...markdownEditorState.value.currentPage,
763
+ frontmatter: {
764
+ ...markdownEditorState.value.currentPage.frontmatter,
765
+ ...updates,
766
+ },
767
+ isDirty: true,
768
+ },
769
+ }
770
+ }
771
+ }
772
+
773
+ export function resetMarkdownEditorState(): void {
774
+ markdownEditorState.value = createInitialMarkdownEditorState()
775
+ }
776
+
777
+ /**
778
+ * Parse a frontmatter value from string to its appropriate type.
779
+ * The manifest stores all values as strings, so we need to convert them back.
780
+ */
781
+ function parseFrontmatterValue(value: string): unknown {
782
+ // Handle booleans
783
+ if (value === 'true') return true
784
+ if (value === 'false') return false
785
+
786
+ // Handle numbers
787
+ const num = Number(value)
788
+ if (!Number.isNaN(num) && value.trim() !== '') {
789
+ return num
790
+ }
791
+
792
+ // Handle arrays (simple comma-separated for now)
793
+ if (value.startsWith('[') && value.endsWith(']')) {
794
+ try {
795
+ return JSON.parse(value)
796
+ } catch {
797
+ // Not valid JSON, return as string
798
+ }
799
+ }
800
+
801
+ // Return as string (already the default)
802
+ return value
803
+ }
804
+
805
+ /**
806
+ * Open the markdown editor for the current page's collection entry.
807
+ * Refreshes the manifest first to ensure we have the latest content.
808
+ */
809
+ export async function openMarkdownEditorForCurrentPage(): Promise<boolean> {
810
+ // Refresh manifest to get the latest content
811
+ try {
812
+ const newManifest = await fetchManifest()
813
+ setManifest(newManifest)
814
+ } catch (err) {
815
+ console.error('[CMS] Failed to refresh manifest:', err)
816
+ // Continue with current manifest if refresh fails
817
+ }
818
+
819
+ const collection = currentPageCollection.value
820
+ if (!collection) {
821
+ return false
822
+ }
823
+
824
+ // Fetch the actual markdown content via the API to get properly parsed frontmatter.
825
+ // The manifest's naive YAML parsing corrupts block scalars (e.g. `description: >-`).
826
+ let frontmatter: Record<string, unknown>
827
+ let content: string
828
+ try {
829
+ const result = await getMarkdownContent(config.value.apiBase, collection.sourcePath)
830
+ if (result) {
831
+ frontmatter = result.frontmatter as Record<string, unknown>
832
+ content = result.content
833
+ } else {
834
+ throw new Error('API returned null')
835
+ }
836
+ } catch {
837
+ // Fall back to manifest data if the API call fails
838
+ frontmatter = {}
839
+ for (const [key, data] of Object.entries(collection.frontmatter)) {
840
+ frontmatter[key] = parseFrontmatterValue(data.value)
841
+ }
842
+ content = collection.body
843
+ }
844
+
845
+ // Look up collection definition for schema-aware field rendering
846
+ const collectionDefinition = manifest.value.collectionDefinitions?.[collection.collectionName]
847
+
848
+ markdownEditorState.value = {
849
+ isOpen: true,
850
+ currentPage: {
851
+ filePath: collection.sourcePath,
852
+ slug: collection.collectionSlug,
853
+ frontmatter: frontmatter as import('./types').BlogFrontmatter,
854
+ content,
855
+ isDirty: false,
856
+ },
857
+ activeElementId: collection.wrapperId ?? null,
858
+ mode: 'edit',
859
+ collectionDefinition,
860
+ }
861
+ return true
862
+ }
863
+
864
+ /**
865
+ * Open the markdown editor in "create" mode for a new page in the given collection.
866
+ * Builds initial frontmatter from the collection's field definitions.
867
+ */
868
+ export function openMarkdownEditorForNewPage(
869
+ collectionName: string,
870
+ collectionDefinition: CollectionDefinition,
871
+ ): void {
872
+ // Build initial frontmatter from field definitions
873
+ const initialFrontmatter: Record<string, unknown> = {}
874
+ for (const field of collectionDefinition.fields) {
875
+ if (field.name === 'title') continue // title handled separately via the header
876
+ if (field.defaultValue !== undefined) {
877
+ initialFrontmatter[field.name] = field.defaultValue
878
+ } else {
879
+ initialFrontmatter[field.name] = getDefaultForFieldType(field)
880
+ }
881
+ }
882
+
883
+ markdownEditorState.value = {
884
+ isOpen: true,
885
+ currentPage: {
886
+ filePath: '',
887
+ slug: '',
888
+ frontmatter: { title: '', ...initialFrontmatter },
889
+ content: '',
890
+ isDirty: false,
891
+ },
892
+ activeElementId: null,
893
+ mode: 'create',
894
+ collectionDefinition,
895
+ createOptions: {
896
+ collectionName,
897
+ collectionDefinition,
898
+ },
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Get a sensible default value for a field based on its type definition.
904
+ */
905
+ function getDefaultForFieldType(field: FieldDefinition): unknown {
906
+ switch (field.type) {
907
+ case 'boolean':
908
+ return false
909
+ case 'number':
910
+ return 0
911
+ case 'array':
912
+ return []
913
+ case 'date':
914
+ return new Date().toISOString().split('T')[0]
915
+ default:
916
+ return ''
917
+ }
918
+ }
919
+
920
+ // ============================================================================
921
+ // Media Library State Mutations
922
+ // ============================================================================
923
+
924
+ export function setMediaLibraryOpen(open: boolean): void {
925
+ mediaLibraryState.value = { ...mediaLibraryState.value, isOpen: open }
926
+ }
927
+
928
+ export function setMediaLibraryItems(items: MediaItem[]): void {
929
+ mediaLibraryState.value = { ...mediaLibraryState.value, items }
930
+ }
931
+
932
+ export function setMediaLibraryLoading(loading: boolean): void {
933
+ mediaLibraryState.value = { ...mediaLibraryState.value, isLoading: loading }
934
+ }
935
+
936
+ export function setMediaLibrarySelectedItem(item: MediaItem | null): void {
937
+ mediaLibraryState.value = { ...mediaLibraryState.value, selectedItem: item }
938
+ }
939
+
940
+ export function setMediaLibraryInsertCallback(
941
+ callback: ((url: string, alt: string) => void) | null,
942
+ ): void {
943
+ mediaLibraryState.value = { ...mediaLibraryState.value, insertCallback: callback }
944
+ }
945
+
946
+ export function openMediaLibraryWithCallback(
947
+ callback: (url: string, alt: string) => void,
948
+ ): void {
949
+ mediaLibraryState.value = {
950
+ ...mediaLibraryState.value,
951
+ isOpen: true,
952
+ insertCallback: callback,
953
+ }
954
+ }
955
+
956
+ export function resetMediaLibraryState(): void {
957
+ mediaLibraryState.value = createInitialMediaLibraryState()
958
+ }
959
+
960
+ // ============================================================================
961
+ // Create Page State Mutations
962
+ // ============================================================================
963
+
964
+ export function setCreatePageOpen(open: boolean): void {
965
+ createPageState.value = { ...createPageState.value, isOpen: open }
966
+ }
967
+
968
+ export function setCreatingPage(creating: boolean): void {
969
+ createPageState.value = { ...createPageState.value, isCreating: creating }
970
+ }
971
+
972
+ export function setSelectedCollection(collection: string | null): void {
973
+ createPageState.value = { ...createPageState.value, selectedCollection: collection }
974
+ }
975
+
976
+ export function resetCreatePageState(): void {
977
+ createPageState.value = createInitialCreatePageState()
978
+ }
979
+
980
+ // ============================================================================
981
+ // Collections Browser State Mutations
982
+ // ============================================================================
983
+
984
+ export function openCollectionsBrowser(): void {
985
+ collectionsBrowserState.value = { isOpen: true, selectedCollection: null }
986
+ }
987
+
988
+ export function selectBrowserCollection(name: string | null): void {
989
+ collectionsBrowserState.value = { ...collectionsBrowserState.value, selectedCollection: name }
990
+ }
991
+
992
+ export function closeCollectionsBrowser(): void {
993
+ collectionsBrowserState.value = createInitialCollectionsBrowserState()
994
+ }
995
+
996
+ /**
997
+ * Open the markdown editor for an existing collection entry.
998
+ * Fetches markdown content via the API and opens in edit mode.
999
+ */
1000
+ export async function openMarkdownEditorForEntry(
1001
+ collectionName: string,
1002
+ slug: string,
1003
+ sourcePath: string,
1004
+ collectionDefinition: CollectionDefinition,
1005
+ ): Promise<void> {
1006
+ let frontmatter: Record<string, unknown> = {}
1007
+ let content = ''
1008
+
1009
+ try {
1010
+ const result = await getMarkdownContent(config.value.apiBase, sourcePath)
1011
+ if (result) {
1012
+ frontmatter = result.frontmatter as Record<string, unknown>
1013
+ content = result.content
1014
+ }
1015
+ } catch (err) {
1016
+ console.error('[CMS] Failed to fetch markdown content for entry:', err)
1017
+ }
1018
+
1019
+ markdownEditorState.value = {
1020
+ isOpen: true,
1021
+ currentPage: {
1022
+ filePath: sourcePath,
1023
+ slug,
1024
+ frontmatter: frontmatter as import('./types').BlogFrontmatter,
1025
+ content,
1026
+ isDirty: false,
1027
+ },
1028
+ activeElementId: null,
1029
+ mode: 'edit',
1030
+ collectionDefinition,
1031
+ }
1032
+ }
1033
+
1034
+ // ============================================================================
1035
+ // Deployment State Mutations
1036
+ // ============================================================================
1037
+
1038
+ export function setDeploymentStatus(status: DeploymentStatusType | null): void {
1039
+ deploymentState.value = { ...deploymentState.value, status }
1040
+ }
1041
+
1042
+ export function setDeploymentPolling(isPolling: boolean): void {
1043
+ deploymentState.value = { ...deploymentState.value, isPolling }
1044
+ }
1045
+
1046
+ export function setLastDeployedAt(timestamp: string | null): void {
1047
+ deploymentState.value = { ...deploymentState.value, lastDeployedAt: timestamp }
1048
+ }
1049
+
1050
+ export function setDeploymentError(error: string | null): void {
1051
+ deploymentState.value = { ...deploymentState.value, error }
1052
+ }
1053
+
1054
+ export function updateDeploymentState(update: Partial<DeploymentState>): void {
1055
+ deploymentState.value = { ...deploymentState.value, ...update }
1056
+ }
1057
+
1058
+ export function resetDeploymentState(): void {
1059
+ deploymentState.value = createInitialDeploymentState()
1060
+ }
1061
+
1062
+ // ============================================================================
1063
+ // Color Editor State Mutations
1064
+ // ============================================================================
1065
+
1066
+ export function setColorEditorOpen(open: boolean): void {
1067
+ colorEditorState.value = { ...colorEditorState.value, isOpen: open }
1068
+ }
1069
+
1070
+ export function setColorEditorTarget(elementId: string | null, rect: DOMRect | null): void {
1071
+ colorEditorState.value = {
1072
+ ...colorEditorState.value,
1073
+ targetElementId: elementId,
1074
+ targetRect: rect,
1075
+ }
1076
+ }
1077
+
1078
+ export function openColorEditor(elementId: string, rect: DOMRect): void {
1079
+ // Close attribute editor when opening color editor
1080
+ attributeEditorState.value = createInitialAttributeEditorState()
1081
+
1082
+ colorEditorState.value = {
1083
+ isOpen: true,
1084
+ targetElementId: elementId,
1085
+ targetRect: rect,
1086
+ }
1087
+ }
1088
+
1089
+ export function closeColorEditor(): void {
1090
+ colorEditorState.value = createInitialColorEditorState()
1091
+ }
1092
+
1093
+ export function resetColorEditorState(): void {
1094
+ colorEditorState.value = createInitialColorEditorState()
1095
+ }
1096
+
1097
+ // ============================================================================
1098
+ // Confirm Dialog State Mutations
1099
+ // ============================================================================
1100
+
1101
+ export interface ShowConfirmOptions {
1102
+ title?: string
1103
+ message: string
1104
+ confirmLabel?: string
1105
+ cancelLabel?: string
1106
+ variant?: 'danger' | 'warning' | 'info'
1107
+ }
1108
+
1109
+ export function showConfirmDialog(
1110
+ options: ShowConfirmOptions,
1111
+ ): Promise<boolean> {
1112
+ return new Promise((resolve) => {
1113
+ confirmDialogState.value = {
1114
+ isOpen: true,
1115
+ title: options.title ?? 'Confirm',
1116
+ message: options.message,
1117
+ confirmLabel: options.confirmLabel ?? 'Confirm',
1118
+ cancelLabel: options.cancelLabel ?? 'Cancel',
1119
+ variant: options.variant ?? 'info',
1120
+ onConfirm: () => {
1121
+ closeConfirmDialog()
1122
+ resolve(true)
1123
+ },
1124
+ onCancel: () => {
1125
+ closeConfirmDialog()
1126
+ resolve(false)
1127
+ },
1128
+ }
1129
+ })
1130
+ }
1131
+
1132
+ export function closeConfirmDialog(): void {
1133
+ confirmDialogState.value = createInitialConfirmDialogState()
1134
+ }
1135
+
1136
+ // ============================================================================
1137
+ // Settings State Mutations
1138
+ // ============================================================================
1139
+
1140
+ export function setShowEditableHighlights(show: boolean): void {
1141
+ settings.value = { ...settings.value, showEditableHighlights: show }
1142
+ }
1143
+
1144
+ export function toggleShowEditableHighlights(): void {
1145
+ settings.value = { ...settings.value, showEditableHighlights: !settings.value.showEditableHighlights }
1146
+ }
1147
+
1148
+ export function updateSettings(update: Partial<CmsSettings>): void {
1149
+ settings.value = { ...settings.value, ...update }
1150
+ }
1151
+
1152
+ export function resetSettings(): void {
1153
+ settings.value = createInitialSettings()
1154
+ }
1155
+
1156
+ // ============================================================================
1157
+ // SEO Editor State Mutations
1158
+ // ============================================================================
1159
+
1160
+ export function setSeoEditorOpen(open: boolean): void {
1161
+ seoEditorState.value = { ...seoEditorState.value, isOpen: open }
1162
+ }
1163
+
1164
+ export function openSeoEditor(): void {
1165
+ seoEditorState.value = { isOpen: true }
1166
+ }
1167
+
1168
+ export function closeSeoEditor(): void {
1169
+ seoEditorState.value = createInitialSeoEditorState()
1170
+ }
1171
+
1172
+ export function resetSeoEditorState(): void {
1173
+ seoEditorState.value = createInitialSeoEditorState()
1174
+ pendingSeoChanges.value = new Map()
1175
+ }
1176
+
1177
+ // ============================================================================
1178
+ // Attribute Editor State Mutations
1179
+ // ============================================================================
1180
+
1181
+ export function setAttributeEditorOpen(open: boolean): void {
1182
+ attributeEditorState.value = { ...attributeEditorState.value, isOpen: open }
1183
+ }
1184
+
1185
+ export function setAttributeEditorTarget(elementId: string | null, rect: DOMRect | null): void {
1186
+ attributeEditorState.value = {
1187
+ ...attributeEditorState.value,
1188
+ targetElementId: elementId,
1189
+ targetRect: rect,
1190
+ }
1191
+ }
1192
+
1193
+ export function openAttributeEditor(elementId: string, rect: DOMRect): void {
1194
+ // Close color editor when opening attribute editor
1195
+ colorEditorState.value = createInitialColorEditorState()
1196
+
1197
+ // Ensure pending attribute change exists for this element
1198
+ if (!pendingAttributeChanges.value.has(elementId)) {
1199
+ const manifestEntry = manifest.value.entries[elementId]
1200
+ if (manifestEntry?.attributes && Object.keys(manifestEntry.attributes).length > 0) {
1201
+ // Deep copy the flat attributes map
1202
+ const originalAttributes: Record<string, import('./types').Attribute> = {}
1203
+ const newAttributes: Record<string, import('./types').Attribute> = {}
1204
+ for (const [key, attr] of Object.entries(manifestEntry.attributes)) {
1205
+ originalAttributes[key] = { ...attr }
1206
+ newAttributes[key] = { ...attr }
1207
+ }
1208
+
1209
+ // Find the element in the DOM
1210
+ const element = document.querySelector(`[data-cms-id="${elementId}"]`) as HTMLElement | null
1211
+
1212
+ if (element) {
1213
+ const newMap = new Map(pendingAttributeChanges.value)
1214
+ newMap.set(elementId, {
1215
+ element,
1216
+ cmsId: elementId,
1217
+ originalAttributes,
1218
+ newAttributes,
1219
+ isDirty: false,
1220
+ })
1221
+ pendingAttributeChanges.value = newMap
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ attributeEditorState.value = {
1227
+ isOpen: true,
1228
+ targetElementId: elementId,
1229
+ targetRect: rect,
1230
+ }
1231
+ }
1232
+
1233
+ export function closeAttributeEditor(): void {
1234
+ attributeEditorState.value = createInitialAttributeEditorState()
1235
+ }
1236
+
1237
+ export function resetAttributeEditorState(): void {
1238
+ attributeEditorState.value = createInitialAttributeEditorState()
1239
+ pendingAttributeChanges.value = new Map()
1240
+ }
1241
+
1242
+ // ============================================================================
1243
+ // Toast Mutations
1244
+ // ============================================================================
1245
+
1246
+ export function showToast(message: string, type: ToastType = 'info'): string {
1247
+ const id = `toast-${++toastIdCounter}`
1248
+ toasts.value = [...toasts.value, { id, message, type }]
1249
+ return id
1250
+ }
1251
+
1252
+ export function removeToast(id: string): void {
1253
+ toasts.value = toasts.value.filter((t) => t.id !== id)
1254
+ }
1255
+
1256
+ // ============================================================================
1257
+ // Config Mutations
1258
+ // ============================================================================
1259
+
1260
+ export function setConfig(newConfig: CmsConfig): void {
1261
+ config.value = newConfig
1262
+ }
1263
+
1264
+ // ============================================================================
1265
+ // Change Navigation Mutations
1266
+ // ============================================================================
1267
+
1268
+ export function navigateToNextChange(): void {
1269
+ const elements = allDirtyElements.value
1270
+ if (elements.length === 0) return
1271
+
1272
+ const nextIndex = (changeNavigationIndex.value + 1) % elements.length
1273
+ changeNavigationIndex.value = nextIndex
1274
+
1275
+ const target = elements[nextIndex]
1276
+ if (!target?.element) return
1277
+
1278
+ target.element.scrollIntoView({ behavior: 'smooth', block: 'center' })
1279
+
1280
+ if (target.type === 'text') {
1281
+ target.element.focus()
1282
+ }
1283
+ setCurrentEditingId(target.cmsId)
1284
+ }
1285
+
1286
+ export function resetChangeNavigationIndex(): void {
1287
+ changeNavigationIndex.value = 0
1288
+ }
1289
+
1290
+ // ============================================================================
1291
+ // Legacy Compatibility Layer
1292
+ // ============================================================================
1293
+
1294
+ /**
1295
+ * Get a snapshot of the current state for legacy code paths.
1296
+ * Prefer using individual signals directly when possible.
1297
+ */
1298
+ export function getStateSnapshot(): EditorState {
1299
+ return {
1300
+ isEnabled: isEnabled.value,
1301
+ isEditing: isEditing.value,
1302
+ showingOriginal: showingOriginal.value,
1303
+ currentEditingId: currentEditingId.value,
1304
+ currentComponentId: currentComponentId.value,
1305
+ pendingChanges: pendingChanges.value,
1306
+ pendingComponentChanges: pendingComponentChanges.value,
1307
+ pendingInserts: pendingInserts.value,
1308
+ manifest: manifest.value,
1309
+ ai: aiState.value,
1310
+ blockEditor: blockEditorState.value,
1311
+ }
1312
+ }
1313
+
1314
+ /**
1315
+ * Batch multiple state updates for performance.
1316
+ * Use this when updating multiple signals at once.
1317
+ */
1318
+ export { batch }
1319
+
1320
+ /**
1321
+ * Reset all state to initial values.
1322
+ */
1323
+ export function resetAllState(): void {
1324
+ batch(() => {
1325
+ isEnabled.value = false
1326
+ isEditing.value = false
1327
+ showingOriginal.value = false
1328
+ currentEditingId.value = null
1329
+ currentComponentId.value = null
1330
+ pendingChanges.value = new Map()
1331
+ pendingComponentChanges.value = new Map()
1332
+ pendingInserts.value = new Map()
1333
+ pendingImageChanges.value = new Map()
1334
+ pendingColorChanges.value = new Map()
1335
+ pendingSeoChanges.value = new Map()
1336
+ pendingAttributeChanges.value = new Map()
1337
+ manifest.value = { entries: {}, components: {}, componentDefinitions: {} }
1338
+ aiState.value = createInitialAIState()
1339
+ blockEditorState.value = createInitialBlockEditorState()
1340
+ markdownEditorState.value = createInitialMarkdownEditorState()
1341
+ mediaLibraryState.value = createInitialMediaLibraryState()
1342
+ createPageState.value = createInitialCreatePageState()
1343
+ collectionsBrowserState.value = createInitialCollectionsBrowserState()
1344
+ deploymentState.value = createInitialDeploymentState()
1345
+ colorEditorState.value = createInitialColorEditorState()
1346
+ confirmDialogState.value = createInitialConfirmDialogState()
1347
+ seoEditorState.value = createInitialSeoEditorState()
1348
+ attributeEditorState.value = createInitialAttributeEditorState()
1349
+ toasts.value = []
1350
+ })
1351
+ }