@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,505 @@
1
+ import { type Editor, editorViewCtx } from '@milkdown/core'
2
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
3
+ import { updateMarkdownPage } from '../api'
4
+ import { STORAGE_KEYS } from '../constants'
5
+ import { startDeploymentPolling } from '../editor'
6
+ import { createMarkdownPage } from '../markdown-api'
7
+ import {
8
+ config,
9
+ currentMarkdownPage,
10
+ isMarkdownPreview,
11
+ markdownEditorState,
12
+ resetMarkdownEditorState,
13
+ showToast,
14
+ startRedirectCountdown,
15
+ updateMarkdownFrontmatter,
16
+ } from '../signals'
17
+ import { CreateModeFrontmatter, EditModeFrontmatter, slugify } from './frontmatter-fields'
18
+ import { MarkdownInlineEditor } from './markdown-inline-editor'
19
+
20
+ /**
21
+ * Wrapper component that renders the editor in place of markdown content.
22
+ * Supports both "edit" mode (existing page) and "create" mode (new page).
23
+ */
24
+ export function MarkdownEditorOverlay() {
25
+ const page = currentMarkdownPage.value
26
+ const editorState = markdownEditorState.value
27
+ const isCreateMode = editorState.mode === 'create'
28
+ const createOptions = editorState.createOptions
29
+ const collectionDef = editorState.collectionDefinition
30
+
31
+ const [isSaving, setIsSaving] = useState(false)
32
+ const [showFrontmatter, setShowFrontmatter] = useState(isCreateMode)
33
+ // Track whether the user has manually edited the slug (disables auto-slug from title)
34
+ const [slugManuallyEdited, setSlugManuallyEdited] = useState(false)
35
+ // Preview mode state
36
+ const [isPreview, setIsPreview] = useState(false)
37
+ const originalHTMLRef = useRef<string | null>(null)
38
+ const editorInstanceRef = useRef<Editor | null>(null)
39
+
40
+ // Open metadata by default when entering create mode
41
+ useEffect(() => {
42
+ if (isCreateMode) {
43
+ setShowFrontmatter(true)
44
+ }
45
+ }, [isCreateMode])
46
+
47
+ const handleDeploymentComplete = useCallback(
48
+ (status: 'completed' | 'failed' | 'timeout') => {
49
+ if (status === 'failed') {
50
+ showToast('Deployment failed', 'error')
51
+ }
52
+ },
53
+ [],
54
+ )
55
+
56
+ const restoreOriginalHTML = useCallback(() => {
57
+ const activeId = markdownEditorState.value.activeElementId
58
+ if (originalHTMLRef.current !== null && activeId) {
59
+ const el = document.querySelector(`[data-cms-id="${activeId}"]`)
60
+ if (el) {
61
+ el.innerHTML = originalHTMLRef.current
62
+ }
63
+ originalHTMLRef.current = null
64
+ }
65
+ }, [])
66
+
67
+ const handleSave = useCallback(
68
+ async (content: string) => {
69
+ if (isSaving) return
70
+ const currentPage = currentMarkdownPage.value
71
+ if (!currentPage) return
72
+
73
+ setIsSaving(true)
74
+ try {
75
+ const result = await updateMarkdownPage(config.value.apiBase, {
76
+ filePath: currentPage.filePath,
77
+ content,
78
+ frontmatter: currentPage.frontmatter,
79
+ })
80
+
81
+ if (result.success) {
82
+ // Keep the preview HTML in place so user sees changes immediately
83
+ // If not in preview mode, inject editor HTML into the page element
84
+ const activeId = markdownEditorState.value.activeElementId
85
+ if (activeId && editorInstanceRef.current && !isPreview) {
86
+ const el = document.querySelector(
87
+ `[data-cms-id="${activeId}"]`,
88
+ )
89
+ if (el) {
90
+ try {
91
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
92
+ el.innerHTML = view.dom.innerHTML
93
+ } catch {
94
+ // If we can't get editor HTML, leave the page as-is
95
+ }
96
+ }
97
+ }
98
+ // Clear the original ref without restoring — we want to keep the new content visible
99
+ originalHTMLRef.current = null
100
+ setIsPreview(false)
101
+ isMarkdownPreview.value = false
102
+ setIsSaving(false)
103
+
104
+ showToast('Content saved, deploying...', 'success')
105
+ // Clear pending entry navigation so editor doesn't auto-open after save
106
+ sessionStorage.removeItem(STORAGE_KEYS.PENDING_ENTRY_NAVIGATION)
107
+ // Close the editor immediately — the toolbar will show deploying state
108
+ resetMarkdownEditorState()
109
+ startDeploymentPolling(config.value, {
110
+ onComplete: handleDeploymentComplete,
111
+ })
112
+ } else {
113
+ showToast(result.error || 'Failed to save markdown', 'error')
114
+ setIsSaving(false)
115
+ }
116
+ } catch (error) {
117
+ const message = error instanceof Error ? error.message : 'Unknown error'
118
+ showToast(`Save failed: ${message}`, 'error')
119
+ setIsSaving(false)
120
+ }
121
+ },
122
+ [isSaving, isPreview, handleDeploymentComplete],
123
+ )
124
+
125
+ const handleCreate = useCallback(async () => {
126
+ if (isSaving) return
127
+ const currentPage = currentMarkdownPage.value
128
+ const opts = markdownEditorState.value.createOptions
129
+ if (!currentPage || !opts) return
130
+
131
+ const title = (currentPage.frontmatter.title as string) || ''
132
+ if (!title.trim()) {
133
+ showToast('Please enter a title', 'error')
134
+ return
135
+ }
136
+
137
+ const slug = currentPage.slug || slugify(title)
138
+ if (!slug) {
139
+ showToast('Please enter a slug', 'error')
140
+ return
141
+ }
142
+
143
+ setIsSaving(true)
144
+ try {
145
+ // Build frontmatter excluding title and slug
146
+ const frontmatter: Record<string, unknown> = {}
147
+ for (const [key, value] of Object.entries(currentPage.frontmatter)) {
148
+ if (key !== 'title' && value !== undefined && value !== '') {
149
+ frontmatter[key] = value
150
+ }
151
+ }
152
+
153
+ const result = await createMarkdownPage(config.value, {
154
+ collection: opts.collectionName,
155
+ title: title.trim(),
156
+ slug,
157
+ frontmatter,
158
+ content: currentPage.content || '',
159
+ })
160
+
161
+ if (result.success) {
162
+ // Derive the new page URL from existing collection entry pathnames
163
+ const entries = opts.collectionDefinition.entries ?? []
164
+ const existingEntry = entries.find((e) => e.pathname)
165
+ let redirectUrl: string | undefined
166
+ if (existingEntry?.pathname) {
167
+ // Extract base path from an existing entry pathname (e.g., "/blog/first-post" → "/blog/")
168
+ const lastSlash = existingEntry.pathname.lastIndexOf('/')
169
+ if (lastSlash >= 0) {
170
+ redirectUrl = existingEntry.pathname.slice(0, lastSlash + 1) + slug
171
+ }
172
+ }
173
+
174
+ showToast('Page created, deploying...', 'success')
175
+ resetMarkdownEditorState()
176
+ startDeploymentPolling(config.value, {
177
+ onComplete: (status) => {
178
+ handleDeploymentComplete(status)
179
+ if (status === 'completed' && redirectUrl) {
180
+ startRedirectCountdown(redirectUrl, title.trim())
181
+ }
182
+ },
183
+ })
184
+ } else {
185
+ showToast(result.error || 'Failed to create page', 'error')
186
+ }
187
+ } catch (error) {
188
+ const message = error instanceof Error ? error.message : 'Unknown error'
189
+ showToast(`Create failed: ${message}`, 'error')
190
+ } finally {
191
+ setIsSaving(false)
192
+ }
193
+ }, [isSaving, handleDeploymentComplete])
194
+
195
+ const handlePreview = useCallback(() => {
196
+ const activeId = markdownEditorState.value.activeElementId
197
+ if (!editorInstanceRef.current || !activeId) return
198
+
199
+ if (!isPreview) {
200
+ // Enter preview
201
+ const el = document.querySelector(`[data-cms-id="${activeId}"]`)
202
+ if (!el) {
203
+ showToast('Could not find page element to preview', 'error')
204
+ return
205
+ }
206
+ originalHTMLRef.current = el.innerHTML
207
+ try {
208
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
209
+ el.innerHTML = view.dom.innerHTML
210
+ } catch (error) {
211
+ console.error('Failed to get editor HTML for preview:', error)
212
+ originalHTMLRef.current = null
213
+ showToast('Failed to generate preview', 'error')
214
+ return
215
+ }
216
+ setIsPreview(true)
217
+ isMarkdownPreview.value = true
218
+ } else {
219
+ // Exit preview
220
+ restoreOriginalHTML()
221
+ setIsPreview(false)
222
+ isMarkdownPreview.value = false
223
+ }
224
+ }, [isPreview, restoreOriginalHTML])
225
+
226
+ const handleCancel = useCallback(() => {
227
+ restoreOriginalHTML()
228
+ isMarkdownPreview.value = false
229
+ resetMarkdownEditorState()
230
+ }, [restoreOriginalHTML])
231
+
232
+ if (!page) return null
233
+
234
+ const stopPropagation = (e: Event) => e.stopPropagation()
235
+ const collectionLabel = createOptions?.collectionDefinition.label ?? 'Page'
236
+
237
+ if (isPreview) {
238
+ return (
239
+ <div
240
+ class="fixed bottom-6 left-1/2 -translate-x-1/2 z-2147483647 flex items-center gap-3 px-5 py-3 bg-cms-dark/95 border border-white/15 rounded-cms-pill shadow-[0_8px_32px_rgba(0,0,0,0.4)] backdrop-blur-md"
241
+ data-cms-ui
242
+ onMouseDown={stopPropagation}
243
+ onClick={stopPropagation}
244
+ >
245
+ <div class="flex items-center gap-2 text-white/70">
246
+ <svg
247
+ class="w-4 h-4"
248
+ fill="none"
249
+ stroke="currentColor"
250
+ viewBox="0 0 24 24"
251
+ stroke-width="2"
252
+ >
253
+ <path
254
+ stroke-linecap="round"
255
+ stroke-linejoin="round"
256
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
257
+ />
258
+ <path
259
+ stroke-linecap="round"
260
+ stroke-linejoin="round"
261
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
262
+ />
263
+ </svg>
264
+ <span class="text-sm font-medium">Previewing</span>
265
+ </div>
266
+ <div class="w-px h-5 bg-white/20" />
267
+ <button
268
+ type="button"
269
+ onClick={handlePreview}
270
+ class="px-3 py-1.5 text-sm text-white/80 hover:text-white hover:bg-white/10 rounded-cms-pill transition-colors"
271
+ data-cms-ui
272
+ >
273
+ Back to Editor
274
+ </button>
275
+ <button
276
+ type="button"
277
+ onClick={() => {
278
+ const currentContent = currentMarkdownPage.value?.content
279
+ if (currentContent !== undefined) {
280
+ handleSave(currentContent)
281
+ }
282
+ }}
283
+ disabled={isSaving}
284
+ class="px-3 py-1.5 text-sm bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover rounded-cms-pill transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
285
+ data-cms-ui
286
+ >
287
+ {isSaving && <div class="animate-spin rounded-full h-3 w-3 border-2 border-cms-primary-text/30 border-t-cms-primary-text" />}
288
+ {isSaving ? 'Saving...' : 'Save'}
289
+ </button>
290
+ </div>
291
+ )
292
+ }
293
+
294
+ return (
295
+ <div
296
+ class="fixed inset-0 z-2147483647 bg-black/40 flex items-center justify-center p-4 backdrop-blur-md"
297
+ data-cms-ui
298
+ onMouseDown={stopPropagation}
299
+ onClick={stopPropagation}
300
+ >
301
+ <div
302
+ class="bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] border border-white/10 w-full max-w-4xl max-h-[90vh] flex flex-col"
303
+ data-cms-ui
304
+ >
305
+ {/* Header */}
306
+ <div class="flex items-center justify-between px-5 py-4 border-b border-white/10">
307
+ <div class="flex items-center gap-3">
308
+ <div class="flex items-center text-white">
309
+ <svg
310
+ width="20"
311
+ height="20"
312
+ viewBox="0 0 24 24"
313
+ fill="none"
314
+ stroke="currentColor"
315
+ stroke-width="2"
316
+ stroke-linecap="round"
317
+ stroke-linejoin="round"
318
+ >
319
+ <path d="M14.5 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V7.5L14.5 2z" />
320
+ <polyline points="14 2 14 8 20 8" />
321
+ <line x1="16" y1="13" x2="8" y2="13" />
322
+ <line x1="16" y1="17" x2="8" y2="17" />
323
+ <line x1="10" y1="9" x2="8" y2="9" />
324
+ </svg>
325
+ </div>
326
+ <div>
327
+ <input
328
+ type="text"
329
+ value={(page.frontmatter.title as string) || ''}
330
+ placeholder="Page title..."
331
+ onInput={(e) => {
332
+ const title = (e.target as HTMLInputElement).value
333
+ updateMarkdownFrontmatter({ title })
334
+ // Auto-generate slug in create mode if not manually edited
335
+ if (isCreateMode && !slugManuallyEdited) {
336
+ markdownEditorState.value = {
337
+ ...markdownEditorState.value,
338
+ currentPage: markdownEditorState.value.currentPage
339
+ ? {
340
+ ...markdownEditorState.value.currentPage,
341
+ slug: slugify(title),
342
+ isDirty: true,
343
+ }
344
+ : null,
345
+ }
346
+ }
347
+ }}
348
+ class="text-base font-semibold text-white m-0 bg-transparent border-none outline-none placeholder-white/40 w-64"
349
+ data-cms-ui
350
+ />
351
+ </div>
352
+ </div>
353
+ <div class="flex items-center gap-2">
354
+ <button
355
+ type="button"
356
+ onClick={() => setShowFrontmatter(!showFrontmatter)}
357
+ class={`px-3 py-2 text-sm rounded-cms-pill transition-colors flex items-center gap-1.5 ${
358
+ showFrontmatter
359
+ ? 'bg-white/20 text-white'
360
+ : 'text-white/70 hover:text-white hover:bg-white/10'
361
+ }`}
362
+ data-cms-ui
363
+ >
364
+ <svg
365
+ class="w-4 h-4"
366
+ fill="none"
367
+ stroke="currentColor"
368
+ viewBox="0 0 24 24"
369
+ stroke-width="2"
370
+ >
371
+ <path
372
+ stroke-linecap="round"
373
+ stroke-linejoin="round"
374
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
375
+ />
376
+ <path
377
+ stroke-linecap="round"
378
+ stroke-linejoin="round"
379
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
380
+ />
381
+ </svg>
382
+ Metadata
383
+ <svg
384
+ class={`w-3.5 h-3.5 transition-transform ${showFrontmatter ? 'rotate-180' : ''}`}
385
+ fill="none"
386
+ stroke="currentColor"
387
+ viewBox="0 0 24 24"
388
+ stroke-width="2.5"
389
+ >
390
+ <path
391
+ stroke-linecap="round"
392
+ stroke-linejoin="round"
393
+ d="M19 9l-7 7-7-7"
394
+ />
395
+ </svg>
396
+ </button>
397
+ {!isCreateMode && editorState.activeElementId && (
398
+ <button
399
+ type="button"
400
+ onClick={handlePreview}
401
+ class={`px-3 py-2 text-sm rounded-cms-pill transition-colors flex items-center gap-1.5 ${
402
+ isPreview
403
+ ? 'bg-white/20 text-white'
404
+ : 'text-white/70 hover:text-white hover:bg-white/10'
405
+ }`}
406
+ data-cms-ui
407
+ >
408
+ <svg
409
+ class="w-4 h-4"
410
+ fill="none"
411
+ stroke="currentColor"
412
+ viewBox="0 0 24 24"
413
+ stroke-width="2"
414
+ >
415
+ <path
416
+ stroke-linecap="round"
417
+ stroke-linejoin="round"
418
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
419
+ />
420
+ <path
421
+ stroke-linecap="round"
422
+ stroke-linejoin="round"
423
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
424
+ />
425
+ </svg>
426
+ Preview
427
+ </button>
428
+ )}
429
+ <button
430
+ type="button"
431
+ onClick={handleCancel}
432
+ class="px-4 py-2 text-sm text-white/70 hover:text-white hover:bg-white/10 rounded-cms-pill transition-colors"
433
+ data-cms-ui
434
+ >
435
+ Cancel
436
+ </button>
437
+ {isCreateMode
438
+ ? (
439
+ <button
440
+ type="button"
441
+ onClick={handleCreate}
442
+ disabled={isSaving || !(page.frontmatter.title as string)?.trim()}
443
+ class="px-4 py-2 text-sm bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover rounded-cms-pill transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
444
+ data-cms-ui
445
+ >
446
+ {isSaving && <div class="animate-spin rounded-full h-3.5 w-3.5 border-2 border-cms-primary-text/30 border-t-cms-primary-text" />}
447
+ {isSaving ? 'Creating...' : `Create ${collectionLabel}`}
448
+ </button>
449
+ )
450
+ : (
451
+ <button
452
+ type="button"
453
+ onClick={() => {
454
+ const currentContent = currentMarkdownPage.value?.content
455
+ if (currentContent !== undefined) {
456
+ handleSave(currentContent)
457
+ }
458
+ }}
459
+ disabled={isSaving}
460
+ class="px-4 py-2 text-sm bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover rounded-cms-pill transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
461
+ data-cms-ui
462
+ >
463
+ {isSaving && <div class="animate-spin rounded-full h-3.5 w-3.5 border-2 border-cms-primary-text/30 border-t-cms-primary-text" />}
464
+ {isSaving ? 'Saving...' : 'Save'}
465
+ </button>
466
+ )}
467
+ </div>
468
+ </div>
469
+
470
+ {/* Frontmatter Editor */}
471
+ {showFrontmatter && (
472
+ <div class="px-5 py-4 border-b border-white/10 bg-white/5">
473
+ {isCreateMode && createOptions
474
+ ? (
475
+ <CreateModeFrontmatter
476
+ page={page}
477
+ collectionDefinition={createOptions.collectionDefinition}
478
+ onSlugManualEdit={() => setSlugManuallyEdited(true)}
479
+ />
480
+ )
481
+ : (
482
+ <EditModeFrontmatter
483
+ page={page}
484
+ collectionDefinition={collectionDef}
485
+ />
486
+ )}
487
+ </div>
488
+ )}
489
+
490
+ {/* Editor */}
491
+ <div class="flex-1 min-h-0 overflow-auto bg-black/20">
492
+ <MarkdownInlineEditor
493
+ elementId={page.slug || 'new-page'}
494
+ initialContent={page.content}
495
+ onSave={isCreateMode ? () => handleCreate() : handleSave}
496
+ onCancel={handleCancel}
497
+ onEditorReady={(editor) => {
498
+ editorInstanceRef.current = editor
499
+ }}
500
+ />
501
+ </div>
502
+ </div>
503
+ </div>
504
+ )
505
+ }