@nuasite/cms 0.18.0 → 0.19.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.
Files changed (62) hide show
  1. package/dist/editor.js +44697 -26834
  2. package/package.json +23 -21
  3. package/src/build-processor.ts +4 -1
  4. package/src/collection-scanner.ts +425 -48
  5. package/src/dev-middleware.ts +26 -203
  6. package/src/editor/api.ts +1 -22
  7. package/src/editor/components/ai-chat.tsx +3 -3
  8. package/src/editor/components/ai-tooltip.tsx +2 -1
  9. package/src/editor/components/block-editor.tsx +13 -108
  10. package/src/editor/components/collections-browser.tsx +168 -205
  11. package/src/editor/components/component-card.tsx +49 -0
  12. package/src/editor/components/confirm-dialog.tsx +34 -47
  13. package/src/editor/components/create-page-modal.tsx +529 -101
  14. package/src/editor/components/delete-page-dialog.tsx +100 -0
  15. package/src/editor/components/fields.tsx +175 -0
  16. package/src/editor/components/frontmatter-fields.tsx +281 -70
  17. package/src/editor/components/frontmatter-sidebar.tsx +223 -0
  18. package/src/editor/components/highlight-overlay.ts +3 -2
  19. package/src/editor/components/markdown-editor-overlay.tsx +131 -85
  20. package/src/editor/components/markdown-inline-editor.tsx +74 -5
  21. package/src/editor/components/mdx-block-view.tsx +102 -0
  22. package/src/editor/components/mdx-component-picker.tsx +123 -0
  23. package/src/editor/components/mdx-props-editor.tsx +94 -0
  24. package/src/editor/components/media-library.tsx +373 -100
  25. package/src/editor/components/modal-shell.tsx +87 -0
  26. package/src/editor/components/prop-editor.tsx +52 -0
  27. package/src/editor/components/redirect-countdown.tsx +3 -1
  28. package/src/editor/components/redirects-manager.tsx +269 -0
  29. package/src/editor/components/reference-picker.tsx +203 -0
  30. package/src/editor/components/seo-editor.tsx +285 -303
  31. package/src/editor/components/toast/toast-container.tsx +2 -1
  32. package/src/editor/components/toolbar.tsx +177 -46
  33. package/src/editor/constants.ts +26 -0
  34. package/src/editor/editor.ts +112 -0
  35. package/src/editor/fetch.ts +62 -0
  36. package/src/editor/index.tsx +19 -1
  37. package/src/editor/markdown-api.ts +105 -156
  38. package/src/editor/milkdown-mdx-plugin.tsx +269 -0
  39. package/src/editor/signals.ts +206 -13
  40. package/src/editor/types.ts +52 -1
  41. package/src/handlers/api-routes.ts +251 -0
  42. package/src/handlers/component-ops.ts +2 -18
  43. package/src/handlers/markdown-ops.ts +202 -47
  44. package/src/handlers/page-ops.ts +229 -0
  45. package/src/handlers/redirect-ops.ts +163 -0
  46. package/src/handlers/source-writer.ts +157 -1
  47. package/src/html-processor.ts +14 -2
  48. package/src/index.ts +76 -2
  49. package/src/manifest-writer.ts +19 -1
  50. package/src/media/contember.ts +2 -1
  51. package/src/media/local.ts +66 -28
  52. package/src/media/project-images.ts +81 -0
  53. package/src/media/s3.ts +32 -11
  54. package/src/media/types.ts +24 -2
  55. package/src/shared.ts +27 -0
  56. package/src/source-finder/collection-finder.ts +219 -41
  57. package/src/source-finder/index.ts +7 -1
  58. package/src/source-finder/search-index.ts +178 -36
  59. package/src/source-finder/snippet-utils.ts +423 -3
  60. package/src/tsconfig.json +0 -2
  61. package/src/types.ts +111 -2
  62. package/src/utils.ts +40 -4
