@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.
- package/dist/editor.js +12615 -12689
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +185 -189
- package/src/editor/api.ts +0 -251
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +46 -70
- package/src/editor/components/markdown-inline-editor.tsx +34 -165
- package/src/editor/components/mdx-block-view.tsx +351 -47
- package/src/editor/components/mdx-component-picker.tsx +35 -11
- package/src/editor/components/media-library.tsx +1 -15
- package/src/editor/components/modal-shell.tsx +1 -1
- package/src/editor/components/toolbar.tsx +0 -75
- package/src/editor/constants.ts +0 -4
- package/src/editor/editor.ts +2 -192
- package/src/editor/hooks/index.ts +0 -3
- package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
- package/src/editor/hooks/useTooltipState.ts +1 -2
- package/src/editor/index.tsx +2 -18
- package/src/editor/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/post-message.ts +0 -6
- package/src/editor/signals.ts +0 -183
- package/src/editor/styles.css +0 -108
- package/src/editor/types.ts +0 -76
- package/src/html-processor.ts +9 -7
- package/src/source-finder/cache.ts +47 -0
- package/src/source-finder/collection-finder.ts +181 -0
- package/src/source-finder/index.ts +5 -2
- package/src/source-finder/search-index.ts +79 -0
- package/src/source-finder/snippet-utils.ts +36 -61
- package/src/types.ts +0 -4
- package/src/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/ai.ts +0 -185
- package/src/editor/components/ai-chat.tsx +0 -631
- package/src/editor/components/ai-tooltip.tsx +0 -180
- package/src/editor/components/mdx-props-editor.tsx +0 -94
- package/src/editor/hooks/useAIHandlers.ts +0 -345
package/src/editor/api.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { setAvailableTextStyles } from './text-styling'
|
|
|
4
4
|
import type {
|
|
5
5
|
CmsManifest,
|
|
6
6
|
ComponentInsertOperation,
|
|
7
|
-
DeploymentStatusResponse,
|
|
8
7
|
SaveBatchRequest,
|
|
9
8
|
SaveBatchResponse,
|
|
10
9
|
UpdateMarkdownPageRequest,
|
|
@@ -20,68 +19,6 @@ export interface GetMarkdownContentResponse {
|
|
|
20
19
|
filePath: string
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
/**
|
|
24
|
-
* Types for CMS AI chat communication (SSE events)
|
|
25
|
-
*/
|
|
26
|
-
export interface CmsAiChatRequest {
|
|
27
|
-
prompt: string
|
|
28
|
-
elementId?: string
|
|
29
|
-
currentContent?: string
|
|
30
|
-
pageUrl: string
|
|
31
|
-
/** Source file context for the element being edited */
|
|
32
|
-
context?: string
|
|
33
|
-
sessionId?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type CmsAiEvent =
|
|
37
|
-
| CmsAiTokenEvent
|
|
38
|
-
| CmsAiStatusEvent
|
|
39
|
-
| CmsAiActionEvent
|
|
40
|
-
| CmsAiErrorEvent
|
|
41
|
-
| CmsAiDoneEvent
|
|
42
|
-
|
|
43
|
-
export interface CmsAiTokenEvent {
|
|
44
|
-
type: 'token'
|
|
45
|
-
token: string
|
|
46
|
-
fullText: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface CmsAiStatusEvent {
|
|
50
|
-
type: 'status'
|
|
51
|
-
status: 'thinking' | 'coding' | 'building' | 'deploying' | 'complete'
|
|
52
|
-
message?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface CmsAiActionEvent {
|
|
56
|
-
type: 'action'
|
|
57
|
-
action: CmsAiAction
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type CmsAiAction =
|
|
61
|
-
| { name: 'refresh' }
|
|
62
|
-
| { name: 'preview'; url: string }
|
|
63
|
-
| { name: 'commit'; sha: string; message: string }
|
|
64
|
-
| { name: 'apply-edit'; elementId: string; content: string; htmlContent?: string }
|
|
65
|
-
|
|
66
|
-
export interface CmsAiErrorEvent {
|
|
67
|
-
type: 'error'
|
|
68
|
-
error: string
|
|
69
|
-
code?: string
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface CmsAiDoneEvent {
|
|
73
|
-
type: 'done'
|
|
74
|
-
summary?: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface CmsAiStreamCallbacks {
|
|
78
|
-
onToken?: (token: string, fullText: string) => void
|
|
79
|
-
onStatus?: (status: CmsAiStatusEvent['status'], message?: string) => void
|
|
80
|
-
onAction?: (action: CmsAiAction) => void
|
|
81
|
-
onError?: (error: string, code?: string) => void
|
|
82
|
-
onDone?: (summary?: string) => void
|
|
83
|
-
}
|
|
84
|
-
|
|
85
22
|
/**
|
|
86
23
|
* Get the manifest URL for the current page
|
|
87
24
|
* For example: /about -> /about.json
|
|
@@ -325,194 +262,6 @@ export async function removeComponent(
|
|
|
325
262
|
return res.json()
|
|
326
263
|
}
|
|
327
264
|
|
|
328
|
-
/**
|
|
329
|
-
* Parse SSE data line to event
|
|
330
|
-
*/
|
|
331
|
-
export function parseSseEvent(data: string): CmsAiEvent | null {
|
|
332
|
-
if (data === '[DONE]') {
|
|
333
|
-
return { type: 'done' }
|
|
334
|
-
}
|
|
335
|
-
try {
|
|
336
|
-
return JSON.parse(data) as CmsAiEvent
|
|
337
|
-
} catch {
|
|
338
|
-
return null
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Stream AI chat response from the server using SSE
|
|
344
|
-
* Provides rich events including status updates, actions (refresh, preview), and streaming tokens
|
|
345
|
-
*/
|
|
346
|
-
export async function streamAiChat(
|
|
347
|
-
apiBase: string,
|
|
348
|
-
request: CmsAiChatRequest,
|
|
349
|
-
callbacks: CmsAiStreamCallbacks,
|
|
350
|
-
abortSignal?: AbortSignal,
|
|
351
|
-
): Promise<void> {
|
|
352
|
-
const controller = new AbortController()
|
|
353
|
-
const timeoutId = setTimeout(() => controller.abort(), API.AI_STREAM_TIMEOUT_MS)
|
|
354
|
-
|
|
355
|
-
// Allow external abort signal to also abort the request
|
|
356
|
-
if (abortSignal) {
|
|
357
|
-
abortSignal.addEventListener('abort', () => controller.abort())
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
let doneCalled = false
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
const res = await fetch(`${apiBase}/ai/chat`, {
|
|
364
|
-
method: 'POST',
|
|
365
|
-
credentials: 'include',
|
|
366
|
-
headers: {
|
|
367
|
-
'Content-Type': 'application/json',
|
|
368
|
-
},
|
|
369
|
-
body: JSON.stringify(request),
|
|
370
|
-
signal: controller.signal,
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
if (!res.ok) {
|
|
374
|
-
const text = await res.text().catch(() => '')
|
|
375
|
-
callbacks.onError?.(text || `Request failed (${res.status})`, String(res.status))
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (!res.body) {
|
|
380
|
-
callbacks.onError?.('No response body')
|
|
381
|
-
return
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const reader = res.body.getReader()
|
|
385
|
-
const decoder = new TextDecoder()
|
|
386
|
-
let buffer = ''
|
|
387
|
-
|
|
388
|
-
while (true) {
|
|
389
|
-
const { done, value } = await reader.read()
|
|
390
|
-
|
|
391
|
-
if (done) {
|
|
392
|
-
break
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
buffer += decoder.decode(value, { stream: true })
|
|
396
|
-
const lines = buffer.split('\n')
|
|
397
|
-
|
|
398
|
-
// Keep the last incomplete line in the buffer
|
|
399
|
-
buffer = lines.pop() || ''
|
|
400
|
-
|
|
401
|
-
for (const line of lines) {
|
|
402
|
-
if (line.startsWith('data: ')) {
|
|
403
|
-
const data = line.slice(6).trim()
|
|
404
|
-
if (!data) continue
|
|
405
|
-
|
|
406
|
-
const event = parseSseEvent(data)
|
|
407
|
-
if (!event) continue
|
|
408
|
-
|
|
409
|
-
switch (event.type) {
|
|
410
|
-
case 'token':
|
|
411
|
-
callbacks.onToken?.(event.token, event.fullText)
|
|
412
|
-
break
|
|
413
|
-
case 'status':
|
|
414
|
-
callbacks.onStatus?.(event.status, event.message)
|
|
415
|
-
break
|
|
416
|
-
case 'action':
|
|
417
|
-
callbacks.onAction?.(event.action)
|
|
418
|
-
break
|
|
419
|
-
case 'error':
|
|
420
|
-
callbacks.onError?.(event.error, event.code)
|
|
421
|
-
break
|
|
422
|
-
case 'done':
|
|
423
|
-
if (!doneCalled) {
|
|
424
|
-
doneCalled = true
|
|
425
|
-
callbacks.onDone?.(event.summary)
|
|
426
|
-
}
|
|
427
|
-
return
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Process any remaining buffer
|
|
434
|
-
if (buffer.startsWith('data: ')) {
|
|
435
|
-
const data = buffer.slice(6).trim()
|
|
436
|
-
if (data) {
|
|
437
|
-
const event = parseSseEvent(data)
|
|
438
|
-
if (event?.type === 'done' && !doneCalled) {
|
|
439
|
-
doneCalled = true
|
|
440
|
-
callbacks.onDone?.(event.summary)
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (!doneCalled) {
|
|
446
|
-
doneCalled = true
|
|
447
|
-
callbacks.onDone?.()
|
|
448
|
-
}
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
451
|
-
callbacks.onError?.('Request timed out or was cancelled', 'TIMEOUT')
|
|
452
|
-
return
|
|
453
|
-
}
|
|
454
|
-
callbacks.onError?.(error instanceof Error ? error.message : 'Unknown error')
|
|
455
|
-
} finally {
|
|
456
|
-
clearTimeout(timeoutId)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Chat message type from history endpoint
|
|
462
|
-
*/
|
|
463
|
-
export interface ChatHistoryMessage {
|
|
464
|
-
id: string
|
|
465
|
-
role: 'user' | 'assistant' | 'tool'
|
|
466
|
-
content: string | null
|
|
467
|
-
created_at: string
|
|
468
|
-
channel?: string
|
|
469
|
-
identifier?: string
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export interface ChatHistoryResponse {
|
|
473
|
-
messages: ChatHistoryMessage[]
|
|
474
|
-
hasMore: boolean
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Fetch AI chat history for the current project
|
|
479
|
-
*/
|
|
480
|
-
export async function getChatHistory(
|
|
481
|
-
apiBase: string,
|
|
482
|
-
limit = 50,
|
|
483
|
-
): Promise<ChatHistoryResponse> {
|
|
484
|
-
const res = await fetchWithTimeout(`${apiBase}/ai/chat/history?limit=${limit}`, {
|
|
485
|
-
method: 'GET',
|
|
486
|
-
credentials: 'include',
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
if (!res.ok) {
|
|
490
|
-
const text = await res.text().catch(() => '')
|
|
491
|
-
throw new Error(`Failed to fetch chat history (${res.status}): ${text || res.statusText}`)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return res.json()
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Fetch deployment status for the current project
|
|
499
|
-
*/
|
|
500
|
-
export async function getDeploymentStatus(
|
|
501
|
-
apiBase: string,
|
|
502
|
-
): Promise<DeploymentStatusResponse> {
|
|
503
|
-
const res = await fetchWithTimeout(`${apiBase}/deployment/status`, {
|
|
504
|
-
method: 'GET',
|
|
505
|
-
credentials: 'include',
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
if (!res.ok) {
|
|
509
|
-
const text = await res.text().catch(() => '')
|
|
510
|
-
throw new Error(`Failed to fetch deployment status (${res.status}): ${text || res.statusText}`)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return res.json()
|
|
514
|
-
}
|
|
515
|
-
|
|
516
265
|
/**
|
|
517
266
|
* Fetch markdown content from a file
|
|
518
267
|
*/
|
|
@@ -83,18 +83,18 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
83
83
|
const hasImage = !!value && value.length > 0
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
|
-
<div class="space-y-1.5">
|
|
86
|
+
<div class="space-y-1.5 min-w-0">
|
|
87
87
|
<FieldLabel label={label} isDirty={isDirty} onReset={onReset} />
|
|
88
88
|
{hasImage && (
|
|
89
89
|
<div
|
|
90
|
-
class="relative w-full
|
|
90
|
+
class="relative w-full rounded-cms-sm overflow-hidden bg-white/5 border border-white/10 cursor-pointer group"
|
|
91
91
|
onClick={onBrowse}
|
|
92
92
|
data-cms-ui
|
|
93
93
|
>
|
|
94
94
|
<img
|
|
95
95
|
src={value}
|
|
96
96
|
alt={label}
|
|
97
|
-
class="w-full h-
|
|
97
|
+
class="w-full h-auto max-h-48"
|
|
98
98
|
onError={(e) => {
|
|
99
99
|
;(e.target as HTMLImageElement).style.display = 'none'
|
|
100
100
|
}}
|
|
@@ -104,14 +104,14 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
104
104
|
</div>
|
|
105
105
|
</div>
|
|
106
106
|
)}
|
|
107
|
-
<div class="flex gap-2">
|
|
107
|
+
<div class="flex gap-2 min-w-0">
|
|
108
108
|
<input
|
|
109
109
|
type="text"
|
|
110
110
|
value={value ?? ''}
|
|
111
111
|
placeholder={placeholder}
|
|
112
112
|
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
113
113
|
class={cn(
|
|
114
|
-
'flex-1 px-3 py-2 bg-white/10 border rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:ring-1 transition-colors',
|
|
114
|
+
'flex-1 min-w-0 px-3 py-2 bg-white/10 border rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:ring-1 transition-colors',
|
|
115
115
|
isDirty
|
|
116
116
|
? 'border-cms-primary focus:border-cms-primary focus:ring-cms-primary/30'
|
|
117
117
|
: 'border-white/20 focus:border-white/40 focus:ring-white/10',
|
|
@@ -121,7 +121,7 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
121
121
|
<button
|
|
122
122
|
type="button"
|
|
123
123
|
onClick={onBrowse}
|
|
124
|
-
class="px-3 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-cms-sm text-sm text-white transition-colors cursor-pointer"
|
|
124
|
+
class="shrink-0 px-3 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-cms-sm text-sm text-white transition-colors cursor-pointer"
|
|
125
125
|
data-cms-ui
|
|
126
126
|
>
|
|
127
127
|
Browse
|
|
@@ -3,7 +3,6 @@ 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 { startDeploymentPolling } from '../editor'
|
|
7
6
|
import { createMarkdownPage } from '../markdown-api'
|
|
8
7
|
import {
|
|
9
8
|
config,
|
|
@@ -50,6 +49,7 @@ export function MarkdownEditorOverlay() {
|
|
|
50
49
|
// Preview mode state
|
|
51
50
|
const [isPreview, setIsPreview] = useState(false)
|
|
52
51
|
const originalHTMLRef = useRef<string | null>(null)
|
|
52
|
+
const previewTargetRef = useRef<HTMLElement | null>(null)
|
|
53
53
|
const editorInstanceRef = useRef<Editor | null>(null)
|
|
54
54
|
|
|
55
55
|
useEffect(() => {
|
|
@@ -58,23 +58,34 @@ export function MarkdownEditorOverlay() {
|
|
|
58
58
|
}
|
|
59
59
|
}, [isCreateMode, isDataCollection])
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
// Auto-generate slug from title in create mode (unless user manually edited the slug)
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isCreateMode || slugManuallyEdited) return
|
|
64
|
+
const title = (page?.frontmatter.title as string) || (page?.frontmatter.name as string) || ''
|
|
65
|
+
if (!title) return
|
|
66
|
+
markdownEditorState.value = {
|
|
67
|
+
...markdownEditorState.value,
|
|
68
|
+
currentPage: markdownEditorState.value.currentPage
|
|
69
|
+
? { ...markdownEditorState.value.currentPage, slug: slugify(title), isDirty: true }
|
|
70
|
+
: null,
|
|
71
|
+
}
|
|
72
|
+
}, [isCreateMode, slugManuallyEdited, page?.frontmatter.title, page?.frontmatter.name])
|
|
73
|
+
|
|
74
|
+
/** Find the [data-cms-markdown] wrapper element on the actual page (not CMS UI). */
|
|
75
|
+
const findMarkdownWrapper = useCallback((): HTMLElement | null => {
|
|
76
|
+
const SKIP_TAGS = new Set(['BODY', 'HTML', 'BUTTON', 'SPAN', 'A'])
|
|
77
|
+
const candidates = document.querySelectorAll('[data-cms-markdown]:not([data-cms-ui])')
|
|
78
|
+
for (const c of candidates) {
|
|
79
|
+
if (!SKIP_TAGS.has(c.tagName)) return c as HTMLElement
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
82
|
+
}, [])
|
|
69
83
|
|
|
70
84
|
const restoreOriginalHTML = useCallback(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const el = document.querySelector(`[data-cms-id="${activeId}"]`)
|
|
74
|
-
if (el) {
|
|
75
|
-
el.innerHTML = originalHTMLRef.current
|
|
76
|
-
}
|
|
85
|
+
if (originalHTMLRef.current !== null && previewTargetRef.current) {
|
|
86
|
+
previewTargetRef.current.innerHTML = originalHTMLRef.current
|
|
77
87
|
originalHTMLRef.current = null
|
|
88
|
+
previewTargetRef.current = null
|
|
78
89
|
}
|
|
79
90
|
}, [])
|
|
80
91
|
|
|
@@ -95,11 +106,8 @@ export function MarkdownEditorOverlay() {
|
|
|
95
106
|
if (result.success) {
|
|
96
107
|
// Keep the preview HTML in place so user sees changes immediately
|
|
97
108
|
// If not in preview mode, inject editor HTML into the page element
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const el = document.querySelector(
|
|
101
|
-
`[data-cms-id="${activeId}"]`,
|
|
102
|
-
)
|
|
109
|
+
if (editorInstanceRef.current && !isPreview) {
|
|
110
|
+
const el = findMarkdownWrapper()
|
|
103
111
|
if (el) {
|
|
104
112
|
try {
|
|
105
113
|
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
@@ -115,14 +123,10 @@ export function MarkdownEditorOverlay() {
|
|
|
115
123
|
isMarkdownPreview.value = false
|
|
116
124
|
setIsSaving(false)
|
|
117
125
|
|
|
118
|
-
showToast('Content saved
|
|
126
|
+
showToast('Content saved', 'success')
|
|
119
127
|
// Clear pending entry navigation so editor doesn't auto-open after save
|
|
120
128
|
sessionStorage.removeItem(STORAGE_KEYS.PENDING_ENTRY_NAVIGATION)
|
|
121
|
-
// Close the editor immediately — the toolbar will show deploying state
|
|
122
129
|
resetMarkdownEditorState()
|
|
123
|
-
startDeploymentPolling(config.value, {
|
|
124
|
-
onComplete: handleDeploymentComplete,
|
|
125
|
-
})
|
|
126
130
|
} else {
|
|
127
131
|
showToast(result.error || 'Failed to save markdown', 'error')
|
|
128
132
|
setIsSaving(false)
|
|
@@ -133,7 +137,7 @@ export function MarkdownEditorOverlay() {
|
|
|
133
137
|
setIsSaving(false)
|
|
134
138
|
}
|
|
135
139
|
},
|
|
136
|
-
[isSaving, isPreview,
|
|
140
|
+
[isSaving, isPreview, findMarkdownWrapper],
|
|
137
141
|
)
|
|
138
142
|
|
|
139
143
|
const handleCreate = useCallback(async () => {
|
|
@@ -189,16 +193,11 @@ export function MarkdownEditorOverlay() {
|
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
showToast('Page created
|
|
196
|
+
showToast('Page created', 'success')
|
|
193
197
|
resetMarkdownEditorState()
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (status === 'completed' && redirectUrl) {
|
|
198
|
-
startRedirectCountdown(redirectUrl, title.trim())
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
})
|
|
198
|
+
if (redirectUrl) {
|
|
199
|
+
startRedirectCountdown(redirectUrl, title.trim())
|
|
200
|
+
}
|
|
202
201
|
} else {
|
|
203
202
|
showToast(result.error || 'Failed to create page', 'error')
|
|
204
203
|
}
|
|
@@ -208,26 +207,28 @@ export function MarkdownEditorOverlay() {
|
|
|
208
207
|
} finally {
|
|
209
208
|
setIsSaving(false)
|
|
210
209
|
}
|
|
211
|
-
}, [isSaving
|
|
210
|
+
}, [isSaving])
|
|
212
211
|
|
|
213
212
|
const handlePreview = useCallback(() => {
|
|
214
213
|
const activeId = markdownEditorState.value.activeElementId
|
|
215
214
|
if (!editorInstanceRef.current || !activeId) return
|
|
216
215
|
|
|
217
216
|
if (!isPreview) {
|
|
218
|
-
// Enter preview
|
|
219
|
-
const el =
|
|
217
|
+
// Enter preview — inject editor HTML into the markdown wrapper element.
|
|
218
|
+
const el = findMarkdownWrapper()
|
|
220
219
|
if (!el) {
|
|
221
220
|
showToast('Could not find page element to preview', 'error')
|
|
222
221
|
return
|
|
223
222
|
}
|
|
224
223
|
originalHTMLRef.current = el.innerHTML
|
|
224
|
+
previewTargetRef.current = el
|
|
225
225
|
try {
|
|
226
226
|
const view = editorInstanceRef.current.ctx.get(editorViewCtx)
|
|
227
227
|
el.innerHTML = view.dom.innerHTML
|
|
228
228
|
} catch (error) {
|
|
229
229
|
console.error('Failed to get editor HTML for preview:', error)
|
|
230
230
|
originalHTMLRef.current = null
|
|
231
|
+
previewTargetRef.current = null
|
|
231
232
|
showToast('Failed to generate preview', 'error')
|
|
232
233
|
return
|
|
233
234
|
}
|
|
@@ -239,7 +240,7 @@ export function MarkdownEditorOverlay() {
|
|
|
239
240
|
setIsPreview(false)
|
|
240
241
|
isMarkdownPreview.value = false
|
|
241
242
|
}
|
|
242
|
-
}, [isPreview, restoreOriginalHTML])
|
|
243
|
+
}, [isPreview, restoreOriginalHTML, findMarkdownWrapper])
|
|
243
244
|
|
|
244
245
|
const handleCancel = useCallback(() => {
|
|
245
246
|
restoreOriginalHTML()
|
|
@@ -326,8 +327,8 @@ export function MarkdownEditorOverlay() {
|
|
|
326
327
|
>
|
|
327
328
|
{/* Header */}
|
|
328
329
|
<div class="flex items-center justify-between px-5 py-4 border-b border-white/10">
|
|
329
|
-
<div class="flex items-center gap-3">
|
|
330
|
-
<div class="flex items-center text-white">
|
|
330
|
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
331
|
+
<div class="flex items-center text-white shrink-0">
|
|
331
332
|
<svg
|
|
332
333
|
width="20"
|
|
333
334
|
height="20"
|
|
@@ -345,36 +346,11 @@ export function MarkdownEditorOverlay() {
|
|
|
345
346
|
<line x1="10" y1="9" x2="8" y2="9" />
|
|
346
347
|
</svg>
|
|
347
348
|
</div>
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
value={(page.frontmatter.title as string) || (page.frontmatter.name as string) || ''}
|
|
352
|
-
placeholder={isDataCollection ? 'Entry name...' : 'Page title...'}
|
|
353
|
-
onInput={(e) => {
|
|
354
|
-
const title = (e.target as HTMLInputElement).value
|
|
355
|
-
// Data collections may use 'name' instead of 'title'
|
|
356
|
-
const titleField = isDataCollection && !('title' in page.frontmatter) && 'name' in page.frontmatter ? 'name' : 'title'
|
|
357
|
-
updateMarkdownFrontmatter({ [titleField]: title })
|
|
358
|
-
// Auto-generate slug in create mode if not manually edited
|
|
359
|
-
if (isCreateMode && !slugManuallyEdited) {
|
|
360
|
-
markdownEditorState.value = {
|
|
361
|
-
...markdownEditorState.value,
|
|
362
|
-
currentPage: markdownEditorState.value.currentPage
|
|
363
|
-
? {
|
|
364
|
-
...markdownEditorState.value.currentPage,
|
|
365
|
-
slug: slugify(title),
|
|
366
|
-
isDirty: true,
|
|
367
|
-
}
|
|
368
|
-
: null,
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}}
|
|
372
|
-
class="text-base font-semibold text-white m-0 bg-transparent border-none outline-none placeholder-white/40 w-64"
|
|
373
|
-
data-cms-ui
|
|
374
|
-
/>
|
|
375
|
-
</div>
|
|
349
|
+
<span class="text-base font-semibold text-white truncate">
|
|
350
|
+
{(page.frontmatter.title as string) || (page.frontmatter.name as string) || (isDataCollection ? 'Entry name' : 'Page title')}
|
|
351
|
+
</span>
|
|
376
352
|
</div>
|
|
377
|
-
<div class="flex items-center gap-2">
|
|
353
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
378
354
|
{!isDataCollection && (
|
|
379
355
|
<button
|
|
380
356
|
type="button"
|