@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,780 @@
1
+ import { commandsCtx, defaultValueCtx, Editor, editorViewCtx, rootCtx } from '@milkdown/core'
2
+ import { listener, listenerCtx } from '@milkdown/plugin-listener'
3
+ import {
4
+ commonmark,
5
+ liftListItemCommand,
6
+ toggleEmphasisCommand,
7
+ toggleLinkCommand,
8
+ toggleStrongCommand,
9
+ wrapInBlockquoteCommand,
10
+ wrapInBulletListCommand,
11
+ wrapInOrderedListCommand,
12
+ } from '@milkdown/preset-commonmark'
13
+ import { gfm, toggleStrikethroughCommand } from '@milkdown/preset-gfm'
14
+ import { callCommand, insert, replaceAll } from '@milkdown/utils'
15
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
16
+ import { uploadMedia } from '../markdown-api'
17
+ import { config, openMediaLibraryWithCallback, resetMarkdownEditorState, showToast, updateMarkdownContent } from '../signals'
18
+
19
+ export interface MarkdownInlineEditorProps {
20
+ elementId: string
21
+ initialContent: string
22
+ onSave: (content: string) => void
23
+ onCancel: () => void
24
+ onEditorReady?: (editor: Editor) => void
25
+ }
26
+
27
+ export function MarkdownInlineEditor({
28
+ elementId,
29
+ initialContent,
30
+ onSave,
31
+ onCancel,
32
+ onEditorReady,
33
+ }: MarkdownInlineEditorProps) {
34
+ const editorRef = useRef<HTMLDivElement>(null)
35
+ const editorInstanceRef = useRef<Editor | null>(null)
36
+ const [content, setContent] = useState(initialContent)
37
+ const [isReady, setIsReady] = useState(false)
38
+ const [isDragging, setIsDragging] = useState(false)
39
+ const [uploadProgress, setUploadProgress] = useState<number | null>(null)
40
+
41
+ // Track active formatting for toolbar highlighting
42
+ const [activeFormats, setActiveFormats] = useState<{
43
+ bold: boolean
44
+ italic: boolean
45
+ strikethrough: boolean
46
+ link: boolean
47
+ linkHref: string | null
48
+ bulletList: boolean
49
+ orderedList: boolean
50
+ blockquote: boolean
51
+ heading: number | null
52
+ }>({
53
+ bold: false,
54
+ italic: false,
55
+ strikethrough: false,
56
+ link: false,
57
+ linkHref: null,
58
+ bulletList: false,
59
+ orderedList: false,
60
+ blockquote: false,
61
+ heading: null,
62
+ })
63
+
64
+ // Store initial content in ref to avoid stale closure issues
65
+ const initialContentRef = useRef(initialContent)
66
+ // Track current content in ref for use in callbacks
67
+ const contentRef = useRef(content)
68
+ contentRef.current = content
69
+ // Store onEditorReady in ref to avoid re-initializing editor when callback changes
70
+ const onEditorReadyRef = useRef(onEditorReady)
71
+ onEditorReadyRef.current = onEditorReady
72
+
73
+ // Check active formatting at current selection
74
+ const updateActiveFormats = useCallback(() => {
75
+ if (!editorInstanceRef.current) return
76
+
77
+ try {
78
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
79
+ const { state } = view
80
+ const { $from, from, to } = state.selection
81
+
82
+ // Check marks (inline formatting)
83
+ let bold = false
84
+ let italic = false
85
+ let strikethrough = false
86
+ let link = false
87
+ let linkHref: string | null = null
88
+
89
+ // Check if marks are active in the selection
90
+ const marks = state.storedMarks || $from.marks()
91
+ for (const mark of marks) {
92
+ if (mark.type.name === 'strong') bold = true
93
+ if (mark.type.name === 'emphasis') italic = true
94
+ if (mark.type.name === 'strikethrough') strikethrough = true
95
+ if (mark.type.name === 'link') {
96
+ link = true
97
+ linkHref = mark.attrs.href as string
98
+ }
99
+ }
100
+
101
+ // Also check marks in the selection range
102
+ if (from !== to) {
103
+ state.doc.nodesBetween(from, to, (node) => {
104
+ if (node.marks) {
105
+ for (const mark of node.marks) {
106
+ if (mark.type.name === 'strong') bold = true
107
+ if (mark.type.name === 'emphasis') italic = true
108
+ if (mark.type.name === 'strikethrough') strikethrough = true
109
+ if (mark.type.name === 'link') {
110
+ link = true
111
+ linkHref = mark.attrs.href as string
112
+ }
113
+ }
114
+ }
115
+ })
116
+ }
117
+
118
+ // Check block types (lists, blockquote, heading)
119
+ let bulletList = false
120
+ let orderedList = false
121
+ let blockquote = false
122
+ let heading: number | null = null
123
+
124
+ for (let depth = $from.depth; depth > 0; depth--) {
125
+ const node = $from.node(depth)
126
+ if (node.type.name === 'bullet_list') bulletList = true
127
+ if (node.type.name === 'ordered_list') orderedList = true
128
+ if (node.type.name === 'blockquote') blockquote = true
129
+ }
130
+
131
+ // Check heading at current position
132
+ const parentNode = $from.parent
133
+ if (parentNode.type.name === 'heading') {
134
+ heading = parentNode.attrs.level as number
135
+ }
136
+
137
+ setActiveFormats({
138
+ bold,
139
+ italic,
140
+ strikethrough,
141
+ link,
142
+ linkHref,
143
+ bulletList,
144
+ orderedList,
145
+ blockquote,
146
+ heading,
147
+ })
148
+ } catch {
149
+ // Ignore errors during format checking
150
+ }
151
+ }, [])
152
+
153
+ // Initialize Milkdown editor
154
+ useEffect(() => {
155
+ if (!editorRef.current) return
156
+
157
+ const initEditor = async () => {
158
+ try {
159
+ const editor = await Editor.make()
160
+ .config((ctx) => {
161
+ ctx.set(rootCtx, editorRef.current)
162
+ ctx.set(defaultValueCtx, initialContentRef.current)
163
+ ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
164
+ setContent(markdown)
165
+ updateMarkdownContent(markdown)
166
+ })
167
+ })
168
+ .use(commonmark)
169
+ .use(gfm)
170
+ .use(listener)
171
+ .create()
172
+
173
+ editorInstanceRef.current = editor
174
+ setIsReady(true)
175
+ onEditorReadyRef.current?.(editor)
176
+
177
+ // Set up selection change listener
178
+ const view = editor.ctx.get(editorViewCtx)
179
+ const originalDispatch = view.dispatch.bind(view)
180
+ view.dispatch = (tr) => {
181
+ originalDispatch(tr)
182
+ if (tr.selectionSet || tr.docChanged) {
183
+ updateActiveFormats()
184
+ }
185
+ }
186
+
187
+ // Initial format check
188
+ updateActiveFormats()
189
+ } catch (error) {
190
+ console.error('Milkdown editor initialization failed:', error)
191
+ showToast('Failed to initialize markdown editor', 'error')
192
+ }
193
+ }
194
+
195
+ initEditor()
196
+
197
+ return () => {
198
+ editorInstanceRef.current?.destroy()
199
+ editorInstanceRef.current = null
200
+ }
201
+ }, [updateActiveFormats])
202
+
203
+ const handleSave = useCallback(() => {
204
+ onSave(content)
205
+ resetMarkdownEditorState()
206
+ }, [content, onSave])
207
+
208
+ const handleCancel = useCallback(() => {
209
+ onCancel()
210
+ resetMarkdownEditorState()
211
+ }, [onCancel])
212
+
213
+ const handleInsertImage = useCallback(() => {
214
+ openMediaLibraryWithCallback((url, alt) => {
215
+ const imageMarkdown = `\n\n![${alt}](${url})\n\n`
216
+
217
+ // Insert at cursor position using Milkdown's insert command
218
+ if (editorInstanceRef.current) {
219
+ try {
220
+ editorInstanceRef.current.action(insert(imageMarkdown))
221
+ } catch (error) {
222
+ console.error('Failed to insert image:', error)
223
+ // Fallback: append to content
224
+ const newContent = `${contentRef.current}\n\n![${alt}](${url})`
225
+ setContent(newContent)
226
+ editorInstanceRef.current.action(replaceAll(newContent))
227
+ }
228
+ }
229
+ })
230
+ }, [])
231
+
232
+ // Formatting commands
233
+ const runCommand = useCallback(
234
+ (command: Parameters<typeof callCommand>[0]) => {
235
+ if (editorInstanceRef.current) {
236
+ try {
237
+ editorInstanceRef.current.action(callCommand(command))
238
+ } catch (error) {
239
+ console.error('Failed to run command:', error)
240
+ }
241
+ }
242
+ },
243
+ [],
244
+ )
245
+
246
+ const handleBold = useCallback(
247
+ () => runCommand(toggleStrongCommand.key),
248
+ [runCommand],
249
+ )
250
+ const handleItalic = useCallback(
251
+ () => runCommand(toggleEmphasisCommand.key),
252
+ [runCommand],
253
+ )
254
+ const handleStrikethrough = useCallback(
255
+ () => runCommand(toggleStrikethroughCommand.key),
256
+ [runCommand],
257
+ )
258
+ const handleQuote = useCallback(
259
+ () => runCommand(wrapInBlockquoteCommand.key),
260
+ [runCommand],
261
+ )
262
+
263
+ // Check if selection is inside a list of given type
264
+ const isInList = useCallback(
265
+ (listType: 'bullet_list' | 'ordered_list'): boolean => {
266
+ if (!editorInstanceRef.current) return false
267
+ try {
268
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
269
+ const { state } = view
270
+ const { $from } = state.selection
271
+ for (let depth = $from.depth; depth > 0; depth--) {
272
+ const node = $from.node(depth)
273
+ if (node.type.name === listType) return true
274
+ }
275
+ return false
276
+ } catch {
277
+ return false
278
+ }
279
+ },
280
+ [],
281
+ )
282
+
283
+ // Toggle bullet list - if in bullet list, remove it; otherwise add it
284
+ const handleBulletList = useCallback(() => {
285
+ if (isInList('bullet_list')) {
286
+ runCommand(liftListItemCommand.key)
287
+ } else {
288
+ runCommand(wrapInBulletListCommand.key)
289
+ }
290
+ }, [runCommand, isInList])
291
+
292
+ // Toggle ordered list - if in ordered list, remove it; otherwise add it
293
+ const handleOrderedList = useCallback(() => {
294
+ if (isInList('ordered_list')) {
295
+ runCommand(liftListItemCommand.key)
296
+ } else {
297
+ runCommand(wrapInOrderedListCommand.key)
298
+ }
299
+ }, [runCommand, isInList])
300
+
301
+ const handleInsertLink = useCallback(() => {
302
+ if (!editorInstanceRef.current) return
303
+
304
+ // If already in a link, remove it
305
+ if (activeFormats.link) {
306
+ try {
307
+ // Use toggleLinkCommand with empty href to remove link
308
+ editorInstanceRef.current.action(
309
+ callCommand(toggleLinkCommand.key, { href: '' }),
310
+ )
311
+ return
312
+ } catch (error) {
313
+ console.error('Failed to remove link:', error)
314
+ }
315
+ }
316
+
317
+ // Get selected text from editor
318
+ let selectedText = ''
319
+ try {
320
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
321
+ const { state } = view
322
+ const { from, to } = state.selection
323
+ if (from !== to) {
324
+ selectedText = state.doc.textBetween(from, to, ' ')
325
+ }
326
+ } catch {
327
+ // Ignore errors
328
+ }
329
+
330
+ // Prompt for URL (pre-fill with existing URL if editing)
331
+ const defaultUrl = activeFormats.linkHref || ''
332
+ const url = prompt('Enter URL:', defaultUrl)
333
+ if (url) {
334
+ try {
335
+ // Use toggleLinkCommand to add/update link
336
+ editorInstanceRef.current.action(
337
+ callCommand(toggleLinkCommand.key, { href: url }),
338
+ )
339
+ } catch (error) {
340
+ console.error('Failed to add link:', error)
341
+ // Fallback: use markdown insertion
342
+ const linkText = selectedText || prompt('Enter link text:', 'Link') || 'Link'
343
+ const linkMarkdown = `[${linkText}](${url})`
344
+ editorInstanceRef.current.action(insert(linkMarkdown))
345
+ }
346
+ }
347
+ }, [activeFormats.link, activeFormats.linkHref])
348
+
349
+ const handleInsertHeading = useCallback((level: number) => {
350
+ const prefix = '#'.repeat(level) + ' '
351
+ const headingMarkdown = `\n\n${prefix}Heading\n\n`
352
+
353
+ // Insert at cursor position
354
+ if (editorInstanceRef.current) {
355
+ try {
356
+ editorInstanceRef.current.action(insert(headingMarkdown))
357
+ } catch (error) {
358
+ console.error('Failed to insert heading:', error)
359
+ }
360
+ }
361
+ }, [])
362
+
363
+ // Drag and drop handlers for direct image upload
364
+ const handleDragOver = useCallback((e: DragEvent) => {
365
+ e.preventDefault()
366
+ e.stopPropagation()
367
+ setIsDragging(true)
368
+ }, [])
369
+
370
+ const handleDragLeave = useCallback((e: DragEvent) => {
371
+ e.preventDefault()
372
+ e.stopPropagation()
373
+ setIsDragging(false)
374
+ }, [])
375
+
376
+ const handleDrop = useCallback(async (e: DragEvent) => {
377
+ e.preventDefault()
378
+ e.stopPropagation()
379
+ setIsDragging(false)
380
+
381
+ const file = e.dataTransfer?.files[0]
382
+ if (!file || !file.type.startsWith('image/')) {
383
+ showToast('Please drop an image file', 'error')
384
+ return
385
+ }
386
+
387
+ // Upload the image
388
+ setUploadProgress(0)
389
+ try {
390
+ const result = await uploadMedia(config.value, file, (percent) => {
391
+ setUploadProgress(percent)
392
+ })
393
+
394
+ if (result.success && result.url) {
395
+ const alt = result.annotation || file.name.replace(/\.[^/.]+$/, '') || 'Image'
396
+ const imageMarkdown = `\n\n![${alt}](${result.url})\n\n`
397
+
398
+ // Insert at cursor position
399
+ if (editorInstanceRef.current) {
400
+ try {
401
+ editorInstanceRef.current.action(insert(imageMarkdown))
402
+ showToast('Image uploaded and inserted', 'success')
403
+ } catch (error) {
404
+ console.error('Failed to insert image:', error)
405
+ }
406
+ }
407
+ } else {
408
+ showToast(result.error || 'Upload failed', 'error')
409
+ }
410
+ } catch (error) {
411
+ showToast('Upload failed', 'error')
412
+ } finally {
413
+ setUploadProgress(null)
414
+ }
415
+ }, [])
416
+
417
+ // Handle paste for images
418
+ const handlePaste = useCallback(async (e: ClipboardEvent) => {
419
+ const items = e.clipboardData?.items
420
+ if (!items) return
421
+
422
+ for (const item of items) {
423
+ if (item.type.startsWith('image/')) {
424
+ e.preventDefault()
425
+ const file = item.getAsFile()
426
+ if (!file) continue
427
+
428
+ setUploadProgress(0)
429
+ try {
430
+ const result = await uploadMedia(config.value, file, (percent) => {
431
+ setUploadProgress(percent)
432
+ })
433
+
434
+ if (result.success && result.url) {
435
+ const alt = result.annotation || 'Pasted image'
436
+ const imageMarkdown = `\n\n![${alt}](${result.url})\n\n`
437
+
438
+ if (editorInstanceRef.current) {
439
+ editorInstanceRef.current.action(insert(imageMarkdown))
440
+ showToast('Image uploaded and inserted', 'success')
441
+ }
442
+ } else {
443
+ showToast(result.error || 'Upload failed', 'error')
444
+ }
445
+ } catch (error) {
446
+ showToast('Upload failed', 'error')
447
+ } finally {
448
+ setUploadProgress(null)
449
+ }
450
+ break
451
+ }
452
+ }
453
+ }, [])
454
+
455
+ return (
456
+ <div
457
+ class="markdown-inline-editor flex flex-col h-full min-h-0"
458
+ data-cms-ui
459
+ data-element-id={elementId}
460
+ >
461
+ {/* Formatting Toolbar */}
462
+ <div class="flex items-center gap-1 px-4 py-3 border-b border-white/10 bg-cms-dark/50 flex-wrap shrink-0 sticky top-0 z-50 backdrop-blur-md">
463
+ {/* Text Formatting */}
464
+ <div class="flex items-center gap-0.5 mr-2">
465
+ <ToolbarButton
466
+ onClick={handleBold}
467
+ title="Bold (Ctrl+B)"
468
+ active={activeFormats.bold}
469
+ >
470
+ <svg
471
+ class="w-4 h-4"
472
+ fill="none"
473
+ stroke="currentColor"
474
+ viewBox="0 0 24 24"
475
+ stroke-width="2.5"
476
+ >
477
+ <path
478
+ stroke-linecap="round"
479
+ stroke-linejoin="round"
480
+ d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"
481
+ />
482
+ <path
483
+ stroke-linecap="round"
484
+ stroke-linejoin="round"
485
+ d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"
486
+ />
487
+ </svg>
488
+ </ToolbarButton>
489
+ <ToolbarButton
490
+ onClick={handleItalic}
491
+ title="Italic (Ctrl+I)"
492
+ active={activeFormats.italic}
493
+ >
494
+ <svg
495
+ class="w-4 h-4"
496
+ fill="none"
497
+ stroke="currentColor"
498
+ viewBox="0 0 24 24"
499
+ stroke-width="2"
500
+ >
501
+ <line x1="19" y1="4" x2="10" y2="4" />
502
+ <line x1="14" y1="20" x2="5" y2="20" />
503
+ <line x1="15" y1="4" x2="9" y2="20" />
504
+ </svg>
505
+ </ToolbarButton>
506
+ <ToolbarButton
507
+ onClick={handleStrikethrough}
508
+ title="Strikethrough"
509
+ active={activeFormats.strikethrough}
510
+ >
511
+ <svg
512
+ class="w-4 h-4"
513
+ fill="none"
514
+ stroke="currentColor"
515
+ viewBox="0 0 24 24"
516
+ stroke-width="2"
517
+ >
518
+ <path
519
+ stroke-linecap="round"
520
+ stroke-linejoin="round"
521
+ d="M6 12h12M6 12a4 4 0 0 1 4-4h4a4 4 0 0 1 0 8H10a4 4 0 0 1-4-4z"
522
+ />
523
+ </svg>
524
+ </ToolbarButton>
525
+ </div>
526
+
527
+ {/* Divider */}
528
+ <div class="w-px h-5 bg-white/20 mx-1" />
529
+
530
+ {/* Headings */}
531
+ <div class="flex items-center gap-0.5 mr-2">
532
+ <ToolbarButton
533
+ onClick={() => handleInsertHeading(1)}
534
+ title="Heading 1"
535
+ active={activeFormats.heading === 1}
536
+ >
537
+ <span class="text-xs font-bold">H1</span>
538
+ </ToolbarButton>
539
+ <ToolbarButton
540
+ onClick={() => handleInsertHeading(2)}
541
+ title="Heading 2"
542
+ active={activeFormats.heading === 2}
543
+ >
544
+ <span class="text-xs font-bold">H2</span>
545
+ </ToolbarButton>
546
+ <ToolbarButton
547
+ onClick={() => handleInsertHeading(3)}
548
+ title="Heading 3"
549
+ active={activeFormats.heading === 3}
550
+ >
551
+ <span class="text-xs font-bold">H3</span>
552
+ </ToolbarButton>
553
+ <ToolbarButton
554
+ onClick={() => handleInsertHeading(4)}
555
+ title="Heading 4"
556
+ active={activeFormats.heading === 4}
557
+ >
558
+ <span class="text-xs font-bold">H4</span>
559
+ </ToolbarButton>
560
+ </div>
561
+
562
+ {/* Divider */}
563
+ <div class="w-px h-5 bg-white/20 mx-1" />
564
+
565
+ {/* Lists & Quote */}
566
+ <div class="flex items-center gap-0.5 mr-2">
567
+ <ToolbarButton
568
+ onClick={handleBulletList}
569
+ title="Bullet List"
570
+ active={activeFormats.bulletList}
571
+ >
572
+ <svg
573
+ class="w-4 h-4"
574
+ fill="none"
575
+ stroke="currentColor"
576
+ viewBox="0 0 24 24"
577
+ stroke-width="2"
578
+ >
579
+ <line x1="9" y1="6" x2="20" y2="6" />
580
+ <line x1="9" y1="12" x2="20" y2="12" />
581
+ <line x1="9" y1="18" x2="20" y2="18" />
582
+ <circle cx="4" cy="6" r="1.5" fill="currentColor" />
583
+ <circle cx="4" cy="12" r="1.5" fill="currentColor" />
584
+ <circle cx="4" cy="18" r="1.5" fill="currentColor" />
585
+ </svg>
586
+ </ToolbarButton>
587
+ <ToolbarButton
588
+ onClick={handleOrderedList}
589
+ title="Numbered List"
590
+ active={activeFormats.orderedList}
591
+ >
592
+ <svg
593
+ class="w-4 h-4"
594
+ fill="none"
595
+ stroke="currentColor"
596
+ viewBox="0 0 24 24"
597
+ stroke-width="2"
598
+ >
599
+ <line x1="10" y1="6" x2="21" y2="6" />
600
+ <line x1="10" y1="12" x2="21" y2="12" />
601
+ <line x1="10" y1="18" x2="21" y2="18" />
602
+ <text x="3" y="8" font-size="7" fill="currentColor" stroke="none">
603
+ 1
604
+ </text>
605
+ <text
606
+ x="3"
607
+ y="14"
608
+ font-size="7"
609
+ fill="currentColor"
610
+ stroke="none"
611
+ >
612
+ 2
613
+ </text>
614
+ <text
615
+ x="3"
616
+ y="20"
617
+ font-size="7"
618
+ fill="currentColor"
619
+ stroke="none"
620
+ >
621
+ 3
622
+ </text>
623
+ </svg>
624
+ </ToolbarButton>
625
+ <ToolbarButton
626
+ onClick={handleQuote}
627
+ title="Quote"
628
+ active={activeFormats.blockquote}
629
+ >
630
+ <svg
631
+ class="w-4 h-4"
632
+ fill="none"
633
+ stroke="currentColor"
634
+ viewBox="0 0 24 24"
635
+ stroke-width="2"
636
+ >
637
+ <path
638
+ stroke-linecap="round"
639
+ stroke-linejoin="round"
640
+ d="M3 6v12M7 6v12M11 6h10M11 12h7M11 18h4"
641
+ />
642
+ </svg>
643
+ </ToolbarButton>
644
+ </div>
645
+
646
+ {/* Divider */}
647
+ <div class="w-px h-5 bg-white/20 mx-1" />
648
+
649
+ {/* Links & Images */}
650
+ <div class="flex items-center gap-0.5">
651
+ <ToolbarButton
652
+ onClick={handleInsertLink}
653
+ title={activeFormats.link ? 'Remove Link' : 'Insert Link'}
654
+ active={activeFormats.link}
655
+ >
656
+ <svg
657
+ class="w-4 h-4"
658
+ fill="none"
659
+ stroke="currentColor"
660
+ viewBox="0 0 24 24"
661
+ stroke-width="2"
662
+ >
663
+ <path
664
+ stroke-linecap="round"
665
+ stroke-linejoin="round"
666
+ d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
667
+ />
668
+ </svg>
669
+ </ToolbarButton>
670
+ <ToolbarButton onClick={handleInsertImage} title="Insert Image">
671
+ <svg
672
+ class="w-4 h-4"
673
+ fill="none"
674
+ stroke="currentColor"
675
+ viewBox="0 0 24 24"
676
+ stroke-width="2"
677
+ >
678
+ <path
679
+ stroke-linecap="round"
680
+ stroke-linejoin="round"
681
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
682
+ />
683
+ </svg>
684
+ </ToolbarButton>
685
+ </div>
686
+ </div>
687
+
688
+ {/* Editor */}
689
+ <div
690
+ class={`flex-1 min-h-0 overflow-auto relative transition-colors ${isDragging ? 'bg-cms-primary/10' : ''}`}
691
+ onDragOver={handleDragOver}
692
+ onDragLeave={handleDragLeave}
693
+ onDrop={handleDrop}
694
+ onPaste={handlePaste}
695
+ >
696
+ <div
697
+ ref={editorRef}
698
+ class="milkdown-editor milkdown-dark prose prose-invert prose-sm max-w-none p-6 min-h-75 focus:outline-none"
699
+ data-cms-ui
700
+ />
701
+
702
+ {/* Drag overlay */}
703
+ {isDragging && (
704
+ <div class="absolute inset-0 flex items-center justify-center bg-cms-primary/10 border-2 border-dashed border-cms-primary rounded-lg pointer-events-none">
705
+ <div class="flex flex-col items-center gap-2 text-cms-primary">
706
+ <svg
707
+ class="w-10 h-10"
708
+ fill="none"
709
+ stroke="currentColor"
710
+ viewBox="0 0 24 24"
711
+ stroke-width="1.5"
712
+ >
713
+ <path
714
+ stroke-linecap="round"
715
+ stroke-linejoin="round"
716
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
717
+ />
718
+ </svg>
719
+ <span class="font-medium">Drop image to upload</span>
720
+ </div>
721
+ </div>
722
+ )}
723
+
724
+ {/* Upload progress */}
725
+ {uploadProgress !== null && (
726
+ <div class="absolute inset-0 flex items-center justify-center bg-cms-dark/80">
727
+ <div class="flex flex-col items-center gap-3">
728
+ <div class="w-48 h-2 bg-white/10 rounded-full overflow-hidden">
729
+ <div
730
+ class="h-full bg-cms-primary transition-all duration-200 rounded-full"
731
+ style={{ width: `${uploadProgress}%` }}
732
+ />
733
+ </div>
734
+ <span class="text-sm text-white font-medium">
735
+ Uploading... {uploadProgress}%
736
+ </span>
737
+ </div>
738
+ </div>
739
+ )}
740
+
741
+ {/* Loading state */}
742
+ {!isReady && (
743
+ <div class="absolute inset-0 flex items-center justify-center bg-cms-dark/80">
744
+ <div class="animate-spin rounded-full h-6 w-6 border-2 border-white/30 border-t-cms-primary" />
745
+ </div>
746
+ )}
747
+ </div>
748
+ </div>
749
+ )
750
+ }
751
+
752
+ interface ToolbarButtonProps {
753
+ onClick: () => void
754
+ title: string
755
+ children: preact.ComponentChildren
756
+ active?: boolean
757
+ }
758
+
759
+ function ToolbarButton({
760
+ onClick,
761
+ title,
762
+ children,
763
+ active,
764
+ }: ToolbarButtonProps) {
765
+ return (
766
+ <button
767
+ type="button"
768
+ onClick={onClick}
769
+ class={`p-2 rounded-cms-sm transition-colors ${
770
+ active
771
+ ? 'bg-cms-primary text-cms-primary-text'
772
+ : 'hover:bg-white/10 text-white/70 hover:text-white'
773
+ }`}
774
+ title={title}
775
+ data-cms-ui
776
+ >
777
+ {children}
778
+ </button>
779
+ )
780
+ }