@@ -2,33 +2,29 @@ import { parse } from 'node-html-parser'
2
2
  import fs from 'node:fs/promises'
3
3
  import type { IncomingMessage, ServerResponse } from 'node:http'
4
4
  import path from 'node:path'
5
- import { scanCollections } from './collection-scanner'
6
5
  import { getProjectRoot } from './config'
7
- import {
8
- buildMapPattern,
9
- detectArrayPattern,
10
- extractArrayElementProps,
11
- handleAddArrayItem,
12
- handleRemoveArrayItem,
13
- parseInlineArrayName,
14
- } from './handlers/array-ops'
6
+ import { handleCmsApiRoute } from './handlers/api-routes'
7
+ import { buildMapPattern, detectArrayPattern, extractArrayElementProps, parseInlineArrayName } from './handlers/array-ops'
15
8
  import {
16
9
  extractPropsFromSource,
17
10
  findComponentInvocationLine,
18
11
  findFrontmatterEnd,
19
12
  getPageFileCandidates,
20
- handleInsertComponent,
21
- handleRemoveComponent,
22
13
  normalizeFilePath,
23
14
  } from './handlers/component-ops'
24
- import { handleCreateMarkdown, handleDeleteMarkdown, handleGetMarkdownContent, handleUpdateMarkdown } from './handlers/markdown-ops'
25
- import { handleCors, parseJsonBody, parseMultipartFile, readBody, sendError, sendJson } from './handlers/request-utils'
26
- import { handleUpdate } from './handlers/source-writer'
15
+ import { handleCors, sendError } from './handlers/request-utils'
27
16
  import { processHtml } from './html-processor'
28
17
  import type { ManifestWriter } from './manifest-writer'
29
18
  import type { MediaStorageAdapter } from './media/types'
30
- import { clearSourceFinderCache, findCollectionSource, findImageSourceLocation, initializeSearchIndex, parseMarkdownContent } from './source-finder'
31
- import type { CmsMarkerOptions, CollectionEntry, ComponentDefinition, PageSeoData } from './types'
19
+ import {
20
+ clearSourceFinderCache,
21
+ findCollectionSource,
22
+ findImageSourceLocation,
23
+ findSourceLocation,
24
+ initializeSearchIndex,
25
+ parseMarkdownContent,
26
+ } from './source-finder'
27
+ import type { CmsMarkerOptions, CollectionEntry, ComponentDefinition } from './types'
32
28
  import { normalizePagePath } from './utils'
33
29
 
34
30
  /** Minimal ViteDevServer interface to avoid version conflicts between Astro's bundled Vite and root Vite */
