@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/__tests__/lib/page-editor-service.test.js +1 -0
  3. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  4. package/dist/__tests__/views/section-inspector-media.test.d.ts +2 -0
  5. package/dist/__tests__/views/section-inspector-media.test.d.ts.map +1 -0
  6. package/dist/__tests__/views/section-inspector-media.test.js +21 -0
  7. package/dist/__tests__/views/section-inspector-media.test.js.map +1 -0
  8. package/dist/actuate-admin.css +1 -1
  9. package/dist/lib/seo-service.d.ts +0 -2
  10. package/dist/lib/seo-service.d.ts.map +1 -1
  11. package/dist/lib/seo-service.js.map +1 -1
  12. package/dist/views/page-editor/SectionInspector.d.ts +11 -0
  13. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  14. package/dist/views/page-editor/SectionInspector.js +36 -7
  15. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  16. package/dist/views/page-editor/sections/EmbedSection.d.ts +10 -0
  17. package/dist/views/page-editor/sections/EmbedSection.d.ts.map +1 -0
  18. package/dist/views/page-editor/sections/EmbedSection.js +24 -0
  19. package/dist/views/page-editor/sections/EmbedSection.js.map +1 -0
  20. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  21. package/dist/views/page-editor/sections/SectionRenderer.js +4 -0
  22. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  23. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  24. package/dist/views/seo/TechnicalTab.js +0 -10
  25. package/dist/views/seo/TechnicalTab.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/__tests__/lib/page-editor-service.test.ts +1 -0
  28. package/src/__tests__/views/section-inspector-media.test.ts +23 -0
  29. package/src/lib/seo-service.ts +0 -2
  30. package/src/views/page-editor/SectionInspector.tsx +43 -2
  31. package/src/views/page-editor/sections/EmbedSection.tsx +41 -0
  32. package/src/views/page-editor/sections/SectionRenderer.tsx +4 -0
  33. 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
- onChange(url)
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({