@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.
- package/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- 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\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`
|
|
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\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\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
|
+
}
|