@nuasite/cms 0.39.1 → 0.40.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/dist/editor.js +14575 -13938
- package/package.json +1 -1
- package/src/build-processor.ts +1 -1
- package/src/collection-scanner.ts +49 -2
- package/src/dev-middleware.ts +1 -1
- package/src/editor/components/attribute-editor.tsx +0 -1
- package/src/editor/components/bg-image-overlay.tsx +7 -8
- package/src/editor/components/block-editor.tsx +12 -12
- package/src/editor/components/collections-browser.tsx +10 -10
- package/src/editor/components/create-page-modal.tsx +18 -18
- package/src/editor/components/delete-page-dialog.tsx +4 -3
- package/src/editor/components/field-utils.ts +54 -0
- package/src/editor/components/fields.tsx +254 -72
- package/src/editor/components/frontmatter-fields.tsx +135 -54
- package/src/editor/components/frontmatter-sidebar.tsx +55 -58
- package/src/editor/components/link-edit-popover.tsx +10 -5
- package/src/editor/components/markdown-editor-overlay.tsx +100 -39
- package/src/editor/components/markdown-inline-editor.tsx +58 -26
- package/src/editor/components/mdx-block-view.tsx +4 -4
- package/src/editor/components/mdx-component-picker.tsx +2 -2
- package/src/editor/components/media-library.tsx +19 -18
- package/src/editor/components/modal-shell.tsx +16 -3
- package/src/editor/components/prop-editor.tsx +15 -18
- package/src/editor/components/redirects-manager.tsx +42 -35
- package/src/editor/components/reference-picker.tsx +5 -4
- package/src/editor/components/seo-editor.tsx +36 -27
- package/src/editor/components/toolbar.tsx +50 -33
- package/src/editor/dom.ts +13 -2
- package/src/editor/editor.ts +7 -6
- package/src/editor/hooks/useBlockEditorHandlers.ts +7 -6
- package/src/editor/index.tsx +7 -6
- package/src/editor/signals.ts +44 -13
- package/src/editor/strings.ts +123 -0
- package/src/editor/styles.css +75 -2
- package/src/editor/types.ts +8 -0
- package/src/index.ts +6 -0
- package/src/source-finder/image-finder.ts +1 -1
- package/src/source-finder/search-index.ts +12 -4
- package/src/source-finder/snippet-utils.ts +4 -1
- package/src/types.ts +4 -0
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
|
3
3
|
import { slugify } from '../../shared'
|
|
4
4
|
import { updateMarkdownPage } from '../api'
|
|
5
5
|
import { STORAGE_KEYS, Z_INDEX } from '../constants'
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
6
7
|
import { createMarkdownPage } from '../markdown-api'
|
|
7
8
|
import { MDX_EXPR_PREFIX } from '../milkdown-mdx-plugin'
|
|
8
9
|
import {
|
|
@@ -13,11 +14,14 @@ import {
|
|
|
13
14
|
markdownEditorState,
|
|
14
15
|
pendingCollectionEntries,
|
|
15
16
|
resetMarkdownEditorState,
|
|
17
|
+
showConfirmDialog,
|
|
16
18
|
showToast,
|
|
17
19
|
startRedirectCountdown,
|
|
18
20
|
updateMarkdownFrontmatter,
|
|
19
21
|
} from '../signals'
|
|
20
22
|
import { clearMarkdownDraft } from '../storage'
|
|
23
|
+
import { STRINGS } from '../strings'
|
|
24
|
+
import type { FieldDefinition } from '../types'
|
|
21
25
|
import { CreateModeFrontmatter, EditModeFrontmatter } from './frontmatter-fields'
|
|
22
26
|
import { FrontmatterSidebar, partitionFields } from './frontmatter-sidebar'
|
|
23
27
|
import { MarkdownInlineEditor } from './markdown-inline-editor'
|
|
@@ -36,9 +40,16 @@ export function MarkdownEditorOverlay() {
|
|
|
36
40
|
const collectionDef = editorState.collectionDefinition
|
|
37
41
|
|
|
38
42
|
const activeCollectionDef = isCreateMode ? createOptions?.collectionDefinition : collectionDef
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// Always surface a Draft toggle in the sidebar — even when the schema scanner
|
|
44
|
+
// didn't pick it up (e.g. inferred schemas where draft only appears on some entries).
|
|
45
|
+
// Push a proper FieldDefinition into the input list so partitionFields() handles
|
|
46
|
+
// placement (above the Date field) via its existing logic.
|
|
47
|
+
const fields: FieldDefinition[] = activeCollectionDef ? [...activeCollectionDef.fields] : []
|
|
48
|
+
const draftInPage = page && Object.hasOwn(page.frontmatter, 'draft')
|
|
49
|
+
if (draftInPage && !fields.some((f) => f.name === 'draft')) {
|
|
50
|
+
fields.push({ name: 'draft', type: 'boolean', required: false, position: 'sidebar', role: 'publish-toggle' })
|
|
51
|
+
}
|
|
52
|
+
const { sidebar: sidebarFields, header: headerFields } = partitionFields(fields)
|
|
42
53
|
const hasSidebar = sidebarFields.length > 0
|
|
43
54
|
const isDataCollection = activeCollectionDef?.type === 'data'
|
|
44
55
|
// Derive MDX mode from the actual file extension when available
|
|
@@ -58,14 +69,19 @@ export function MarkdownEditorOverlay() {
|
|
|
58
69
|
const previewTargetRef = useRef<HTMLElement | null>(null)
|
|
59
70
|
const editorInstanceRef = useRef<Editor | null>(null)
|
|
60
71
|
|
|
61
|
-
// Lock page scroll while the modal overlay is visible (not during preview)
|
|
72
|
+
// Lock page scroll while the modal overlay is visible (not during preview).
|
|
73
|
+
// scrollbar-gutter: stable reserves space for the scrollbar so the page
|
|
74
|
+
// doesn't shift horizontally when overflow flips to hidden — no manual padding math.
|
|
62
75
|
useEffect(() => {
|
|
63
76
|
if (!page || isPreview) return
|
|
64
77
|
const html = document.documentElement
|
|
65
78
|
const prevOverflow = html.style.overflow
|
|
79
|
+
const prevGutter = html.style.scrollbarGutter
|
|
66
80
|
html.style.overflow = 'hidden'
|
|
81
|
+
html.style.scrollbarGutter = 'stable'
|
|
67
82
|
return () => {
|
|
68
83
|
html.style.overflow = prevOverflow
|
|
84
|
+
html.style.scrollbarGutter = prevGutter
|
|
69
85
|
}
|
|
70
86
|
}, [!!page, isPreview])
|
|
71
87
|
|
|
@@ -165,17 +181,17 @@ export function MarkdownEditorOverlay() {
|
|
|
165
181
|
setIsSaving(false)
|
|
166
182
|
|
|
167
183
|
clearMarkdownDraft(currentPage.filePath)
|
|
168
|
-
showToast(
|
|
184
|
+
showToast(STRINGS.markdown.saveSuccess, 'success')
|
|
169
185
|
// Clear pending entry navigation so editor doesn't auto-open after save
|
|
170
186
|
sessionStorage.removeItem(STORAGE_KEYS.PENDING_ENTRY_NAVIGATION)
|
|
171
187
|
resetMarkdownEditorState()
|
|
172
188
|
} else {
|
|
173
|
-
showToast(result.error ||
|
|
189
|
+
showToast(result.error || STRINGS.markdown.saveFailed, 'error')
|
|
174
190
|
setIsSaving(false)
|
|
175
191
|
}
|
|
176
192
|
} catch (error) {
|
|
177
193
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
178
|
-
showToast(
|
|
194
|
+
showToast(STRINGS.markdown.saveFailedDetails(message), 'error')
|
|
179
195
|
setIsSaving(false)
|
|
180
196
|
}
|
|
181
197
|
},
|
|
@@ -190,13 +206,13 @@ export function MarkdownEditorOverlay() {
|
|
|
190
206
|
|
|
191
207
|
const title = (currentPage.frontmatter.title as string) || ''
|
|
192
208
|
if (!title.trim()) {
|
|
193
|
-
showToast(
|
|
209
|
+
showToast(STRINGS.markdown.titleRequired, 'error')
|
|
194
210
|
return
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
const slug = currentPage.slug || slugify(title)
|
|
198
214
|
if (!slug) {
|
|
199
|
-
showToast(
|
|
215
|
+
showToast(STRINGS.markdown.slugRequired, 'error')
|
|
200
216
|
return
|
|
201
217
|
}
|
|
202
218
|
|
|
@@ -236,17 +252,17 @@ export function MarkdownEditorOverlay() {
|
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
|
|
239
|
-
showToast(
|
|
255
|
+
showToast(STRINGS.markdown.pageCreated, 'success')
|
|
240
256
|
resetMarkdownEditorState()
|
|
241
257
|
if (redirectUrl) {
|
|
242
258
|
startRedirectCountdown(redirectUrl, title.trim())
|
|
243
259
|
}
|
|
244
260
|
} else {
|
|
245
|
-
showToast(result.error ||
|
|
261
|
+
showToast(result.error || STRINGS.markdown.createFailed, 'error')
|
|
246
262
|
}
|
|
247
263
|
} catch (error) {
|
|
248
264
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
249
|
-
showToast(
|
|
265
|
+
showToast(STRINGS.markdown.createFailedDetails(message), 'error')
|
|
250
266
|
} finally {
|
|
251
267
|
setIsSaving(false)
|
|
252
268
|
}
|
|
@@ -260,7 +276,7 @@ export function MarkdownEditorOverlay() {
|
|
|
260
276
|
// Enter preview — inject editor HTML into the markdown wrapper element.
|
|
261
277
|
const el = findMarkdownWrapper()
|
|
262
278
|
if (!el) {
|
|
263
|
-
showToast(
|
|
279
|
+
showToast(STRINGS.markdown.previewElementMissing, 'error')
|
|
264
280
|
return
|
|
265
281
|
}
|
|
266
282
|
originalHTMLRef.current = el.innerHTML
|
|
@@ -307,7 +323,7 @@ export function MarkdownEditorOverlay() {
|
|
|
307
323
|
console.error('Failed to get editor HTML for preview:', error)
|
|
308
324
|
originalHTMLRef.current = null
|
|
309
325
|
previewTargetRef.current = null
|
|
310
|
-
showToast(
|
|
326
|
+
showToast(STRINGS.markdown.previewGenerationFailed, 'error')
|
|
311
327
|
return
|
|
312
328
|
}
|
|
313
329
|
setIsPreview(true)
|
|
@@ -320,7 +336,18 @@ export function MarkdownEditorOverlay() {
|
|
|
320
336
|
}
|
|
321
337
|
}, [isPreview, restoreOriginalHTML, findMarkdownWrapper])
|
|
322
338
|
|
|
323
|
-
const handleCancel = useCallback(() => {
|
|
339
|
+
const handleCancel = useCallback(async () => {
|
|
340
|
+
const hasUnsavedChanges = currentMarkdownPage.value?.isDirty === true
|
|
341
|
+
if (hasUnsavedChanges) {
|
|
342
|
+
const confirmed = await showConfirmDialog({
|
|
343
|
+
title: STRINGS.dialog.discardMarkdown.title,
|
|
344
|
+
message: STRINGS.dialog.discardMarkdown.message,
|
|
345
|
+
confirmLabel: STRINGS.dialog.discardMarkdown.confirm,
|
|
346
|
+
cancelLabel: STRINGS.dialog.discardMarkdown.cancel,
|
|
347
|
+
variant: 'danger',
|
|
348
|
+
})
|
|
349
|
+
if (!confirmed) return
|
|
350
|
+
}
|
|
324
351
|
restoreOriginalHTML()
|
|
325
352
|
isMarkdownPreview.value = false
|
|
326
353
|
resetMarkdownEditorState()
|
|
@@ -390,15 +417,16 @@ export function MarkdownEditorOverlay() {
|
|
|
390
417
|
return (
|
|
391
418
|
<div
|
|
392
419
|
style={{ zIndex: Z_INDEX.MODAL }}
|
|
393
|
-
class="fixed inset-0 bg-black/40 flex items-
|
|
420
|
+
class="fixed inset-0 bg-black/40 flex items-start justify-center p-4 pt-[5vh] backdrop-blur-md"
|
|
394
421
|
data-cms-ui
|
|
395
422
|
onMouseDown={stopPropagation}
|
|
396
423
|
onClick={stopPropagation}
|
|
397
424
|
>
|
|
398
425
|
<form
|
|
399
|
-
class={
|
|
400
|
-
|
|
401
|
-
|
|
426
|
+
class={cn(
|
|
427
|
+
'bg-cms-dark rounded-cms-xl shadow-[0_8px_32px_rgba(0,0,0,0.4)] border border-white/10 w-full max-h-[90vh] flex flex-col overflow-hidden',
|
|
428
|
+
hasSidebar ? 'max-w-6xl' : 'max-w-4xl',
|
|
429
|
+
)}
|
|
402
430
|
data-cms-ui
|
|
403
431
|
onSubmit={(e) => {
|
|
404
432
|
e.preventDefault()
|
|
@@ -436,6 +464,11 @@ export function MarkdownEditorOverlay() {
|
|
|
436
464
|
<span class="text-base font-semibold text-white truncate">
|
|
437
465
|
{(page.frontmatter.title as string) || (page.frontmatter.name as string) || (isDataCollection ? 'Entry name' : 'Page title')}
|
|
438
466
|
</span>
|
|
467
|
+
{activeCollectionDef && (
|
|
468
|
+
<span class="shrink-0 px-2 py-0.5 text-xs font-medium text-white/70 bg-white/10 rounded-full border border-white/15">
|
|
469
|
+
{activeCollectionDef.label}
|
|
470
|
+
</span>
|
|
471
|
+
)}
|
|
439
472
|
</div>
|
|
440
473
|
<div class="flex items-center gap-2 shrink-0">
|
|
441
474
|
{!isDataCollection && (
|
|
@@ -467,7 +500,7 @@ export function MarkdownEditorOverlay() {
|
|
|
467
500
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
468
501
|
/>
|
|
469
502
|
</svg>
|
|
470
|
-
Metadata
|
|
503
|
+
{showFrontmatter ? 'Hide' : 'Show'} Metadata
|
|
471
504
|
<svg
|
|
472
505
|
class={`w-3.5 h-3.5 transition-transform ${showFrontmatter ? 'rotate-180' : ''}`}
|
|
473
506
|
fill="none"
|
|
@@ -530,26 +563,54 @@ export function MarkdownEditorOverlay() {
|
|
|
530
563
|
{/* Main: frontmatter header + editor */}
|
|
531
564
|
<div class="flex-1 min-w-0 flex flex-col">
|
|
532
565
|
{/* Frontmatter Editor (header-positioned fields only) */}
|
|
533
|
-
{
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
566
|
+
{isDataCollection
|
|
567
|
+
? (
|
|
568
|
+
<div class="px-5 py-4 border-b border-white/10 bg-white/5 overflow-y-auto flex-1">
|
|
569
|
+
{isCreateMode && createOptions
|
|
570
|
+
? (
|
|
571
|
+
<CreateModeFrontmatter
|
|
572
|
+
page={page}
|
|
573
|
+
collectionDefinition={createOptions.collectionDefinition}
|
|
574
|
+
fields={headerFields}
|
|
575
|
+
onSlugManualEdit={() => setSlugManuallyEdited(true)}
|
|
576
|
+
/>
|
|
577
|
+
)
|
|
578
|
+
: (
|
|
579
|
+
<EditModeFrontmatter
|
|
580
|
+
page={page}
|
|
581
|
+
collectionDefinition={collectionDef}
|
|
582
|
+
fields={headerFields}
|
|
583
|
+
/>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
586
|
+
)
|
|
587
|
+
: (
|
|
588
|
+
<div
|
|
589
|
+
class={cn(
|
|
590
|
+
'overflow-hidden transition-[max-height,opacity,border-bottom-width] duration-300 ease-out border-white/10',
|
|
591
|
+
showFrontmatter ? 'max-h-[40vh] opacity-100 border-b' : 'max-h-0 opacity-0 border-b-0',
|
|
550
592
|
)}
|
|
551
|
-
|
|
552
|
-
|
|
593
|
+
>
|
|
594
|
+
<div class="px-5 py-4 bg-white/5 overflow-y-auto max-h-[40vh]" style={{ scrollbarGutter: 'stable' }}>
|
|
595
|
+
{isCreateMode && createOptions
|
|
596
|
+
? (
|
|
597
|
+
<CreateModeFrontmatter
|
|
598
|
+
page={page}
|
|
599
|
+
collectionDefinition={createOptions.collectionDefinition}
|
|
600
|
+
fields={headerFields}
|
|
601
|
+
onSlugManualEdit={() => setSlugManuallyEdited(true)}
|
|
602
|
+
/>
|
|
603
|
+
)
|
|
604
|
+
: (
|
|
605
|
+
<EditModeFrontmatter
|
|
606
|
+
page={page}
|
|
607
|
+
collectionDefinition={collectionDef}
|
|
608
|
+
fields={headerFields}
|
|
609
|
+
/>
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
)}
|
|
553
614
|
|
|
554
615
|
{/* Editor — hidden for data collections (JSON/YAML have no body) */}
|
|
555
616
|
{!isDataCollection && (
|
|
@@ -12,11 +12,14 @@ import {
|
|
|
12
12
|
import { gfm, toggleStrikethroughCommand } from '@milkdown/preset-gfm'
|
|
13
13
|
import { callCommand, insert, replaceAll } from '@milkdown/utils'
|
|
14
14
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
15
|
+
import { lift } from 'prosemirror-commands'
|
|
15
16
|
import { useLinkPopover } from '../hooks/useLinkPopover'
|
|
17
|
+
import { cn } from '../lib/cn'
|
|
16
18
|
import { uploadMedia } from '../markdown-api'
|
|
17
19
|
import { insertMdxComponentCommand, mdxComponentPlugin } from '../milkdown-mdx-plugin'
|
|
18
20
|
import { type ActiveFormats, defaultActiveFormats, isInListType, setupFormatTracking, toggleHeading } from '../milkdown-utils'
|
|
19
21
|
import { config, mdxComponentPickerOpen, openMediaLibraryWithCallback, resetMarkdownEditorState, showToast, updateMarkdownContent } from '../signals'
|
|
22
|
+
import { STRINGS } from '../strings'
|
|
20
23
|
import { LinkEditPopover } from './link-edit-popover'
|
|
21
24
|
import { MdxComponentIcon } from './mdx-block-view'
|
|
22
25
|
import { MdxComponentPicker } from './mdx-component-picker'
|
|
@@ -108,7 +111,7 @@ export function MarkdownInlineEditor({
|
|
|
108
111
|
cleanupTracking = setupFormatTracking(editor, setActiveFormats)
|
|
109
112
|
} catch (error) {
|
|
110
113
|
console.error('Milkdown editor initialization failed:', error)
|
|
111
|
-
showToast(
|
|
114
|
+
showToast(STRINGS.markdown.initFailed, 'error')
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
@@ -176,10 +179,29 @@ export function MarkdownInlineEditor({
|
|
|
176
179
|
() => runCommand(toggleStrikethroughCommand.key),
|
|
177
180
|
[runCommand],
|
|
178
181
|
)
|
|
179
|
-
const handleQuote = useCallback(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
const handleQuote = useCallback(() => {
|
|
183
|
+
if (!editorInstanceRef.current) return
|
|
184
|
+
try {
|
|
185
|
+
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
186
|
+
const { $from } = view.state.selection
|
|
187
|
+
let inBlockquote = false
|
|
188
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
189
|
+
if ($from.node(depth).type.name === 'blockquote') {
|
|
190
|
+
inBlockquote = true
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (inBlockquote) {
|
|
195
|
+
// Lift selection out of the blockquote (may need multiple lifts for nested wrappers)
|
|
196
|
+
lift(view.state, view.dispatch)
|
|
197
|
+
view.focus()
|
|
198
|
+
} else {
|
|
199
|
+
runCommand(wrapInBlockquoteCommand.key)
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
runCommand(wrapInBlockquoteCommand.key)
|
|
203
|
+
}
|
|
204
|
+
}, [runCommand])
|
|
183
205
|
|
|
184
206
|
// Check if selection is inside a list of given type
|
|
185
207
|
const checkInList = useCallback(
|
|
@@ -269,7 +291,7 @@ export function MarkdownInlineEditor({
|
|
|
269
291
|
|
|
270
292
|
const file = files[0]
|
|
271
293
|
if (!file || !file.type.startsWith('image/')) {
|
|
272
|
-
showToast(
|
|
294
|
+
showToast(STRINGS.media.imageRequired, 'error')
|
|
273
295
|
return
|
|
274
296
|
}
|
|
275
297
|
|
|
@@ -288,16 +310,16 @@ export function MarkdownInlineEditor({
|
|
|
288
310
|
if (editorInstanceRef.current) {
|
|
289
311
|
try {
|
|
290
312
|
editorInstanceRef.current.action(insert(imageMarkdown))
|
|
291
|
-
showToast(
|
|
313
|
+
showToast(STRINGS.media.imageInserted, 'success')
|
|
292
314
|
} catch (error) {
|
|
293
315
|
console.error('Failed to insert image:', error)
|
|
294
316
|
}
|
|
295
317
|
}
|
|
296
318
|
} else {
|
|
297
|
-
showToast(result.error ||
|
|
319
|
+
showToast(result.error || STRINGS.media.uploadFailed, 'error')
|
|
298
320
|
}
|
|
299
321
|
} catch (error) {
|
|
300
|
-
showToast(
|
|
322
|
+
showToast(STRINGS.media.uploadFailed, 'error')
|
|
301
323
|
} finally {
|
|
302
324
|
setUploadProgress(null)
|
|
303
325
|
}
|
|
@@ -326,13 +348,13 @@ export function MarkdownInlineEditor({
|
|
|
326
348
|
|
|
327
349
|
if (editorInstanceRef.current) {
|
|
328
350
|
editorInstanceRef.current.action(insert(imageMarkdown))
|
|
329
|
-
showToast(
|
|
351
|
+
showToast(STRINGS.media.imageInserted, 'success')
|
|
330
352
|
}
|
|
331
353
|
} else {
|
|
332
|
-
showToast(result.error ||
|
|
354
|
+
showToast(result.error || STRINGS.media.uploadFailed, 'error')
|
|
333
355
|
}
|
|
334
356
|
} catch (error) {
|
|
335
|
-
showToast(
|
|
357
|
+
showToast(STRINGS.media.uploadFailed, 'error')
|
|
336
358
|
} finally {
|
|
337
359
|
setUploadProgress(null)
|
|
338
360
|
}
|
|
@@ -422,6 +444,7 @@ export function MarkdownInlineEditor({
|
|
|
422
444
|
onClick={() => handleInsertHeading(1)}
|
|
423
445
|
title="Heading 1"
|
|
424
446
|
active={activeFormats.heading === 1}
|
|
447
|
+
compact
|
|
425
448
|
>
|
|
426
449
|
<span class="text-xs font-bold">H1</span>
|
|
427
450
|
</ToolbarButton>
|
|
@@ -429,6 +452,7 @@ export function MarkdownInlineEditor({
|
|
|
429
452
|
onClick={() => handleInsertHeading(2)}
|
|
430
453
|
title="Heading 2"
|
|
431
454
|
active={activeFormats.heading === 2}
|
|
455
|
+
compact
|
|
432
456
|
>
|
|
433
457
|
<span class="text-xs font-bold">H2</span>
|
|
434
458
|
</ToolbarButton>
|
|
@@ -436,6 +460,7 @@ export function MarkdownInlineEditor({
|
|
|
436
460
|
onClick={() => handleInsertHeading(3)}
|
|
437
461
|
title="Heading 3"
|
|
438
462
|
active={activeFormats.heading === 3}
|
|
463
|
+
compact
|
|
439
464
|
>
|
|
440
465
|
<span class="text-xs font-bold">H3</span>
|
|
441
466
|
</ToolbarButton>
|
|
@@ -443,6 +468,7 @@ export function MarkdownInlineEditor({
|
|
|
443
468
|
onClick={() => handleInsertHeading(4)}
|
|
444
469
|
title="Heading 4"
|
|
445
470
|
active={activeFormats.heading === 4}
|
|
471
|
+
compact
|
|
446
472
|
>
|
|
447
473
|
<span class="text-xs font-bold">H4</span>
|
|
448
474
|
</ToolbarButton>
|
|
@@ -583,17 +609,6 @@ export function MarkdownInlineEditor({
|
|
|
583
609
|
</div>
|
|
584
610
|
</div>
|
|
585
611
|
|
|
586
|
-
{/* Link edit popover — rendered outside the toolbar stacking context so it layers above the sidebar */}
|
|
587
|
-
{linkPopoverState && (
|
|
588
|
-
<LinkEditPopover
|
|
589
|
-
initialUrl={linkPopoverState.href}
|
|
590
|
-
suggestions={pageSuggestions}
|
|
591
|
-
onApply={applyLink}
|
|
592
|
-
onRemove={linkPopoverState.isEdit ? removeLink : undefined}
|
|
593
|
-
onClose={closeLinkPopover}
|
|
594
|
-
/>
|
|
595
|
-
)}
|
|
596
|
-
|
|
597
612
|
{/* Editor */}
|
|
598
613
|
<div
|
|
599
614
|
class={`flex-1 min-h-0 overflow-auto relative transition-colors ${isDragging ? 'bg-cms-primary/10' : ''}`}
|
|
@@ -602,6 +617,18 @@ export function MarkdownInlineEditor({
|
|
|
602
617
|
onDrop={handleDrop}
|
|
603
618
|
onPaste={handlePaste}
|
|
604
619
|
>
|
|
620
|
+
{/* Link edit popover — overlays the top of the editor so it doesn't push content */}
|
|
621
|
+
{linkPopoverState && (
|
|
622
|
+
<div class="absolute top-0 left-0 right-0 z-40">
|
|
623
|
+
<LinkEditPopover
|
|
624
|
+
initialUrl={linkPopoverState.href}
|
|
625
|
+
suggestions={pageSuggestions}
|
|
626
|
+
onApply={applyLink}
|
|
627
|
+
onRemove={linkPopoverState.isEdit ? removeLink : undefined}
|
|
628
|
+
onClose={closeLinkPopover}
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
)}
|
|
605
632
|
<div
|
|
606
633
|
ref={editorRef}
|
|
607
634
|
class="milkdown-editor milkdown-dark prose prose-invert prose-sm max-w-none p-6 min-h-75 focus:outline-none"
|
|
@@ -666,6 +693,8 @@ interface ToolbarButtonProps {
|
|
|
666
693
|
title: string
|
|
667
694
|
children: preact.ComponentChildren
|
|
668
695
|
active?: boolean
|
|
696
|
+
/** Use tighter padding suitable for text-only labels like H1/H2 */
|
|
697
|
+
compact?: boolean
|
|
669
698
|
}
|
|
670
699
|
|
|
671
700
|
function ToolbarButton({
|
|
@@ -673,16 +702,19 @@ function ToolbarButton({
|
|
|
673
702
|
title,
|
|
674
703
|
children,
|
|
675
704
|
active,
|
|
705
|
+
compact,
|
|
676
706
|
}: ToolbarButtonProps) {
|
|
677
707
|
return (
|
|
678
708
|
<button
|
|
679
709
|
type="button"
|
|
680
710
|
onClick={onClick}
|
|
681
|
-
class={
|
|
711
|
+
class={cn(
|
|
712
|
+
'rounded-cms-sm transition-colors',
|
|
713
|
+
compact ? 'px-2 pt-0.5 pb-1' : 'p-2',
|
|
682
714
|
active
|
|
683
715
|
? 'bg-cms-primary text-cms-primary-text'
|
|
684
|
-
: 'hover:bg-white/10 text-white/70 hover:text-white'
|
|
685
|
-
}
|
|
716
|
+
: 'hover:bg-white/10 text-white/70 hover:text-white',
|
|
717
|
+
)}
|
|
686
718
|
title={title}
|
|
687
719
|
data-cms-ui
|
|
688
720
|
>
|
|
@@ -302,7 +302,7 @@ function InlineInput({ value, onChange, placeholder }: { value: string; onChange
|
|
|
302
302
|
if (el.value !== value) onChange(el.value)
|
|
303
303
|
}}
|
|
304
304
|
placeholder={placeholder}
|
|
305
|
-
class="w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white/80 placeholder:text-white/30 outline-none focus:border-white/
|
|
305
|
+
class="w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white/80 placeholder:text-white/30 outline-none focus:border-white/40 transition-colors"
|
|
306
306
|
/>
|
|
307
307
|
)
|
|
308
308
|
}
|
|
@@ -317,7 +317,7 @@ const INLINE_INPUT_TYPES: Record<string, string> = {
|
|
|
317
317
|
tel: 'tel',
|
|
318
318
|
}
|
|
319
319
|
const inputClass =
|
|
320
|
-
'w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white/80 placeholder:text-white/30 outline-none focus:border-white/
|
|
320
|
+
'w-full bg-white/5 border border-white/10 rounded-cms-sm px-2.5 py-1.5 text-[13px] text-white/80 placeholder:text-white/30 outline-none focus:border-white/40 transition-colors'
|
|
321
321
|
|
|
322
322
|
function InlinePropField(
|
|
323
323
|
{ name, value, propDef, onChange }: { name: string; value: string; propDef?: ComponentProp; onChange: (v: string) => void },
|
|
@@ -434,7 +434,7 @@ export function MdxBlockCard({ componentName, props, hasExpressions, slotContent
|
|
|
434
434
|
data-cms-ui
|
|
435
435
|
>
|
|
436
436
|
{/* Header */}
|
|
437
|
-
<div class="flex items-center justify-between px-4 py-2
|
|
437
|
+
<div class="flex items-center justify-between px-4 py-2 bg-white/5 border-b border-white/10 rounded-t-cms-md">
|
|
438
438
|
<div class="flex items-center gap-2">
|
|
439
439
|
<MdxComponentIcon />
|
|
440
440
|
<span class="text-[13px] font-semibold text-white">{componentName}</span>
|
|
@@ -461,7 +461,7 @@ export function MdxBlockCard({ componentName, props, hasExpressions, slotContent
|
|
|
461
461
|
|
|
462
462
|
{/* Slot content editor */}
|
|
463
463
|
{hasSlotContent && (
|
|
464
|
-
<div class="px-4 py-2
|
|
464
|
+
<div class="px-4 py-2 border-b border-white/10" data-mdx-action="children">
|
|
465
465
|
<MiniMilkdownEditor
|
|
466
466
|
value={slotContent || ''}
|
|
467
467
|
onChange={onSlotContentChange}
|
|
@@ -88,7 +88,7 @@ export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
|
88
88
|
onInput={(e) => setChildrenValue((e.target as HTMLTextAreaElement).value)}
|
|
89
89
|
placeholder="Enter content..."
|
|
90
90
|
rows={3}
|
|
91
|
-
class="w-full px-4 py-2
|
|
91
|
+
class="w-full px-4 py-2 bg-white/10 border border-white/20 text-[13px] text-white placeholder:text-white/40 outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10 transition-all rounded-cms-md resize-y"
|
|
92
92
|
/>
|
|
93
93
|
</div>
|
|
94
94
|
)}
|
|
@@ -121,7 +121,7 @@ export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
|
121
121
|
value={searchQuery}
|
|
122
122
|
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}
|
|
123
123
|
placeholder="Search components..."
|
|
124
|
-
class="w-full px-4 py-2
|
|
124
|
+
class="w-full px-4 py-2 bg-white/10 border border-white/20 text-[13px] text-white placeholder:text-white/40 outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10 transition-all rounded-cms-md"
|
|
125
125
|
/>
|
|
126
126
|
</div>
|
|
127
127
|
<div class="p-5 overflow-y-auto flex-1">
|
|
@@ -3,6 +3,7 @@ import { Z_INDEX } from '../constants'
|
|
|
3
3
|
import { useSearchFilter } from '../hooks/useSearchFilter'
|
|
4
4
|
import { createMediaFolder, fetchMediaLibrary, fetchProjectImages, uploadMedia } from '../markdown-api'
|
|
5
5
|
import { config, isMediaLibraryOpen, mediaLibraryState, resetMediaLibraryState, showToast } from '../signals'
|
|
6
|
+
import { STRINGS } from '../strings'
|
|
6
7
|
import type { MediaFolderItem, MediaItem, MediaTypeFilter } from '../types'
|
|
7
8
|
import { CloseButton, PrimaryButton } from './modal-shell'
|
|
8
9
|
import { Spinner } from './spinner'
|
|
@@ -77,7 +78,7 @@ export function MediaLibrary() {
|
|
|
77
78
|
}
|
|
78
79
|
setAllItems(combined)
|
|
79
80
|
} catch (error) {
|
|
80
|
-
showToast(
|
|
81
|
+
showToast(STRINGS.media.loadFailed, 'error')
|
|
81
82
|
} finally {
|
|
82
83
|
setIsLoading(false)
|
|
83
84
|
}
|
|
@@ -129,7 +130,7 @@ export function MediaLibrary() {
|
|
|
129
130
|
if (uploadContext && result.url.startsWith('./')) {
|
|
130
131
|
insertCallback?.(result.url, '')
|
|
131
132
|
handleClose()
|
|
132
|
-
showToast(
|
|
133
|
+
showToast(STRINGS.media.uploadedNextToEntry, 'success')
|
|
133
134
|
return
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -142,12 +143,12 @@ export function MediaLibrary() {
|
|
|
142
143
|
folder: currentFolder || undefined,
|
|
143
144
|
}
|
|
144
145
|
setAllItems((prev) => [newItem, ...prev])
|
|
145
|
-
showToast(
|
|
146
|
+
showToast(STRINGS.media.uploadSucceeded, 'success')
|
|
146
147
|
} else {
|
|
147
|
-
showToast(result.error ||
|
|
148
|
+
showToast(result.error || STRINGS.media.uploadFailed, 'error')
|
|
148
149
|
}
|
|
149
150
|
} catch (error) {
|
|
150
|
-
showToast(
|
|
151
|
+
showToast(STRINGS.media.uploadFailed, 'error')
|
|
151
152
|
} finally {
|
|
152
153
|
setUploadProgress(null)
|
|
153
154
|
}
|
|
@@ -167,7 +168,7 @@ export function MediaLibrary() {
|
|
|
167
168
|
|
|
168
169
|
const file = e.dataTransfer?.files[0]
|
|
169
170
|
if (!file || !file.type.startsWith('image/')) {
|
|
170
|
-
showToast(
|
|
171
|
+
showToast(STRINGS.media.imageRequired, 'error')
|
|
171
172
|
return
|
|
172
173
|
}
|
|
173
174
|
await handleUploadFile(file)
|
|
@@ -182,7 +183,7 @@ export function MediaLibrary() {
|
|
|
182
183
|
const name = newFolderName.trim()
|
|
183
184
|
if (!name) return
|
|
184
185
|
if (/[/\\:*?"<>|]/.test(name)) {
|
|
185
|
-
showToast(
|
|
186
|
+
showToast(STRINGS.media.invalidFolderName, 'error')
|
|
186
187
|
return
|
|
187
188
|
}
|
|
188
189
|
const folderPath = currentFolder ? `${currentFolder}/${name}` : name
|
|
@@ -190,12 +191,12 @@ export function MediaLibrary() {
|
|
|
190
191
|
const result = await createMediaFolder(config.value, folderPath)
|
|
191
192
|
if (result.success) {
|
|
192
193
|
setFolders((prev) => [...prev, { name, path: folderPath }].sort((a, b) => a.name.localeCompare(b.name)))
|
|
193
|
-
showToast(
|
|
194
|
+
showToast(STRINGS.media.folderCreated, 'success')
|
|
194
195
|
} else {
|
|
195
|
-
showToast(result.error ||
|
|
196
|
+
showToast(result.error || STRINGS.media.folderCreateFailed, 'error')
|
|
196
197
|
}
|
|
197
198
|
} catch {
|
|
198
|
-
showToast(
|
|
199
|
+
showToast(STRINGS.media.folderCreateFailed, 'error')
|
|
199
200
|
}
|
|
200
201
|
setNewFolderName('')
|
|
201
202
|
setShowNewFolderInput(false)
|
|
@@ -236,7 +237,7 @@ export function MediaLibrary() {
|
|
|
236
237
|
data-cms-ui
|
|
237
238
|
>
|
|
238
239
|
{/* Header */}
|
|
239
|
-
<div class="flex items-center justify-between
|
|
240
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-white/10">
|
|
240
241
|
<h2 class="text-lg font-semibold text-white">Media Library</h2>
|
|
241
242
|
<CloseButton onClick={handleClose} />
|
|
242
243
|
</div>
|
|
@@ -287,17 +288,17 @@ export function MediaLibrary() {
|
|
|
287
288
|
placeholder="Search files..."
|
|
288
289
|
value={searchQuery}
|
|
289
290
|
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}
|
|
290
|
-
class="flex-1 px-4 py-2
|
|
291
|
+
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
|
|
291
292
|
data-cms-ui
|
|
292
293
|
/>
|
|
293
294
|
<button
|
|
294
295
|
type="button"
|
|
295
296
|
onClick={() => setShowNewFolderInput((v) => !v)}
|
|
296
|
-
class="px-3 py-2.5 bg-white/10 text-white/70 rounded-cms-
|
|
297
|
+
class="px-3 py-2.5 bg-white/10 text-white/70 rounded-cms-sm text-sm hover:bg-white/15 hover:text-white transition-colors border border-white/20"
|
|
297
298
|
title="New folder"
|
|
298
299
|
data-cms-ui
|
|
299
300
|
>
|
|
300
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
301
|
+
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
301
302
|
<path
|
|
302
303
|
stroke-linecap="round"
|
|
303
304
|
stroke-linejoin="round"
|
|
@@ -363,10 +364,10 @@ export function MediaLibrary() {
|
|
|
363
364
|
setNewFolderName('')
|
|
364
365
|
}
|
|
365
366
|
}}
|
|
366
|
-
class="flex-1 px-3 py-1.5 bg-white/10 border border-white/20 rounded-cms-
|
|
367
|
+
class="flex-1 px-3 py-1.5 bg-white/10 border border-white/20 rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
|
367
368
|
data-cms-ui
|
|
368
369
|
/>
|
|
369
|
-
<PrimaryButton onClick={handleCreateFolder} className="px-3 py-1.5 rounded-cms-
|
|
370
|
+
<PrimaryButton onClick={handleCreateFolder} className="px-3 py-1.5 rounded-cms-sm text-xs">
|
|
370
371
|
Create
|
|
371
372
|
</PrimaryButton>
|
|
372
373
|
<button
|
|
@@ -430,7 +431,7 @@ export function MediaLibrary() {
|
|
|
430
431
|
<button
|
|
431
432
|
type="button"
|
|
432
433
|
onClick={() => navigateToFolder(folder.path)}
|
|
433
|
-
class="w-full h-full rounded-cms-
|
|
434
|
+
class="w-full h-full rounded-cms-sm overflow-hidden border-2 border-white/10 hover:border-white/30 focus:outline-none focus:border-white/40 transition-all bg-white/5 hover:bg-white/10 flex flex-col items-center justify-center gap-2"
|
|
434
435
|
data-cms-ui
|
|
435
436
|
>
|
|
436
437
|
<svg
|
|
@@ -459,7 +460,7 @@ export function MediaLibrary() {
|
|
|
459
460
|
<button
|
|
460
461
|
type="button"
|
|
461
462
|
onClick={() => handleSelectImage(item)}
|
|
462
|
-
class="w-full h-full rounded-cms-
|
|
463
|
+
class="w-full h-full rounded-cms-sm overflow-hidden border-2 border-white/10 hover:border-cms-primary focus:outline-none focus:border-white/40 transition-all"
|
|
463
464
|
data-cms-ui
|
|
464
465
|
>
|
|
465
466
|
{item.contentType.startsWith('image/')
|