@@ -263,7 +259,7 @@ export function createDevMiddleware(
263
259
  const pagePath = normalizePagePath(requestUrl)
264
260
 
265
261
  // Process HTML asynchronously
266
- processHtmlForDev(html, pagePath, config, idCounter)
262
+ processHtmlForDev(html, pagePath, config, idCounter, manifestWriter)
267
263
  .then(({ html: transformed, entries, components, collection, seo }) => {
268
264
  manifestWriter.addPage(pagePath, entries, components, collection, seo)
269
265
 
@@ -293,193 +289,12 @@ export function createDevMiddleware(
293
289
  })
294
290
  }
295
291
 
296
- async function handleCmsApiRoute(
297
- route: string,
298
- req: IncomingMessage,
299
- res: ServerResponse,
300
- manifestWriter: ManifestWriter,
301
- contentDir: string,
302
- mediaAdapter?: MediaStorageAdapter,
303
- ): Promise<void> {
304
- // POST /_nua/cms/update
305
- if (route === 'update' && req.method === 'POST') {
306
- const body = await parseJsonBody<Parameters<typeof handleUpdate>[0]>(req)
307
- const result = await handleUpdate(body, manifestWriter)
308
- sendJson(res, result)
309
- return
310
- }
311
-
312
- // POST /_nua/cms/insert-component
313
- if (route === 'insert-component' && req.method === 'POST') {
314
- const body = await parseJsonBody<Parameters<typeof handleInsertComponent>[0]>(req)
315
- const result = await handleInsertComponent(body, manifestWriter)
316
- sendJson(res, result)
317
- return
318
- }
319
-
320
- // POST /_nua/cms/remove-component
321
- if (route === 'remove-component' && req.method === 'POST') {
322
- const body = await parseJsonBody<Parameters<typeof handleRemoveComponent>[0]>(req)
323
- const result = await handleRemoveComponent(body, manifestWriter)
324
- sendJson(res, result)
325
- return
326
- }
327
-
328
- // POST /_nua/cms/add-array-item
329
- if (route === 'add-array-item' && req.method === 'POST') {
330
- const body = await parseJsonBody<Parameters<typeof handleAddArrayItem>[0]>(req)
331
- const result = await handleAddArrayItem(body, manifestWriter)
332
- sendJson(res, result)
333
- return
334
- }
335
-
336
- // POST /_nua/cms/remove-array-item
337
- if (route === 'remove-array-item' && req.method === 'POST') {
338
- const body = await parseJsonBody<Parameters<typeof handleRemoveArrayItem>[0]>(req)
339
- const result = await handleRemoveArrayItem(body, manifestWriter)
340
- sendJson(res, result)
341
- return
342
- }
343
-
344
- // GET /_nua/cms/markdown/content?filePath=...
345
- if (route === 'markdown/content' && req.method === 'GET') {
346
- const urlObj = new URL(req.url!, `http://${req.headers.host}`)
347
- const filePath = urlObj.searchParams.get('filePath')
348
- if (!filePath) {
349
- sendError(res, 'filePath query parameter required')
350
- return
351
- }
352
- const result = await handleGetMarkdownContent(filePath)
353
- if (!result) {
354
- sendError(res, 'File not found', 404)
355
- return
356
- }
357
- sendJson(res, result)
358
- return
359
- }
360
-
361
- // POST /_nua/cms/markdown/update
362
- if (route === 'markdown/update' && req.method === 'POST') {
363
- const body = await parseJsonBody<Parameters<typeof handleUpdateMarkdown>[0]>(req)
364
- const result = await handleUpdateMarkdown(body)
365
- sendJson(res, result)
366
- return
367
- }
368
-
369
- // POST /_nua/cms/markdown/create
370
- if (route === 'markdown/create' && req.method === 'POST') {
371
- const body = await parseJsonBody<Parameters<typeof handleCreateMarkdown>[0]>(req)
372
- const result = await handleCreateMarkdown(body)
373
- sendJson(res, result, result.success ? 200 : 400)
374
- return
375
- }
376
-
377
- // POST /_nua/cms/markdown/delete
378
- if (route === 'markdown/delete' && req.method === 'POST') {
379
- const body = await parseJsonBody<Parameters<typeof handleDeleteMarkdown>[0]>(req)
380
- // Register expected deletion so the Vite watcher ignores the unlink
381
- const fullPath = path.resolve(getProjectRoot(), body.filePath?.replace(/^\//, '') ?? '')
382
- expectedDeletions.add(fullPath)
383
- const result = await handleDeleteMarkdown(body)
384
- if (result.success) {
385
- // Re-scan collections so the manifest reflects the deletion
386
- const updatedCollections = await scanCollections(contentDir)
387
- manifestWriter.setCollectionDefinitions(updatedCollections)
388
- } else {
389
- expectedDeletions.delete(fullPath)
390
- }
391
- sendJson(res, result, result.success ? 200 : 400)
392
- return
393
- }
394
-
395
- // GET /_nua/cms/media/list
396
- if (route === 'media/list' && req.method === 'GET') {
397
- if (!mediaAdapter) {
398
- sendError(res, 'Media storage not configured', 501)
399
- return
400
- }
401
- const urlObj = new URL(req.url!, `http://${req.headers.host}`)
402
- const parsedLimit = parseInt(urlObj.searchParams.get('limit') ?? '50', 10)
403
- const limit = Number.isNaN(parsedLimit) || parsedLimit < 1 ? 50 : Math.min(parsedLimit, 1000)
404
- const cursor = urlObj.searchParams.get('cursor') ?? undefined
405
- const result = await mediaAdapter.list({ limit, cursor })
406
- sendJson(res, result)
407
- return
408
- }
409
-
410
- // POST /_nua/cms/media/upload
411
- if (route === 'media/upload' && req.method === 'POST') {
412
- if (!mediaAdapter) {
413
- sendError(res, 'Media storage not configured', 501)
414
- return
415
- }
416
- const contentType = req.headers['content-type'] ?? ''
417
- if (!contentType.includes('multipart/form-data')) {
418
- sendError(res, 'Expected multipart/form-data')
419
- return
420
- }
421
- // 50 MB limit for file uploads
422
- const body = await readBody(req, 50 * 1024 * 1024)
423
- const file = parseMultipartFile(body, contentType)
424
- if (!file) {
425
- sendError(res, 'No file found in request')
426
- return
427
- }
428
-
429
- // Validate file content type — allow images, videos, PDFs, and common web assets
430
- const allowedTypes = [
431
- 'image/jpeg',
432
- 'image/png',
433
- 'image/gif',
434
- 'image/webp',
435
- 'image/avif',
436
- 'image/x-icon',
437
- 'video/mp4',
438
- 'video/webm',
439
- 'application/pdf',
440
- ]
441
- // Block SVG (can contain scripts) unless explicitly served with safe headers
442
- if (!allowedTypes.includes(file.contentType)) {
443
- sendError(res, `File type not allowed: ${file.contentType}`)
444
- return
445
- }
446
-
447
- const result = await mediaAdapter.upload(file.buffer, file.filename, file.contentType)
448
- sendJson(res, result)
449
- return
450
- }
451
-
452
- // DELETE /_nua/cms/media/<id> — only match paths with an actual ID segment
453
- if (route.startsWith('media/') && req.method === 'DELETE') {
454
- if (!mediaAdapter) {
455
- sendError(res, 'Media storage not configured', 501)
456
- return
457
- }
458
- const id = route.slice('media/'.length)
459
- // Don't match known sub-routes like 'list' or 'upload'
460
- if (!id || id === 'list' || id === 'upload') {
461
- sendError(res, 'Not found', 404)
462
- return
463
- }
464
- const result = await mediaAdapter.delete(decodeURIComponent(id))
465
- sendJson(res, result)
466
- return
467
- }
468
-
469
- // GET /_nua/cms/deployment/status
470
- if (route === 'deployment/status' && req.method === 'GET') {
471
- sendJson(res, { currentDeployment: null, pendingCount: 0, deploymentEnabled: false })
472
- return
473
- }
474
-
475
- sendError(res, 'Not found', 404)
476
- }
477
-
478
292
  async function processHtmlForDev(
479
293
  html: string,
480
294
  pagePath: string,
481
295
  config: Required<CmsMarkerOptions>,
482
296
  idCounter: { value: number },
297
+ manifestWriter: ManifestWriter,
483
298
  ) {
484
299
  // Clear cached parsed files so variable definitions reflect the latest source
485
300
  clearSourceFinderCache()
@@ -524,6 +339,8 @@ async function processHtmlForDev(
524
339
  : undefined,
525
340
  // Pass SEO options
526
341
  seo: config.seo,
342
+ // Pass collection definitions for resolving frontmatter text on listing pages
343
+ collectionDefinitions: manifestWriter.getCollectionDefinitions(),
527
344
  },
528
345
  idGenerator,
529
346
  )
@@ -663,10 +480,8 @@ async function processHtmlForDev(
663
480
  // (idempotent - only scans files on first call)
664
481
  await initializeSearchIndex()
665
482
 
666
- // In dev mode, we use the source info from Astro compiler attributes
667
- // which is already extracted by html-processor
668
- // Always search for image source by src value - the sourcePath from HTML attributes
669
- // may point to a shared Image component rather than the actual usage site
483
+ // Re-resolve sources with the fully-built search index (the earlier enhancement
484
+ // step runs before the index is ready, so its results may be stale).
670
485
  for (const entry of Object.values(result.entries)) {
671
486
  if (entry.imageMetadata?.src) {
672
487
  const imageSource = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
@@ -675,6 +490,14 @@ async function processHtmlForDev(
675
490
  entry.sourceLine = imageSource.line
676
491
  entry.sourceSnippet = imageSource.snippet
677
492
  }
493
+ } else if (entry.text && entry.tag) {
494
+ const textSource = await findSourceLocation(entry.text, entry.tag)
495
+ if (textSource) {
496
+ entry.sourcePath = textSource.file
497
+ entry.sourceLine = textSource.line
498
+ entry.sourceSnippet = textSource.snippet
499
+ if (textSource.variableName) entry.variableName = textSource.variableName
500
+ }
678
501
  }
679
502
  }
680
503
 
package/src/editor/api.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { API } from './constants'
2
+ import { fetchWithTimeout } from './fetch'
2
3
  import { setAvailableTextStyles } from './text-styling'
3
4
  import type {
4
5
  CmsManifest,
@@ -81,28 +82,6 @@ export interface CmsAiStreamCallbacks {
81
82
  onDone?: (summary?: string) => void
82
83
  }
83
84
 
84
- /**
85
- * Create a fetch request with timeout
86
- */
87
- async function fetchWithTimeout(
88
- url: string,
89
- options: RequestInit = {},
90
- timeoutMs: number = API.REQUEST_TIMEOUT_MS,
91
- ): Promise<Response> {
92
- const controller = new AbortController()
93
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
94
-
95
- try {
96
- const response = await fetch(url, {
97
- ...options,
98
- signal: controller.signal,
99
- })
100
- return response
101
- } finally {
102
- clearTimeout(timeoutId)
103
- }
104
- }
105
-
106
85
  /**
107
86
  * Get the manifest URL for the current page
108
87
  * For example: /about -> /about.json
@@ -1,6 +1,6 @@
1
1
  import { marked } from 'marked'
2
2
  import { useEffect, useRef, useState } from 'preact/hooks'
3
- import { CSS } from '../constants'
3
+ import { CSS, Z_INDEX } from '../constants'
4
4
  import { getComponentInstance } from '../manifest'
5
5
  import * as signals from '../signals'
6
6
 
@@ -244,10 +244,10 @@ export const AIChat = ({ callbacks }: AIChatProps) => {
244
244
  ref={containerRef}
245
245
  class={`fixed ${dragPosition ? '' : positionClass} top-5 ${
246
246
  isMinimized ? '' : 'bottom-5'
247
- } w-100 max-w-[calc(100vw-40px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] rounded-cms-xl border border-white/10 z-2147483645 flex flex-col font-sans overflow-hidden ${
247
+ } w-100 max-w-[calc(100vw-40px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] rounded-cms-xl border border-white/10 flex flex-col font-sans overflow-hidden ${
248
248
  isDragging ? '' : 'transition-all duration-300'
249
249
  }`}
250
- style={containerStyle}
250
+ style={{ ...containerStyle, zIndex: Z_INDEX.OVERLAY }}
251
251
  data-cms-ui
252
252
  onMouseDown={stopPropagation}
253
253
  onClick={stopPropagation}
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks'
2
+ import { Z_INDEX } from '../constants'
2
3
 
3
4
  export interface AITooltipCallbacks {
4
5
  onPromptSubmit: (prompt: string, elementId: string) => void
@@ -97,7 +98,7 @@ export function AITooltip({ callbacks, visible, elementId, rect, processing }: A
97
98
  position: 'fixed',
98
99
  left: `${left}px`,
99
100
  top: `${top}px`,
100
- zIndex: 2147483645,
101
+ zIndex: Z_INDEX.OVERLAY,
101
102
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
102
103
  fontSize: '12px',
103
104
  }}
@@ -1,8 +1,10 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
2
- import { LAYOUT } from '../constants'
2
+ import { LAYOUT, Z_INDEX } from '../constants'
3
3
  import { getComponentDefinition, getComponentDefinitions, getComponentInstance, getComponentInstances } from '../manifest'
4
4
  import { manifest } from '../signals'
5
- import type { ComponentProp, InsertPosition } from '../types'
5
+ import type { InsertPosition } from '../types'
6
+ import { ComponentCard, getDefaultProps } from './component-card'
7
+ import { PropEditor } from './prop-editor'
6
8
 
7
9
  export interface BlockEditorProps {
8
10
  visible: boolean
@@ -252,19 +254,10 @@ export function BlockEditor({
252
254
  setInsertPosition(position)
253
255
 
254
256
  if (isArrayItem && currentInstance) {
255
- // For array items, skip the component picker — use the same component type
256
257
  const definition = componentDefinitions[currentInstance.componentName]
257
258
  if (definition) {
258
- const defaultProps: Record<string, any> = {}
259
- for (const prop of definition.props) {
260
- if (prop.defaultValue !== undefined) {
261
- defaultProps[prop.name] = prop.defaultValue
262
- } else if (prop.required) {
263
- defaultProps[prop.name] = ''
264
- }
265
- }
266
259
  setSelectedComponent(currentInstance.componentName)
267
- setPropValues(defaultProps)
260
+ setPropValues(getDefaultProps(definition))
268
261
  setMode('insert-props')
269
262
  return
270
263
  }
@@ -279,18 +272,8 @@ export function BlockEditor({
279
272
  const definition = componentDefinitions[componentName]
280
273
  if (!definition) return
281
274
 
282
- // Initialize with default values
283
- const defaultProps: Record<string, any> = {}
284
- for (const prop of definition.props) {
285
- if (prop.defaultValue !== undefined) {
286
- defaultProps[prop.name] = prop.defaultValue
287
- } else if (prop.required) {
288
- defaultProps[prop.name] = ''
289
- }
290
- }
291
-
292
275
  setSelectedComponent(componentName)
293
- setPropValues(defaultProps)
276
+ setPropValues(getDefaultProps(definition))
294
277
  setMode('insert-props')
295
278
  }
296
279
 
@@ -316,7 +299,8 @@ export function BlockEditor({
316
299
  data-cms-ui
317
300
  onClick={onClose}
318
301
  onMouseDown={(e: MouseEvent) => e.stopPropagation()}
319
- class="fixed inset-0 z-2147483646"
302
+ style={{ zIndex: Z_INDEX.SELECTION }}
303
+ class="fixed inset-0"
320
304
  />
321
305
 
322
306
  {/* Editor panel */}
@@ -325,11 +309,12 @@ export function BlockEditor({
325
309
  data-cms-ui
326
310
  onMouseDown={(e: MouseEvent) => e.stopPropagation()}
327
311
  onClick={(e: MouseEvent) => e.stopPropagation()}
328
- class="fixed z-2147483647 w-100 max-w-[calc(100vw-32px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] font-sans text-sm overflow-hidden flex flex-col rounded-cms-xl border border-white/10"
312
+ class="fixed w-100 max-w-[calc(100vw-32px)] bg-cms-dark shadow-[0_8px_32px_rgba(0,0,0,0.4)] font-sans text-sm overflow-hidden flex flex-col rounded-cms-xl border border-white/10"
329
313
  style={{
330
314
  top: `${editorPosition.top}px`,
331
315
  left: `${editorPosition.left}px`,
332
316
  maxHeight: `${editorPosition.maxHeight}px`,
317
+ zIndex: Z_INDEX.MODAL,
333
318
  }}
334
319
  >
335
320
  {/* Header */}
@@ -515,40 +500,11 @@ export function BlockEditor({
515
500
  </div>
516
501
  <div class="flex flex-col gap-2">
517
502
  {Object.values(componentDefinitions).map((def) => (
518
- <button
503
+ <ComponentCard
519
504
  key={def.name}
505
+ def={def}
520
506
  onClick={() => handleSelectComponentForInsert(def.name)}
521
- class="p-4 bg-white/5 border border-white/10 rounded-cms-md cursor-pointer text-left transition-all hover:border-cms-primary/50 hover:bg-white/10 group"
522
- >
523
- {def.previewUrl && (
524
- <div class="mb-3 rounded overflow-hidden bg-white h-30 relative">
525
- {(() => {
526
- const pw = def.previewWidth ?? 1280
527
- const scale = 320 / pw
528
- return (
529
- <iframe
530
- src={def.previewUrl}
531
- class="border-none pointer-events-none"
532
- style={{ width: `${pw}px`, height: `${Math.round(120 / scale)}px`, transform: `scale(${scale})`, transformOrigin: 'top left' }}
533
- sandbox="allow-same-origin"
534
- loading="lazy"
535
- tabIndex={-1}
536
- />
537
- )
538
- })()}
539
- </div>
540
- )}
541
- <div class="font-medium text-white">{def.name}</div>
542
- {def.description && (
543
- <div class="text-xs text-white/50 mt-1">
544
- {def.description}
545
- </div>
546
- )}
547
- <div class="text-[11px] text-white/40 mt-2 font-mono">
548
- {def.props.length} props
549
- {def.slots && def.slots.length > 0 && ` • ${def.slots.length} slots`}
550
- </div>
551
- </button>
507
+ />
552
508
  ))}
553
509
  </div>
554
510
  <div class="mt-5 pt-4 border-t border-white/10">
@@ -572,54 +528,3 @@ export function BlockEditor({
572
528
  </>
573
529
  )
574
530
  }
575
-
576
- interface PropEditorProps {
577
- prop: ComponentProp
578
- value: string
579
- onChange: (value: string) => void
580
- }
581
-
582
- function PropEditor({ prop, value, onChange }: PropEditorProps) {
583
- const isBoolean = prop.type === 'boolean'
584
- const isNumber = prop.type === 'number'
585
-
586
- return (
587
- <div class="mb-4">
588
- <label class="block text-[13px] font-medium text-white mb-1.5">
589
- {prop.name}
590
- {prop.required && <span class="text-cms-error ml-1">*</span>}
591
- </label>
592
- {prop.description && (
593
- <div class="text-[11px] text-white/50 mb-1.5">
594
- {prop.description}
595
- </div>
596
- )}
597
- {isBoolean
598
- ? (
599
- <label class="flex items-center gap-2 cursor-pointer">
600
- <input
601
- type="checkbox"
602
- checked={value === 'true'}
603
- onChange={(e) => onChange((e.target as HTMLInputElement).checked ? 'true' : 'false')}
604
- class="accent-cms-primary w-5 h-5 rounded"
605
- />
606
- <span class="text-[13px] text-white">
607
- {value === 'true' ? 'Enabled' : 'Disabled'}
608
- </span>
609
- </label>
610
- )
611
- : (
612
- <input
613
- type={isNumber ? 'number' : 'text'}
614
- value={value}
615
- onInput={(e) => onChange((e.target as HTMLInputElement).value)}
616
- placeholder={prop.defaultValue || `Enter ${prop.name}...`}
617
- 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"
618
- />
619
- )}
620
- <div class="text-[10px] text-white/40 mt-1.5 font-mono">
621
- {prop.type}
622
- </div>
623
- </div>
624
- )
625
- }