@nuasite/cms 0.19.1 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/editor.js +12615 -12689
  2. package/package.json +3 -3
  3. package/src/build-processor.ts +4 -4
  4. package/src/dev-middleware.ts +185 -189
  5. package/src/editor/api.ts +0 -251
  6. package/src/editor/components/fields.tsx +6 -6
  7. package/src/editor/components/markdown-editor-overlay.tsx +46 -70
  8. package/src/editor/components/markdown-inline-editor.tsx +34 -165
  9. package/src/editor/components/mdx-block-view.tsx +351 -47
  10. package/src/editor/components/mdx-component-picker.tsx +35 -11
  11. package/src/editor/components/media-library.tsx +1 -15
  12. package/src/editor/components/modal-shell.tsx +1 -1
  13. package/src/editor/components/toolbar.tsx +0 -75
  14. package/src/editor/constants.ts +0 -4
  15. package/src/editor/editor.ts +2 -192
  16. package/src/editor/hooks/index.ts +0 -3
  17. package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
  18. package/src/editor/hooks/useTooltipState.ts +1 -2
  19. package/src/editor/index.tsx +2 -18
  20. package/src/editor/milkdown-mdx-plugin.tsx +116 -19
  21. package/src/editor/milkdown-utils.ts +174 -0
  22. package/src/editor/post-message.ts +0 -6
  23. package/src/editor/signals.ts +0 -183
  24. package/src/editor/styles.css +0 -108
  25. package/src/editor/types.ts +0 -76
  26. package/src/html-processor.ts +9 -7
  27. package/src/source-finder/cache.ts +47 -0
  28. package/src/source-finder/collection-finder.ts +181 -0
  29. package/src/source-finder/index.ts +5 -2
  30. package/src/source-finder/search-index.ts +79 -0
  31. package/src/source-finder/snippet-utils.ts +36 -61
  32. package/src/types.ts +0 -4
  33. package/src/utils.ts +10 -0
  34. package/src/vite-plugin.ts +24 -4
  35. package/src/editor/ai.ts +0 -185
  36. package/src/editor/components/ai-chat.tsx +0 -631
  37. package/src/editor/components/ai-tooltip.tsx +0 -180
  38. package/src/editor/components/mdx-props-editor.tsx +0 -94
  39. package/src/editor/hooks/useAIHandlers.ts +0 -345
@@ -1,4 +1,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
- onEdit: (cursorPos: { x: number; y: number }) => void
42
+ slotContent?: string
26
43
  onRemove: () => void
44
+ onSlotContentChange?: (content: string) => void
45
+ onPropsChange?: (props: Record<string, string>) => void
27
46
  }
28
47
 
29
- export function MdxBlockCard({ componentName, props, hasExpressions, onEdit, onRemove }: MdxBlockCardProps) {
30
- const propEntries = Object.entries(props).filter(([_, v]) => v !== '')
31
- const displayProps = propEntries.map(([name, value]) => {
32
- if (value.startsWith(MDX_EXPR_PREFIX)) {
33
- return { name, value: value.slice(MDX_EXPR_PREFIX.length), isExpression: true }
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
- return { name, value, isExpression: false }
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
- <div class="flex items-center gap-1">
50
- <button
51
- type="button"
52
- data-mdx-action="edit"
53
- onClick={(e: MouseEvent) => {
54
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
55
- onEdit({ x: rect.left, y: rect.bottom + 4 })
56
- }}
57
- class="p-1.5 rounded-cms-sm text-white/50 hover:text-white hover:bg-white/10 transition-colors"
58
- title="Edit props"
59
- >
60
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
61
- <path
62
- stroke-linecap="round"
63
- stroke-linejoin="round"
64
- d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
65
- />
66
- </svg>
67
- </button>
68
- <button
69
- type="button"
70
- data-mdx-action="remove"
71
- onClick={onRemove}
72
- class="p-1.5 rounded-cms-sm text-white/50 hover:text-red-400 hover:bg-red-500/10 transition-colors"
73
- title="Remove block"
74
- >
75
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
76
- <path
77
- stroke-linecap="round"
78
- stroke-linejoin="round"
79
- 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"
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
- </svg>
82
- </button>
371
+ </div>
372
+ ))}
83
373
  </div>
84
- </div>
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
- {displayProps.length > 0 && (
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
- {displayProps.slice(0, 6).map(({ name, value, isExpression }) => (
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
- {isExpression
93
- ? <span class="text-amber-300/60">{`{${value.length > 20 ? value.slice(0, 20) + '...' : value}}`}</span>
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
- {displayProps.length > 6 && <span class="text-[11px] text-white/30">+{displayProps.length - 6} more</span>}
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>) => void
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 || selectedDef.props.length === 0) {
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 selectedDef.props.map((prop) => (
74
- <PropEditor
75
- key={prop.name}
76
- prop={prop}
77
- value={propValues[prop.name] || ''}
78
- onChange={(value) => setPropValues((prev) => ({ ...prev, [prop.name]: value }))}
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/*,video/mp4,video/webm,application/pdf"
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
  )