@nuasite/cms 0.19.0 → 0.20.1
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 +11055 -10934
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +171 -185
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +41 -46
- 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/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/signals.ts +1 -18
- package/src/editor/types.ts +0 -10
- package/src/html-processor.ts +9 -7
- package/src/index.ts +3 -13
- 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/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/components/mdx-props-editor.tsx +0 -94
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
import { 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 type { ComponentChildren } from 'preact'
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
1
17
|
import { MDX_EXPR_PREFIX } from '../milkdown-mdx-plugin'
|
|
18
|
+
import { type ActiveFormats, defaultActiveFormats, isInListType, setupFormatTracking, toggleHeading } from '../milkdown-utils'
|
|
2
19
|
|
|
3
20
|
const MDX_COMPONENT_ICON_PATH =
|
|
4
21
|
'M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5'
|
|
@@ -22,79 +39,366 @@ export interface MdxBlockCardProps {
|
|
|
22
39
|
componentName: string
|
|
23
40
|
props: Record<string, string>
|
|
24
41
|
hasExpressions: boolean
|
|
25
|
-
|
|
42
|
+
slotContent?: string
|
|
26
43
|
onRemove: () => void
|
|
44
|
+
onSlotContentChange?: (content: string) => void
|
|
45
|
+
onPropsChange?: (props: Record<string, string>) => void
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Inline editors — use refs + DOM to avoid Preact render cycle issues
|
|
50
|
+
// with imperative render() from ProseMirror node views
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
// ---- Mini Milkdown editor for slot content ----
|
|
54
|
+
|
|
55
|
+
function MiniToolbarButton(
|
|
56
|
+
{ onClick, title, active, children: content }: { onClick: () => void; title: string; active?: boolean; children: ComponentChildren },
|
|
57
|
+
) {
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
62
|
+
onClick={onClick}
|
|
63
|
+
class={`p-1 rounded transition-colors ${active ? 'bg-cms-primary text-cms-primary-text' : 'text-white/40 hover:text-white hover:bg-white/10'}`}
|
|
64
|
+
title={title}
|
|
65
|
+
data-mdx-action="format"
|
|
66
|
+
>
|
|
67
|
+
{content}
|
|
68
|
+
</button>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function MiniMilkdownEditor({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
73
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
74
|
+
const editorRef = useRef<Editor | null>(null)
|
|
75
|
+
const latestMarkdown = useRef(value)
|
|
76
|
+
const isFocused = useRef(false)
|
|
77
|
+
const [formats, setFormats] = useState<ActiveFormats>(defaultActiveFormats)
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const el = containerRef.current
|
|
81
|
+
if (!el) return
|
|
82
|
+
|
|
83
|
+
let destroyed = false
|
|
84
|
+
let cleanupTracking: (() => void) | undefined
|
|
85
|
+
|
|
86
|
+
const init = async () => {
|
|
87
|
+
const editor = await Editor.make()
|
|
88
|
+
.config((ctx) => {
|
|
89
|
+
ctx.set(rootCtx, el)
|
|
90
|
+
ctx.set(defaultValueCtx, value)
|
|
91
|
+
ctx.get(listenerCtx).markdownUpdated((_, md) => {
|
|
92
|
+
latestMarkdown.current = md
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
.use(commonmark)
|
|
96
|
+
.use(gfm)
|
|
97
|
+
.use(listener)
|
|
98
|
+
.create()
|
|
99
|
+
|
|
100
|
+
if (destroyed) {
|
|
101
|
+
editor.destroy()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
editorRef.current = editor
|
|
105
|
+
cleanupTracking = setupFormatTracking(editor, setFormats)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
init()
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
destroyed = true
|
|
112
|
+
cleanupTracking?.()
|
|
113
|
+
editorRef.current?.destroy()
|
|
114
|
+
editorRef.current = null
|
|
115
|
+
}
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
// Sync external value changes when not focused
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isFocused.current && editorRef.current && value !== latestMarkdown.current) {
|
|
121
|
+
try {
|
|
122
|
+
editorRef.current.action(replaceAll(value))
|
|
123
|
+
latestMarkdown.current = value
|
|
124
|
+
} catch { /* editor not ready */ }
|
|
34
125
|
}
|
|
35
|
-
|
|
36
|
-
|
|
126
|
+
}, [value])
|
|
127
|
+
|
|
128
|
+
const runCmd = useCallback((cmd: any, payload?: any) => {
|
|
129
|
+
if (!editorRef.current) return
|
|
130
|
+
try {
|
|
131
|
+
editorRef.current.action(callCommand(cmd, payload))
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
}, [])
|
|
134
|
+
|
|
135
|
+
const checkInList = useCallback((listType: string): boolean => {
|
|
136
|
+
if (!editorRef.current) return false
|
|
137
|
+
try {
|
|
138
|
+
const view = editorRef.current.ctx.get(editorViewCtx)
|
|
139
|
+
return isInListType(view, listType)
|
|
140
|
+
} catch { /* ignore */ }
|
|
141
|
+
return false
|
|
142
|
+
}, [])
|
|
143
|
+
|
|
144
|
+
const handleLink = useCallback(() => {
|
|
145
|
+
if (!editorRef.current) return
|
|
146
|
+
const url = prompt('Enter URL:', 'https://')
|
|
147
|
+
if (!url) return
|
|
148
|
+
try {
|
|
149
|
+
editorRef.current.action(callCommand(toggleLinkCommand.key, { href: url }))
|
|
150
|
+
} catch {
|
|
151
|
+
const linkText = window.getSelection()?.toString() || 'Link'
|
|
152
|
+
editorRef.current.action(insert(`[${linkText}](${url})`))
|
|
153
|
+
}
|
|
154
|
+
}, [])
|
|
155
|
+
|
|
156
|
+
const handleHeadingToggle = useCallback((level: number) => {
|
|
157
|
+
if (!editorRef.current) return
|
|
158
|
+
try {
|
|
159
|
+
const view = editorRef.current.ctx.get(editorViewCtx)
|
|
160
|
+
toggleHeading(view, level)
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
const handleList = useCallback((type: 'bullet' | 'ordered') => {
|
|
165
|
+
const listType = type === 'bullet' ? 'bullet_list' : 'ordered_list'
|
|
166
|
+
if (checkInList(listType)) {
|
|
167
|
+
runCmd(liftListItemCommand.key)
|
|
168
|
+
} else {
|
|
169
|
+
runCmd(type === 'bullet' ? wrapInBulletListCommand.key : wrapInOrderedListCommand.key)
|
|
170
|
+
}
|
|
171
|
+
}, [runCmd, checkInList])
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
{/* Toolbar */}
|
|
176
|
+
<div class="flex items-center gap-0.5 mb-1.5 flex-wrap">
|
|
177
|
+
{/* Text formatting */}
|
|
178
|
+
<MiniToolbarButton onClick={() => runCmd(toggleStrongCommand.key)} title="Bold" active={formats.bold}>
|
|
179
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
180
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" />
|
|
181
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" />
|
|
182
|
+
</svg>
|
|
183
|
+
</MiniToolbarButton>
|
|
184
|
+
<MiniToolbarButton onClick={() => runCmd(toggleEmphasisCommand.key)} title="Italic" active={formats.italic}>
|
|
185
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
186
|
+
<line x1="19" y1="4" x2="10" y2="4" />
|
|
187
|
+
<line x1="14" y1="20" x2="5" y2="20" />
|
|
188
|
+
<line x1="15" y1="4" x2="9" y2="20" />
|
|
189
|
+
</svg>
|
|
190
|
+
</MiniToolbarButton>
|
|
191
|
+
<MiniToolbarButton onClick={() => runCmd(toggleStrikethroughCommand.key)} title="Strikethrough" active={formats.strikethrough}>
|
|
192
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
193
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12h12M6 12a4 4 0 0 1 4-4h4a4 4 0 0 1 0 8H10a4 4 0 0 1-4-4z" />
|
|
194
|
+
</svg>
|
|
195
|
+
</MiniToolbarButton>
|
|
196
|
+
|
|
197
|
+
<div class="w-px h-4 bg-white/15 mx-0.5" />
|
|
198
|
+
|
|
199
|
+
{/* Headings */}
|
|
200
|
+
<MiniToolbarButton onClick={() => handleHeadingToggle(2)} title="Heading 2" active={formats.heading === 2}>
|
|
201
|
+
<span class="text-[10px] font-bold leading-none">H2</span>
|
|
202
|
+
</MiniToolbarButton>
|
|
203
|
+
<MiniToolbarButton onClick={() => handleHeadingToggle(3)} title="Heading 3" active={formats.heading === 3}>
|
|
204
|
+
<span class="text-[10px] font-bold leading-none">H3</span>
|
|
205
|
+
</MiniToolbarButton>
|
|
206
|
+
<MiniToolbarButton onClick={() => handleHeadingToggle(4)} title="Heading 4" active={formats.heading === 4}>
|
|
207
|
+
<span class="text-[10px] font-bold leading-none">H4</span>
|
|
208
|
+
</MiniToolbarButton>
|
|
209
|
+
|
|
210
|
+
<div class="w-px h-4 bg-white/15 mx-0.5" />
|
|
211
|
+
|
|
212
|
+
{/* Lists & quote */}
|
|
213
|
+
<MiniToolbarButton onClick={() => handleList('bullet')} title="Bullet list" active={formats.bulletList}>
|
|
214
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
215
|
+
<line x1="9" y1="6" x2="20" y2="6" />
|
|
216
|
+
<line x1="9" y1="12" x2="20" y2="12" />
|
|
217
|
+
<line x1="9" y1="18" x2="20" y2="18" />
|
|
218
|
+
<circle cx="4" cy="6" r="1.5" fill="currentColor" />
|
|
219
|
+
<circle cx="4" cy="12" r="1.5" fill="currentColor" />
|
|
220
|
+
<circle cx="4" cy="18" r="1.5" fill="currentColor" />
|
|
221
|
+
</svg>
|
|
222
|
+
</MiniToolbarButton>
|
|
223
|
+
<MiniToolbarButton onClick={() => handleList('ordered')} title="Numbered list" active={formats.orderedList}>
|
|
224
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
225
|
+
<line x1="10" y1="6" x2="21" y2="6" />
|
|
226
|
+
<line x1="10" y1="12" x2="21" y2="12" />
|
|
227
|
+
<line x1="10" y1="18" x2="21" y2="18" />
|
|
228
|
+
<text x="3" y="8" font-size="7" fill="currentColor" stroke="none">1</text>
|
|
229
|
+
<text x="3" y="14" font-size="7" fill="currentColor" stroke="none">2</text>
|
|
230
|
+
<text x="3" y="20" font-size="7" fill="currentColor" stroke="none">3</text>
|
|
231
|
+
</svg>
|
|
232
|
+
</MiniToolbarButton>
|
|
233
|
+
<MiniToolbarButton onClick={() => runCmd(wrapInBlockquoteCommand.key)} title="Quote" active={formats.blockquote}>
|
|
234
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
235
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 6v12M7 6v12M11 6h10M11 12h7M11 18h4" />
|
|
236
|
+
</svg>
|
|
237
|
+
</MiniToolbarButton>
|
|
238
|
+
|
|
239
|
+
<div class="w-px h-4 bg-white/15 mx-0.5" />
|
|
240
|
+
|
|
241
|
+
{/* Link */}
|
|
242
|
+
<MiniToolbarButton onClick={handleLink} title="Link" active={formats.link}>
|
|
243
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
244
|
+
<path
|
|
245
|
+
stroke-linecap="round"
|
|
246
|
+
stroke-linejoin="round"
|
|
247
|
+
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"
|
|
248
|
+
/>
|
|
249
|
+
</svg>
|
|
250
|
+
</MiniToolbarButton>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Editor */}
|
|
254
|
+
<div
|
|
255
|
+
ref={(el) => {
|
|
256
|
+
;(containerRef as any).current = el
|
|
257
|
+
}}
|
|
258
|
+
onFocusCapture={() => {
|
|
259
|
+
isFocused.current = true
|
|
260
|
+
}}
|
|
261
|
+
onBlurCapture={() => {
|
|
262
|
+
isFocused.current = false
|
|
263
|
+
if (latestMarkdown.current !== value) {
|
|
264
|
+
onChange(latestMarkdown.current)
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
class="mini-milkdown milkdown-dark prose prose-invert prose-sm max-w-none text-[13px] leading-relaxed [&_.milkdown]:outline-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:whitespace-pre-wrap [&_.ProseMirror]:break-words [&_.ProseMirror_p]:my-1 [&_.ProseMirror_p:first-child]:mt-0 [&_.ProseMirror_p:last-child]:mb-0"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function InlineInput({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) {
|
|
274
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
275
|
+
const isFocused = useRef(false)
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (ref.current && !isFocused.current) {
|
|
279
|
+
ref.current.value = value
|
|
280
|
+
}
|
|
281
|
+
}, [value])
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<input
|
|
285
|
+
ref={(el) => {
|
|
286
|
+
;(ref as any).current = el
|
|
287
|
+
if (el) el.value = value
|
|
288
|
+
}}
|
|
289
|
+
type="text"
|
|
290
|
+
onFocus={() => {
|
|
291
|
+
isFocused.current = true
|
|
292
|
+
}}
|
|
293
|
+
onBlur={(e) => {
|
|
294
|
+
isFocused.current = false
|
|
295
|
+
const el = e.target as HTMLInputElement
|
|
296
|
+
if (el.value !== value) onChange(el.value)
|
|
297
|
+
}}
|
|
298
|
+
placeholder={placeholder}
|
|
299
|
+
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/25 transition-colors"
|
|
300
|
+
/>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// Block Card
|
|
306
|
+
// ============================================================================
|
|
307
|
+
|
|
308
|
+
export function MdxBlockCard({ componentName, props, hasExpressions, slotContent, onRemove, onSlotContentChange, onPropsChange }: MdxBlockCardProps) {
|
|
309
|
+
const propEntries = Object.entries(props).filter(([_, v]) => v !== '' || onPropsChange)
|
|
310
|
+
const editableProps = propEntries.filter(([_, v]) => !v.startsWith(MDX_EXPR_PREFIX))
|
|
311
|
+
const expressionProps = propEntries.filter(([_, v]) => v.startsWith(MDX_EXPR_PREFIX))
|
|
312
|
+
|
|
313
|
+
const hasSlotContent = onSlotContentChange != null
|
|
314
|
+
|
|
315
|
+
const handlePropChange = (name: string, newValue: string) => {
|
|
316
|
+
if (onPropsChange) {
|
|
317
|
+
onPropsChange({ ...props, [name]: newValue })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
37
320
|
|
|
38
321
|
return (
|
|
39
322
|
<div
|
|
40
323
|
class="my-3 mx-0 bg-white/5 border border-white/15 rounded-cms-md overflow-hidden select-none"
|
|
41
324
|
data-cms-ui
|
|
42
325
|
>
|
|
326
|
+
{/* Header */}
|
|
43
327
|
<div class="flex items-center justify-between px-4 py-2.5 bg-white/5 border-b border-white/10">
|
|
44
328
|
<div class="flex items-center gap-2">
|
|
45
329
|
<MdxComponentIcon />
|
|
46
330
|
<span class="text-[13px] font-semibold text-white">{componentName}</span>
|
|
47
331
|
{hasExpressions && <span class="text-[10px] px-1.5 py-0.5 bg-amber-500/20 text-amber-300 rounded font-mono">expr</span>}
|
|
48
332
|
</div>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
333
|
+
<button
|
|
334
|
+
type="button"
|
|
335
|
+
data-mdx-action="remove"
|
|
336
|
+
onClick={onRemove}
|
|
337
|
+
class="p-1.5 rounded-cms-sm text-white/50 hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
|
338
|
+
title="Remove block"
|
|
339
|
+
>
|
|
340
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
341
|
+
<path
|
|
342
|
+
stroke-linecap="round"
|
|
343
|
+
stroke-linejoin="round"
|
|
344
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
345
|
+
/>
|
|
346
|
+
</svg>
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Slot content editor */}
|
|
351
|
+
{hasSlotContent && (
|
|
352
|
+
<div class="px-4 py-2.5 border-b border-white/10" data-mdx-action="children">
|
|
353
|
+
<MiniMilkdownEditor
|
|
354
|
+
value={slotContent || ''}
|
|
355
|
+
onChange={onSlotContentChange}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{/* Inline prop editors */}
|
|
361
|
+
{onPropsChange && editableProps.length > 0 && (
|
|
362
|
+
<div class="px-4 py-3 space-y-2" data-mdx-action="props">
|
|
363
|
+
{editableProps.map(([name, value]) => (
|
|
364
|
+
<div key={name} class="flex items-center gap-2">
|
|
365
|
+
<label class="text-[11px] text-white/40 font-medium w-20 shrink-0 text-right">{name}</label>
|
|
366
|
+
<InlineInput
|
|
367
|
+
value={value}
|
|
368
|
+
onChange={(v) => handlePropChange(name, v)}
|
|
369
|
+
placeholder={`Enter ${name}...`}
|
|
80
370
|
/>
|
|
81
|
-
</
|
|
82
|
-
|
|
371
|
+
</div>
|
|
372
|
+
))}
|
|
83
373
|
</div>
|
|
84
|
-
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* Read-only expression props */}
|
|
377
|
+
{expressionProps.length > 0 && (
|
|
378
|
+
<div class="px-4 py-2 flex flex-wrap gap-x-3 gap-y-1 border-t border-white/10">
|
|
379
|
+
{expressionProps.map(([name, value]) => (
|
|
380
|
+
<span key={name} class="text-[11px] text-white/40 font-mono">
|
|
381
|
+
<span class="text-white/60">{name}</span>
|
|
382
|
+
<span class="text-white/30">=</span>
|
|
383
|
+
<span class="text-amber-300/60">{`{${value.slice(MDX_EXPR_PREFIX.length)}}`}</span>
|
|
384
|
+
</span>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
85
388
|
|
|
86
|
-
{
|
|
389
|
+
{/* Read-only props fallback when no onPropsChange */}
|
|
390
|
+
{!onPropsChange && propEntries.length > 0 && (
|
|
87
391
|
<div class="px-4 py-2 flex flex-wrap gap-x-3 gap-y-1">
|
|
88
|
-
{
|
|
392
|
+
{propEntries.slice(0, 6).map(([name, value]) => (
|
|
89
393
|
<span key={name} class="text-[11px] text-white/40 font-mono">
|
|
90
394
|
<span class="text-white/60">{name}</span>
|
|
91
395
|
<span class="text-white/30">=</span>
|
|
92
|
-
{
|
|
93
|
-
? <span class="text-amber-300/60">{`{${value.
|
|
396
|
+
{value.startsWith(MDX_EXPR_PREFIX)
|
|
397
|
+
? <span class="text-amber-300/60">{`{${value.slice(MDX_EXPR_PREFIX.length)}}`}</span>
|
|
94
398
|
: <span class="text-cms-primary/60">"{value.length > 25 ? value.slice(0, 25) + '...' : value}"</span>}
|
|
95
399
|
</span>
|
|
96
400
|
))}
|
|
97
|
-
{
|
|
401
|
+
{propEntries.length > 6 && <span class="text-[11px] text-white/30">+{propEntries.length - 6} more</span>}
|
|
98
402
|
</div>
|
|
99
403
|
)}
|
|
100
404
|
</div>
|
|
@@ -6,13 +6,14 @@ import { CancelButton, ModalBackdrop, ModalHeader } from './modal-shell'
|
|
|
6
6
|
import { PropEditor } from './prop-editor'
|
|
7
7
|
|
|
8
8
|
export interface MdxComponentPickerProps {
|
|
9
|
-
onInsert: (componentName: string, props: Record<string, string
|
|
9
|
+
onInsert: (componentName: string, props: Record<string, string>, children?: string) => void
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
13
13
|
const isOpen = mdxComponentPickerOpen.value
|
|
14
14
|
const [selectedComponent, setSelectedComponent] = useState<string | null>(null)
|
|
15
15
|
const [propValues, setPropValues] = useState<Record<string, string>>({})
|
|
16
|
+
const [childrenValue, setChildrenValue] = useState('')
|
|
16
17
|
const [searchQuery, setSearchQuery] = useState('')
|
|
17
18
|
|
|
18
19
|
if (!isOpen) return null
|
|
@@ -22,6 +23,7 @@ export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
|
22
23
|
const resetSelection = () => {
|
|
23
24
|
setSelectedComponent(null)
|
|
24
25
|
setPropValues({})
|
|
26
|
+
setChildrenValue('')
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const close = () => {
|
|
@@ -35,11 +37,12 @@ export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
|
35
37
|
if (!def) return
|
|
36
38
|
setSelectedComponent(name)
|
|
37
39
|
setPropValues(getDefaultProps(def))
|
|
40
|
+
setChildrenValue('')
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const handleConfirmInsert = () => {
|
|
41
44
|
if (selectedComponent) {
|
|
42
|
-
onInsert(selectedComponent, propValues)
|
|
45
|
+
onInsert(selectedComponent, propValues, childrenValue || undefined)
|
|
43
46
|
close()
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -63,21 +66,42 @@ export function MdxComponentPicker({ onInsert }: MdxComponentPickerProps) {
|
|
|
63
66
|
</div>
|
|
64
67
|
{(() => {
|
|
65
68
|
const selectedDef = componentDefinitions[selectedComponent]
|
|
66
|
-
if (!selectedDef
|
|
69
|
+
if (!selectedDef) return null
|
|
70
|
+
const hasDefaultSlot = selectedDef.slots?.includes('default') ?? false
|
|
71
|
+
const hasProps = selectedDef.props.length > 0
|
|
72
|
+
if (!hasProps && !hasDefaultSlot) {
|
|
67
73
|
return (
|
|
68
74
|
<div class="text-white/50 text-[13px]">
|
|
69
75
|
This component has no configurable props.
|
|
70
76
|
</div>
|
|
71
77
|
)
|
|
72
78
|
}
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
{hasDefaultSlot && (
|
|
82
|
+
<div class="mb-4">
|
|
83
|
+
<label class="block text-[13px] font-medium text-white mb-1.5">
|
|
84
|
+
Content
|
|
85
|
+
</label>
|
|
86
|
+
<textarea
|
|
87
|
+
value={childrenValue}
|
|
88
|
+
onInput={(e) => setChildrenValue((e.target as HTMLTextAreaElement).value)}
|
|
89
|
+
placeholder="Enter content..."
|
|
90
|
+
rows={3}
|
|
91
|
+
class="w-full px-4 py-2.5 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
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{selectedDef.props.map((prop) => (
|
|
96
|
+
<PropEditor
|
|
97
|
+
key={prop.name}
|
|
98
|
+
prop={prop}
|
|
99
|
+
value={propValues[prop.name] || ''}
|
|
100
|
+
onChange={(value) => setPropValues((prev) => ({ ...prev, [prop.name]: value }))}
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
81
105
|
})()}
|
|
82
106
|
</div>
|
|
83
107
|
<div class="px-5 py-4 border-t border-white/10 flex gap-2 justify-end">
|
|
@@ -10,7 +10,6 @@ const TYPE_FILTERS: Array<{ value: MediaTypeFilter; label: string }> = [
|
|
|
10
10
|
{ value: 'all', label: 'All' },
|
|
11
11
|
{ value: 'photo', label: 'Photos' },
|
|
12
12
|
{ value: 'graphic', label: 'Graphics' },
|
|
13
|
-
{ value: 'video', label: 'Videos' },
|
|
14
13
|
{ value: 'document', label: 'Documents' },
|
|
15
14
|
]
|
|
16
15
|
|
|
@@ -18,7 +17,6 @@ function matchesTypeFilter(contentType: string, filter: MediaTypeFilter): boolea
|
|
|
18
17
|
if (filter === 'all') return true
|
|
19
18
|
if (filter === 'photo') return contentType.startsWith('image/') && !VECTOR_TYPES.has(contentType)
|
|
20
19
|
if (filter === 'graphic') return VECTOR_TYPES.has(contentType)
|
|
21
|
-
if (filter === 'video') return contentType.startsWith('video/')
|
|
22
20
|
if (filter === 'document') return contentType === 'application/pdf'
|
|
23
21
|
return true
|
|
24
22
|
}
|
|
@@ -320,7 +318,7 @@ export function MediaLibrary() {
|
|
|
320
318
|
<input
|
|
321
319
|
ref={fileInputRef}
|
|
322
320
|
type="file"
|
|
323
|
-
accept="image/*,
|
|
321
|
+
accept="image/*,application/pdf"
|
|
324
322
|
class="hidden"
|
|
325
323
|
onChange={handleFileChange}
|
|
326
324
|
data-cms-ui
|
|
@@ -533,18 +531,6 @@ export function MediaLibrary() {
|
|
|
533
531
|
}
|
|
534
532
|
|
|
535
533
|
function FileTypeIcon({ contentType }: { contentType: string }) {
|
|
536
|
-
if (contentType.startsWith('video/')) {
|
|
537
|
-
return (
|
|
538
|
-
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
539
|
-
<path
|
|
540
|
-
stroke-linecap="round"
|
|
541
|
-
stroke-linejoin="round"
|
|
542
|
-
stroke-width="1.5"
|
|
543
|
-
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
544
|
-
/>
|
|
545
|
-
</svg>
|
|
546
|
-
)
|
|
547
|
-
}
|
|
548
534
|
if (contentType === 'application/pdf') {
|
|
549
535
|
return (
|
|
550
536
|
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -44,7 +44,7 @@ export function ModalHeader({ title, onBack, onClose }: {
|
|
|
44
44
|
</svg>
|
|
45
45
|
</button>
|
|
46
46
|
)}
|
|
47
|
-
<h2 class="text-lg font-semibold text-white flex-1">{title}</h2>
|
|
47
|
+
<h2 class="text-lg font-semibold text-white flex-1 truncate">{title}</h2>
|
|
48
48
|
<CloseButton onClick={onClose} />
|
|
49
49
|
</div>
|
|
50
50
|
)
|