@nuasite/cms 0.19.1 → 0.20.2

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 (39) hide show
  1. package/dist/editor.js +12615 -12689
  2. package/package.json +3 -3
  3. package/src/build-processor.ts +4 -4
  4. package/src/dev-middleware.ts +185 -189
  5. package/src/editor/api.ts +0 -251
  6. package/src/editor/components/fields.tsx +6 -6
  7. package/src/editor/components/markdown-editor-overlay.tsx +46 -70
  8. package/src/editor/components/markdown-inline-editor.tsx +34 -165
  9. package/src/editor/components/mdx-block-view.tsx +351 -47
  10. package/src/editor/components/mdx-component-picker.tsx +35 -11
  11. package/src/editor/components/media-library.tsx +1 -15
  12. package/src/editor/components/modal-shell.tsx +1 -1
  13. package/src/editor/components/toolbar.tsx +0 -75
  14. package/src/editor/constants.ts +0 -4
  15. package/src/editor/editor.ts +2 -192
  16. package/src/editor/hooks/index.ts +0 -3
  17. package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
  18. package/src/editor/hooks/useTooltipState.ts +1 -2
  19. package/src/editor/index.tsx +2 -18
  20. package/src/editor/milkdown-mdx-plugin.tsx +116 -19
  21. package/src/editor/milkdown-utils.ts +174 -0
  22. package/src/editor/post-message.ts +0 -6
  23. package/src/editor/signals.ts +0 -183
  24. package/src/editor/styles.css +0 -108
  25. package/src/editor/types.ts +0 -76
  26. package/src/html-processor.ts +9 -7
  27. package/src/source-finder/cache.ts +47 -0
  28. package/src/source-finder/collection-finder.ts +181 -0
  29. package/src/source-finder/index.ts +5 -2
  30. package/src/source-finder/search-index.ts +79 -0
  31. package/src/source-finder/snippet-utils.ts +36 -61
  32. package/src/types.ts +0 -4
  33. package/src/utils.ts +10 -0
  34. package/src/vite-plugin.ts +24 -4
  35. package/src/editor/ai.ts +0 -185
  36. package/src/editor/components/ai-chat.tsx +0 -631
  37. package/src/editor/components/ai-tooltip.tsx +0 -180
  38. package/src/editor/components/mdx-props-editor.tsx +0 -94
  39. package/src/editor/hooks/useAIHandlers.ts +0 -345
@@ -1,4 +1,4 @@
1
- import { commandsCtx, defaultValueCtx, Editor, editorViewCtx, rootCtx } from '@milkdown/core'
1
+ import { defaultValueCtx, Editor, editorViewCtx, rootCtx } from '@milkdown/core'
2
2
  import { listener, listenerCtx } from '@milkdown/plugin-listener'
