@nuasite/cms 0.18.1 → 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.
- package/dist/editor.js +52746 -36711
- package/package.json +16 -14
- package/src/build-processor.ts +4 -1
- package/src/collection-scanner.ts +425 -48
- package/src/dev-middleware.ts +26 -203
- package/src/editor/api.ts +1 -22
- package/src/editor/components/ai-chat.tsx +3 -3
- package/src/editor/components/ai-tooltip.tsx +2 -1
- package/src/editor/components/block-editor.tsx +13 -108
- package/src/editor/components/collections-browser.tsx +168 -205
- package/src/editor/components/component-card.tsx +49 -0
- package/src/editor/components/confirm-dialog.tsx +34 -47
- package/src/editor/components/create-page-modal.tsx +529 -101
- package/src/editor/components/delete-page-dialog.tsx +100 -0
- package/src/editor/components/fields.tsx +175 -0
- package/src/editor/components/frontmatter-fields.tsx +281 -70
- package/src/editor/components/frontmatter-sidebar.tsx +223 -0
- package/src/editor/components/highlight-overlay.ts +3 -2
- package/src/editor/components/markdown-editor-overlay.tsx +131 -85
- package/src/editor/components/markdown-inline-editor.tsx +74 -5
- package/src/editor/components/mdx-block-view.tsx +102 -0
- package/src/editor/components/mdx-component-picker.tsx +123 -0
- package/src/editor/components/mdx-props-editor.tsx +94 -0
- package/src/editor/components/media-library.tsx +373 -100
- package/src/editor/components/modal-shell.tsx +87 -0
- package/src/editor/components/prop-editor.tsx +52 -0
- package/src/editor/components/redirect-countdown.tsx +3 -1
- package/src/editor/components/redirects-manager.tsx +269 -0
- package/src/editor/components/reference-picker.tsx +203 -0
- package/src/editor/components/seo-editor.tsx +285 -303
- package/src/editor/components/toast/toast-container.tsx +2 -1
- package/src/editor/components/toolbar.tsx +177 -46
- package/src/editor/constants.ts +26 -0
- package/src/editor/editor.ts +112 -0
- package/src/editor/fetch.ts +62 -0
- package/src/editor/index.tsx +19 -1
- package/src/editor/markdown-api.ts +105 -156
- package/src/editor/milkdown-mdx-plugin.tsx +269 -0
- package/src/editor/signals.ts +206 -13
- package/src/editor/types.ts +52 -1
- package/src/handlers/api-routes.ts +251 -0
- package/src/handlers/component-ops.ts +2 -18
- package/src/handlers/markdown-ops.ts +202 -47
- package/src/handlers/page-ops.ts +229 -0
- package/src/handlers/redirect-ops.ts +163 -0
- package/src/handlers/source-writer.ts +157 -1
- package/src/html-processor.ts +14 -2
- package/src/index.ts +76 -2
- package/src/manifest-writer.ts +19 -1
- package/src/media/contember.ts +2 -1
- package/src/media/local.ts +66 -28
- package/src/media/project-images.ts +81 -0
- package/src/media/s3.ts +32 -11
- package/src/media/types.ts +24 -2
- package/src/shared.ts +27 -0
- package/src/source-finder/collection-finder.ts +219 -41
- package/src/source-finder/index.ts +7 -1
- package/src/source-finder/search-index.ts +178 -36
- package/src/source-finder/snippet-utils.ts +423 -3
- package/src/types.ts +111 -2
- package/src/utils.ts +40 -4
package/src/dev-middleware.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 {
|
|
31
|
-
|
|
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
|
-
//
|
|
667
|
-
//
|
|
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
|
|
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:
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
503
|
+
<ComponentCard
|
|
519
504
|
key={def.name}
|
|
505
|
+
def={def}
|
|
520
506
|
onClick={() => handleSelectComponentForInsert(def.name)}
|
|
521
|
-
|
|
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
|
-
}
|