@actuate-media/cms-admin 0.53.0 → 0.56.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/CHANGELOG.md +30 -0
- package/dist/__tests__/lib/page-editor-service.test.js +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/views/section-inspector-media.test.d.ts +2 -0
- package/dist/__tests__/views/section-inspector-media.test.d.ts.map +1 -0
- package/dist/__tests__/views/section-inspector-media.test.js +21 -0
- package/dist/__tests__/views/section-inspector-media.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/lib/seo-service.d.ts +0 -2
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts +11 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +36 -7
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/sections/EmbedSection.d.ts +10 -0
- package/dist/views/page-editor/sections/EmbedSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/EmbedSection.js +24 -0
- package/dist/views/page-editor/sections/EmbedSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +4 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +0 -10
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lib/page-editor-service.test.ts +1 -0
- package/src/__tests__/views/section-inspector-media.test.ts +23 -0
- package/src/lib/seo-service.ts +0 -2
- package/src/views/page-editor/SectionInspector.tsx +43 -2
- package/src/views/page-editor/sections/EmbedSection.tsx +41 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +4 -0
- package/src/views/seo/TechnicalTab.tsx +0 -10
|
@@ -148,6 +148,7 @@ export function SectionInspector({
|
|
|
148
148
|
field={field}
|
|
149
149
|
value={section.content[field.key]}
|
|
150
150
|
onChange={(v) => onContentChange({ [field.key]: v })}
|
|
151
|
+
onPatch={(patch) => onContentChange(patch)}
|
|
151
152
|
/>
|
|
152
153
|
))}
|
|
153
154
|
</fieldset>
|
|
@@ -302,11 +303,17 @@ function ContentField({
|
|
|
302
303
|
field,
|
|
303
304
|
value,
|
|
304
305
|
onChange,
|
|
306
|
+
onPatch,
|
|
305
307
|
id: idProp,
|
|
306
308
|
}: {
|
|
307
309
|
field: SectionFieldDef
|
|
308
310
|
value: unknown
|
|
309
311
|
onChange: (v: unknown) => void
|
|
312
|
+
/**
|
|
313
|
+
* Optional multi-key patch sink — lets media fields persist companion
|
|
314
|
+
* dimension keys (width/height) atomically with the URL.
|
|
315
|
+
*/
|
|
316
|
+
onPatch?: (patch: Record<string, unknown>) => void
|
|
310
317
|
/** Explicit input id (repeater rows pass a row-scoped id to stay unique). */
|
|
311
318
|
id?: string
|
|
312
319
|
}) {
|
|
@@ -335,7 +342,7 @@ function ContentField({
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
if (field.type === 'media') {
|
|
338
|
-
return <MediaField id={id} field={field} value={text} onChange={onChange} />
|
|
345
|
+
return <MediaField id={id} field={field} value={text} onChange={onChange} onPatch={onPatch} />
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
if (field.type === 'select') {
|
|
@@ -393,16 +400,39 @@ function ContentField({
|
|
|
393
400
|
)
|
|
394
401
|
}
|
|
395
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Companion content keys that hold an image field's intrinsic dimensions.
|
|
405
|
+
* Picking from the media library persists width/height alongside the URL so the
|
|
406
|
+
* public renderer can use the CLS-safe `next/image` path without waiting on
|
|
407
|
+
* read-time `/resolve` enrichment. Mirrors the keys the section renderers read:
|
|
408
|
+
* `imageUrl` → `imageWidth` / `imageHeight`; `url` (gallery row) → `width` / `height`.
|
|
409
|
+
*/
|
|
410
|
+
export function companionDimensionKeys(fieldKey: string): { width: string; height: string } {
|
|
411
|
+
if (fieldKey === 'url') return { width: 'width', height: 'height' }
|
|
412
|
+
if (fieldKey.endsWith('Url')) {
|
|
413
|
+
const base = fieldKey.slice(0, -'Url'.length)
|
|
414
|
+
return { width: `${base}Width`, height: `${base}Height` }
|
|
415
|
+
}
|
|
416
|
+
return { width: `${fieldKey}Width`, height: `${fieldKey}Height` }
|
|
417
|
+
}
|
|
418
|
+
|
|
396
419
|
function MediaField({
|
|
397
420
|
id,
|
|
398
421
|
field,
|
|
399
422
|
value,
|
|
400
423
|
onChange,
|
|
424
|
+
onPatch,
|
|
401
425
|
}: {
|
|
402
426
|
id: string
|
|
403
427
|
field: SectionFieldDef
|
|
404
428
|
value: string
|
|
405
429
|
onChange: (v: unknown) => void
|
|
430
|
+
/**
|
|
431
|
+
* When provided, picking from the library applies a multi-key patch (URL +
|
|
432
|
+
* intrinsic width/height) in one update. Falls back to `onChange` (URL only)
|
|
433
|
+
* for manual entry and when no patch sink is wired.
|
|
434
|
+
*/
|
|
435
|
+
onPatch?: (patch: Record<string, unknown>) => void
|
|
406
436
|
}) {
|
|
407
437
|
const [open, setOpen] = useState(false)
|
|
408
438
|
return (
|
|
@@ -434,8 +464,18 @@ function MediaField({
|
|
|
434
464
|
<MediaPickerModal
|
|
435
465
|
open={open}
|
|
436
466
|
onClose={() => setOpen(false)}
|
|
467
|
+
onSelectItem={(item) => {
|
|
468
|
+
if (!onPatch) return
|
|
469
|
+
const { width: wKey, height: hKey } = companionDimensionKeys(field.key)
|
|
470
|
+
const patch: Record<string, unknown> = { [field.key]: item.url }
|
|
471
|
+
if (typeof item.width === 'number') patch[wKey] = item.width
|
|
472
|
+
if (typeof item.height === 'number') patch[hKey] = item.height
|
|
473
|
+
onPatch(patch)
|
|
474
|
+
}}
|
|
437
475
|
onSelect={(url) => {
|
|
438
|
-
|
|
476
|
+
// onSelectItem handles persistence when a patch sink is wired; only
|
|
477
|
+
// fall back to a URL-only update when it isn't.
|
|
478
|
+
if (!onPatch) onChange(url)
|
|
439
479
|
setOpen(false)
|
|
440
480
|
}}
|
|
441
481
|
accept="image/*"
|
|
@@ -743,6 +783,7 @@ function SortableRepeaterRow({
|
|
|
743
783
|
field={sub}
|
|
744
784
|
value={row[sub.key]}
|
|
745
785
|
onChange={(v) => onPatch({ [sub.key]: v })}
|
|
786
|
+
onPatch={(patch) => onPatch(patch)}
|
|
746
787
|
/>
|
|
747
788
|
))}
|
|
748
789
|
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PlayCircle } from 'lucide-react'
|
|
2
|
+
import { parseEmbed, EMBED_PROVIDER_LABEL } from '@actuate-media/cms-core/page-sections'
|
|
3
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
4
|
+
import { str } from './parts.js'
|
|
5
|
+
|
|
6
|
+
const RATIO_CLASS: Record<string, string> = {
|
|
7
|
+
'16:9': 'aspect-video',
|
|
8
|
+
'4:3': 'aspect-[4/3]',
|
|
9
|
+
'1:1': 'aspect-square',
|
|
10
|
+
'9:16': 'aspect-[9/16]',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canvas body for the `embed` (video) section. Mirrors the public renderer's
|
|
15
|
+
* reserved aspect-ratio box, but shows a lightweight provider placeholder
|
|
16
|
+
* instead of loading the live external iframe inside the editor.
|
|
17
|
+
*/
|
|
18
|
+
export function EmbedSection({ section }: { section: PageSection }) {
|
|
19
|
+
const c = section.content
|
|
20
|
+
const parsed = parseEmbed(str(c, 'url'))
|
|
21
|
+
const ratioClass = RATIO_CLASS[str(c, 'ratio', '16:9')] ?? RATIO_CLASS['16:9']
|
|
22
|
+
const title = str(c, 'title')
|
|
23
|
+
const caption = str(c, 'caption')
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<figure className="mx-auto max-w-3xl px-6">
|
|
27
|
+
<div
|
|
28
|
+
className={`relative flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-xl bg-zinc-100 text-zinc-500 ${ratioClass}`}
|
|
29
|
+
>
|
|
30
|
+
<PlayCircle className="h-10 w-10" aria-hidden />
|
|
31
|
+
<p className="text-sm font-medium text-zinc-700">
|
|
32
|
+
{parsed ? `${EMBED_PROVIDER_LABEL[parsed.provider]} video` : 'No valid video URL'}
|
|
33
|
+
</p>
|
|
34
|
+
{title && <p className="px-4 text-center text-xs text-zinc-500">{title}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
{caption && (
|
|
37
|
+
<figcaption className="mt-2 text-center text-sm text-zinc-500">{caption}</figcaption>
|
|
38
|
+
)}
|
|
39
|
+
</figure>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -16,6 +16,7 @@ import { PullQuoteSection } from './PullQuoteSection.js'
|
|
|
16
16
|
import { AuthorBioSection } from './AuthorBioSection.js'
|
|
17
17
|
import { RelatedPostsSection } from './RelatedPostsSection.js'
|
|
18
18
|
import { GallerySection } from './GallerySection.js'
|
|
19
|
+
import { EmbedSection } from './EmbedSection.js'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Section types with a real canvas body component below. Anything else —
|
|
@@ -34,6 +35,7 @@ const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
|
|
|
34
35
|
'author-bio',
|
|
35
36
|
'related-posts',
|
|
36
37
|
'gallery',
|
|
38
|
+
'embed',
|
|
37
39
|
])
|
|
38
40
|
|
|
39
41
|
/** True when the canvas renders this type for real (not as a placeholder). */
|
|
@@ -66,6 +68,8 @@ function SectionBody({ section, context }: { section: PageSection; context?: Pos
|
|
|
66
68
|
return <RelatedPostsSection section={section} context={context} />
|
|
67
69
|
case 'gallery':
|
|
68
70
|
return <GallerySection section={section} />
|
|
71
|
+
case 'embed':
|
|
72
|
+
return <EmbedSection section={section} />
|
|
69
73
|
default: {
|
|
70
74
|
// Custom (config-declared) and unmanaged types: use a consumer-supplied
|
|
71
75
|
// canvas renderer when one is registered (see admin-renderers.tsx).
|
|
@@ -40,16 +40,6 @@ const CRAWL_TOGGLES: { key: keyof CrawlSettings; label: string; help: string }[]
|
|
|
40
40
|
label: 'Include posts in sitemap',
|
|
41
41
|
help: 'Add published posts to the sitemap.',
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
key: 'noindexPaginatedArchives',
|
|
45
|
-
label: 'No-index paginated archives',
|
|
46
|
-
help: 'Avoid duplicate-content penalties on page 2+.',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
key: 'noindexTagPages',
|
|
50
|
-
label: 'No-index tag pages',
|
|
51
|
-
help: 'Keep thin tag archives out of the index.',
|
|
52
|
-
},
|
|
53
43
|
]
|
|
54
44
|
|
|
55
45
|
function Toggle({
|