3
3
  import {
4
4
  commonmark,
@@ -15,10 +15,10 @@ import { callCommand, insert, replaceAll } from '@milkdown/utils'
15
15
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
16
16
  import { uploadMedia } from '../markdown-api'
17
17
  import { insertMdxComponentCommand, mdxComponentPlugin } from '../milkdown-mdx-plugin'
18
+ import { type ActiveFormats, defaultActiveFormats, isInListType, setupFormatTracking, toggleHeading } from '../milkdown-utils'
18
19
  import { config, mdxComponentPickerOpen, openMediaLibraryWithCallback, resetMarkdownEditorState, showToast, updateMarkdownContent } from '../signals'
19
20
  import { MdxComponentIcon } from './mdx-block-view'
20
21
  import { MdxComponentPicker } from './mdx-component-picker'
21
- import { MdxPropsEditor } from './mdx-props-editor'
22
22
 
23
23
  export interface MarkdownInlineEditorProps {
24
24
  elementId: string
@@ -45,27 +45,7 @@ export function MarkdownInlineEditor({
45
45
  const [uploadProgress, setUploadProgress] = useState<number | null>(null)
46
46
 
47
47
  // Track active formatting for toolbar highlighting
48
- const [activeFormats, setActiveFormats] = useState<{
49
- bold: boolean
50
- italic: boolean
51
- strikethrough: boolean
52
- link: boolean
53
- linkHref: string | null
54
- bulletList: boolean
55
- orderedList: boolean
56
- blockquote: boolean
57
- heading: number | null
58
- }>({
59
- bold: false,
60
- italic: false,
61
- strikethrough: false,
62
- link: false,
63
- linkHref: null,
64
- bulletList: false,
65
- orderedList: false,
66
- blockquote: false,
67
- heading: null,
68
- })
48
+ const [activeFormats, setActiveFormats] = useState<ActiveFormats>(defaultActiveFormats)
69
49
 
70
50
  // Store initial content in ref to avoid stale closure issues
71
51
  const initialContentRef = useRef(initialContent)
@@ -79,91 +59,11 @@ export function MarkdownInlineEditor({
79
59
  const isMdxRef = useRef(isMdx ?? false)
80
60
  isMdxRef.current = isMdx ?? false
81
61
 
82
- // Check active formatting at current selection
83
- const updateActiveFormats = useCallback(() => {
84
- if (!editorInstanceRef.current) return
85
-
86
- try {
87
- const view = editorInstanceRef.current.ctx.get(editorViewCtx)
88
- const { state } = view
89
- const { $from, from, to } = state.selection
90
-
91
- // Check marks (inline formatting)
92
- let bold = false
93
- let italic = false
94
- let strikethrough = false
95
- let link = false
96
- let linkHref: string | null = null
97
-
98
- // Check if marks are active in the selection
99
- const marks = state.storedMarks || $from.marks()
100
- for (const mark of marks) {
101
- if (mark.type.name === 'strong') bold = true
102
- if (mark.type.name === 'emphasis') italic = true
103
- if (mark.type.name === 'strikethrough') strikethrough = true
104
- if (mark.type.name === 'link') {
105
- link = true
106
- linkHref = mark.attrs.href as string
107
- }
108
- }
109
-
110
- // Also check marks in the selection range
111
- if (from !== to) {
112
- state.doc.nodesBetween(from, to, (node) => {
113
- if (node.marks) {
114
- for (const mark of node.marks) {
115
- if (mark.type.name === 'strong') bold = true
116
- if (mark.type.name === 'emphasis') italic = true
117
- if (mark.type.name === 'strikethrough') strikethrough = true
118
- if (mark.type.name === 'link') {
119
- link = true
120
- linkHref = mark.attrs.href as string
121
- }
122
- }
123
- }
124
- })
125
- }
126
-
127
- // Check block types (lists, blockquote, heading)
128
- let bulletList = false
129
- let orderedList = false
130
- let blockquote = false
131
- let heading: number | null = null
132
-
133
- for (let depth = $from.depth; depth > 0; depth--) {
134
- const node = $from.node(depth)
135
- if (node.type.name === 'bullet_list') bulletList = true
136
- if (node.type.name === 'ordered_list') orderedList = true
137
- if (node.type.name === 'blockquote') blockquote = true
138
- }
139
-
140
- // Check heading at current position
141
- const parentNode = $from.parent
142
- if (parentNode.type.name === 'heading') {
143
- heading = parentNode.attrs.level as number
144
- }
145
-
146
- setActiveFormats({
147
- bold,
148
- italic,
149
- strikethrough,
150
- link,
151
- linkHref,
152
- bulletList,
153
- orderedList,
154
- blockquote,
155
- heading,
156
- })
157
- } catch {
158
- // Ignore errors during format checking
159
- }
160
- }, [])
161
-
162
62
  // Initialize Milkdown editor
163
63
  useEffect(() => {
164
64
  if (!editorRef.current) return
165
65
 
166
- let formatRaf = 0
66
+ let cleanupTracking: (() => void) | undefined
167
67
 
168
68
  const initEditor = async () => {
169
69
  try {
@@ -193,20 +93,8 @@ export function MarkdownInlineEditor({
193
93
  setIsReady(true)
194
94
  onEditorReadyRef.current?.(editor)
195
95
 
196
- // Set up selection change listener debounce via rAF to avoid
197
- // redundant mark-scanning on every keystroke
198
- const view = editor.ctx.get(editorViewCtx)
199
- const originalDispatch = view.dispatch.bind(view)
200
- view.dispatch = (tr) => {
201
- originalDispatch(tr)
202
- if (tr.selectionSet || tr.docChanged) {
203
- cancelAnimationFrame(formatRaf)
204
- formatRaf = requestAnimationFrame(updateActiveFormats)
205
- }
206
- }
207
-
208
- // Initial format check
209
- updateActiveFormats()
96
+ // Set up selection change listener with shallow equality check
97
+ cleanupTracking = setupFormatTracking(editor, setActiveFormats)
210
98
  } catch (error) {
211
99
  console.error('Milkdown editor initialization failed:', error)
212
100
  showToast('Failed to initialize markdown editor', 'error')
@@ -216,11 +104,11 @@ export function MarkdownInlineEditor({
216
104
  initEditor()
217
105
 
218
106
  return () => {
219
- cancelAnimationFrame(formatRaf)
107
+ cleanupTracking?.()
220
108
  editorInstanceRef.current?.destroy()
221
109
  editorInstanceRef.current = null
222
110
  }
223
- }, [updateActiveFormats])
111
+ }, [])
224
112
 
225
113
  const handleSave = useCallback(() => {
226
114
  onSave(content)
@@ -283,18 +171,12 @@ export function MarkdownInlineEditor({
283
171
  )
284
172
 
285
173
  // Check if selection is inside a list of given type
286
- const isInList = useCallback(
174
+ const checkInList = useCallback(
287
175
  (listType: 'bullet_list' | 'ordered_list'): boolean => {
288
176
  if (!editorInstanceRef.current) return false
289
177
  try {
290
178
  const view = editorInstanceRef.current.ctx.get(editorViewCtx)
291
- const { state } = view
292
- const { $from } = state.selection
293
- for (let depth = $from.depth; depth > 0; depth--) {
294
- const node = $from.node(depth)
295
- if (node.type.name === listType) return true
296
- }
297
- return false
179
+ return isInListType(view, listType)
298
180
  } catch {
299
181
  return false
300
182
  }
@@ -304,21 +186,21 @@ export function MarkdownInlineEditor({
304
186
 
305
187
  // Toggle bullet list - if in bullet list, remove it; otherwise add it
306
188
  const handleBulletList = useCallback(() => {
307
- if (isInList('bullet_list')) {
189
+ if (checkInList('bullet_list')) {
308
190
  runCommand(liftListItemCommand.key)
309
191
  } else {
310
192
  runCommand(wrapInBulletListCommand.key)
311
193
  }
312
- }, [runCommand, isInList])
194
+ }, [runCommand, checkInList])
313
195
 
314
196
  // Toggle ordered list - if in ordered list, remove it; otherwise add it
315
197
  const handleOrderedList = useCallback(() => {
316
- if (isInList('ordered_list')) {
198
+ if (checkInList('ordered_list')) {
317
199
  runCommand(liftListItemCommand.key)
318
200
  } else {
319
201
  runCommand(wrapInOrderedListCommand.key)
320
202
  }
321
- }, [runCommand, isInList])
203
+ }, [runCommand, checkInList])
322
204
 
323
205
  const handleInsertLink = useCallback(() => {
324
206
  if (!editorInstanceRef.current) return
@@ -369,24 +251,20 @@ export function MarkdownInlineEditor({
369
251
  }, [activeFormats.link, activeFormats.linkHref])
370
252
 
371
253
  const handleInsertHeading = useCallback((level: number) => {
372
- const prefix = '#'.repeat(level) + ' '
373
- const headingMarkdown = `\n\n${prefix}Heading\n\n`
374
-
375
- // Insert at cursor position
376
- if (editorInstanceRef.current) {
377
- try {
378
- editorInstanceRef.current.action(insert(headingMarkdown))
379
- } catch (error) {
380
- console.error('Failed to insert heading:', error)
381
- }
254
+ if (!editorInstanceRef.current) return
255
+ try {
256
+ const view = editorInstanceRef.current.ctx.get(editorViewCtx)
257
+ toggleHeading(view, level)
258
+ } catch (error) {
259
+ console.error('Failed to toggle heading:', error)
382
260
  }
383
261
  }, [])
384
262
 
385
263
  // MDX component insertion
386
- const handleInsertMdxComponent = useCallback((componentName: string, props: Record<string, string>) => {
264
+ const handleInsertMdxComponent = useCallback((componentName: string, props: Record<string, string>, children?: string) => {
387
265
  if (editorInstanceRef.current) {
388
266
  try {
389
- editorInstanceRef.current.action(callCommand(insertMdxComponentCommand.key, { componentName, props }))
267
+ editorInstanceRef.current.action(callCommand(insertMdxComponentCommand.key, { componentName, props, children }))
390
268
  } catch (error) {
391
269
  console.error('Failed to insert MDX component:', error)
392
270
  }
@@ -397,42 +275,36 @@ export function MarkdownInlineEditor({
397
275
  mdxComponentPickerOpen.value = true
398
276
  }, [])
399
277
 
400
- const handleUpdateMdxProps = useCallback((nodePos: number, props: Record<string, string>) => {
401
- if (!editorInstanceRef.current) return
402
- try {
403
- const view = editorInstanceRef.current.ctx.get(editorViewCtx)
404
- const node = view.state.doc.nodeAt(nodePos)
405
- if (node && node.type.name === 'mdx_component') {
406
- const tr = view.state.tr.setNodeMarkup(nodePos, undefined, {
407
- ...node.attrs,
408
- props: JSON.stringify(props),
409
- })
410
- view.dispatch(tr)
411
- }
412
- } catch (error) {
413
- console.error('Failed to update MDX component props:', error)
414
- }
415
- }, [])
416
-
417
278
  // Drag and drop handlers for direct image upload
279
+ // Only intercept external file drags — let ProseMirror handle internal drags (node reorder)
280
+ const hasFiles = (e: DragEvent) => e.dataTransfer?.types?.includes('Files') ?? false
281
+
418
282
  const handleDragOver = useCallback((e: DragEvent) => {
283
+ if (!hasFiles(e)) return
419
284
  e.preventDefault()
420
285
  e.stopPropagation()
421
286
  setIsDragging(true)
422
287
  }, [])
423
288
 
424
289
  const handleDragLeave = useCallback((e: DragEvent) => {
290
+ if (!hasFiles(e)) return
425
291
  e.preventDefault()
426
292
  e.stopPropagation()
427
293
  setIsDragging(false)
428
294
  }, [])
429
295
 
430
296
  const handleDrop = useCallback(async (e: DragEvent) => {
297
+ // Only handle external file drops — let ProseMirror handle internal drags (e.g. node reorder)
298
+ if (!hasFiles(e)) return
299
+
431
300
  e.preventDefault()
432
301
  e.stopPropagation()
433
302
  setIsDragging(false)
434
303
 
435
- const file = e.dataTransfer?.files[0]
304
+ const files = e.dataTransfer?.files
305
+ if (!files || files.length === 0) return
306
+
307
+ const file = files[0]
436
308
  if (!file || !file.type.startsWith('image/')) {
437
309
  showToast('Please drop an image file', 'error')
438
310
  return
@@ -811,9 +683,6 @@ export function MarkdownInlineEditor({
811
683
 
812
684
  {/* MDX Component Picker */}
813
685
  {isMdx && <MdxComponentPicker onInsert={handleInsertMdxComponent} />}
814
-
815
- {/* MDX Props Editor */}
816
- {isMdx && <MdxPropsEditor onUpdateProps={handleUpdateMdxProps} />}
817
686
  </div>
818
687
  )
819
688
  }