@actuate-media/cms-admin 0.41.0 → 0.42.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 (40) hide show
  1. package/dist/AdminRoot.d.ts +10 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +5 -1
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/views/admin-section-renderers.test.d.ts +2 -0
  6. package/dist/__tests__/views/admin-section-renderers.test.d.ts.map +1 -0
  7. package/dist/__tests__/views/admin-section-renderers.test.js +71 -0
  8. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -0
  9. package/dist/__tests__/views/page-section-editor.test.js +14 -0
  10. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  15. package/dist/views/page-editor/PageSectionEditor.js +9 -5
  16. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  17. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  18. package/dist/views/page-editor/sections/SectionRenderer.js +25 -4
  19. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  20. package/dist/views/page-editor/sections/admin-renderers.d.ts +87 -0
  21. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -0
  22. package/dist/views/page-editor/sections/admin-renderers.js +46 -0
  23. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -0
  24. package/dist/views/page-editor/sections/index.d.ts +2 -0
  25. package/dist/views/page-editor/sections/index.d.ts.map +1 -1
  26. package/dist/views/page-editor/sections/index.js +1 -0
  27. package/dist/views/page-editor/sections/index.js.map +1 -1
  28. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  29. package/dist/views/post-editor/PostSectionEditor.js +9 -5
  30. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/AdminRoot.tsx +20 -1
  33. package/src/__tests__/views/admin-section-renderers.test.tsx +101 -0
  34. package/src/__tests__/views/page-section-editor.test.tsx +29 -0
  35. package/src/index.ts +11 -0
  36. package/src/views/page-editor/PageSectionEditor.tsx +9 -5
  37. package/src/views/page-editor/sections/SectionRenderer.tsx +30 -4
  38. package/src/views/page-editor/sections/admin-renderers.tsx +105 -0
  39. package/src/views/page-editor/sections/index.ts +12 -0
  40. package/src/views/post-editor/PostSectionEditor.tsx +13 -5
@@ -0,0 +1,105 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Pluggable canvas renderers for custom section types.
5
+ *
6
+ * By default the editor canvas renders custom (config-declared) and unmanaged
7
+ * section types as structural placeholders. Projects that want a real design
8
+ * preview inside the admin can pass renderers to `<AdminRoot
9
+ * sectionRenderers={...}>`; the canvas and the eye preview then use them in
10
+ * place of the placeholder.
11
+ *
12
+ * The renderer signature is a structural mirror of `SectionRenderer` from
13
+ * `@actuate-media/sections-react` (cms-admin depends on cms-core only, so the
14
+ * type is duplicated rather than imported). Because the shapes match, a
15
+ * consumer can hand `AdminRoot` the exact same components it registers for
16
+ * the public site:
17
+ *
18
+ * ```tsx
19
+ * import { OPAL_RENDERERS } from './sections' // SectionRendererMap
20
+ * <AdminRoot sectionRenderers={OPAL_RENDERERS} ... />
21
+ * ```
22
+ *
23
+ * Renderers run inside the admin bundle, so they must be client-safe (no
24
+ * server-only imports). Each one is wrapped in an error boundary — a throwing
25
+ * renderer falls back to the structural placeholder instead of taking down
26
+ * the editor.
27
+ */
28
+ import { createContext, useContext, type ReactNode } from 'react'
29
+
30
+ /** Structural mirror of `RenderImageProps` from `@actuate-media/sections-react`. */
31
+ export interface AdminRenderImageProps {
32
+ src: string
33
+ alt: string
34
+ width?: number | null
35
+ height?: number | null
36
+ className?: string
37
+ priority?: boolean
38
+ sizes?: string
39
+ }
40
+
41
+ /** Structural mirror of `PostContext` from `@actuate-media/sections-react`. */
42
+ export interface AdminSectionPostContext {
43
+ title?: string
44
+ excerpt?: string
45
+ body?: string
46
+ featuredImageUrl?: string
47
+ publishDate?: string | null
48
+ category?: string
49
+ author?: { name?: string; bio?: string; avatarUrl?: string }
50
+ relatedPosts?: Array<{ title: string; url?: string; excerpt?: string }>
51
+ }
52
+
53
+ /** Props passed to an admin section renderer (mirrors `SectionRendererProps`). */
54
+ export interface AdminSectionRendererProps {
55
+ section: {
56
+ id: string
57
+ sectionType: string
58
+ name?: string
59
+ visible?: boolean
60
+ content?: Record<string, unknown>
61
+ settings?: Record<string, unknown>
62
+ }
63
+ ctx?: AdminSectionPostContext
64
+ options: {
65
+ sanitizeHtml: (html: string) => string
66
+ baseUrl: string
67
+ renderImage: (props: AdminRenderImageProps) => ReactNode
68
+ }
69
+ }
70
+
71
+ /** A canvas renderer for one custom section type. */
72
+ export type AdminSectionRenderer = (props: AdminSectionRendererProps) => ReactNode
73
+
74
+ /** Section-type id → canvas renderer. */
75
+ export type AdminSectionRendererMap = Record<string, AdminSectionRenderer>
76
+
77
+ /**
78
+ * Render options passed to every admin renderer. Rich text is sanitized on
79
+ * write by cms-core, so the canvas (same trust domain as the public default)
80
+ * uses the identity sanitizer; images render as plain lazy `<img>` since the
81
+ * editor canvas has no next/image pipeline.
82
+ */
83
+ export const ADMIN_RENDER_OPTIONS: AdminSectionRendererProps['options'] = {
84
+ sanitizeHtml: (html) => html,
85
+ baseUrl: '',
86
+ renderImage: ({ src, alt, className, priority, width, height }) => (
87
+ <img
88
+ src={src}
89
+ alt={alt}
90
+ className={className}
91
+ width={width ?? undefined}
92
+ height={height ?? undefined}
93
+ loading={priority ? undefined : 'lazy'}
94
+ />
95
+ ),
96
+ }
97
+
98
+ const AdminSectionRenderersContext = createContext<AdminSectionRendererMap>({})
99
+
100
+ export const AdminSectionRenderersProvider = AdminSectionRenderersContext.Provider
101
+
102
+ /** The renderer map passed to `<AdminRoot sectionRenderers>` (empty by default). */
103
+ export function useAdminSectionRenderers(): AdminSectionRendererMap {
104
+ return useContext(AdminSectionRenderersContext)
105
+ }
@@ -1,5 +1,17 @@
1
1
  export { SectionRenderer, hasCanvasRenderer } from './SectionRenderer.js'
2
2
  export type { SectionRendererProps } from './SectionRenderer.js'
3
+ export {
4
+ AdminSectionRenderersProvider,
5
+ useAdminSectionRenderers,
6
+ ADMIN_RENDER_OPTIONS,
7
+ } from './admin-renderers.js'
8
+ export type {
9
+ AdminSectionRenderer,
10
+ AdminSectionRendererMap,
11
+ AdminSectionRendererProps,
12
+ AdminSectionPostContext,
13
+ AdminRenderImageProps,
14
+ } from './admin-renderers.js'
3
15
  export type { PostRenderContext } from './parts.js'
4
16
  export { HeroBannerSection } from './HeroBannerSection.js'
5
17
  export { MissionSection } from './MissionSection.js'
@@ -34,7 +34,11 @@ import {
34
34
  resolveSiteUrl,
35
35
  } from '../../lib/preview-link.js'
36
36
  import type { SectionSettings, SectionTypeId } from '../page-editor/section-types.js'
37
- import { hasCanvasRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
37
+ import {
38
+ hasCanvasRenderer,
39
+ useAdminSectionRenderers,
40
+ type PostRenderContext,
41
+ } from '../page-editor/sections/index.js'
38
42
  import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
39
43
  import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
40
44
  import { SectionsPanel } from '../page-editor/SectionsPanel.js'
@@ -112,6 +116,7 @@ export function PostSectionEditor({
112
116
  const [validation, setValidation] = useState<ValidationResult | null>(null)
113
117
 
114
118
  const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
119
+ const customRenderers = useAdminSectionRenderers()
115
120
 
116
121
  const typeLabel = useMemo(() => {
117
122
  const col = config?.collections?.[postType]
@@ -419,12 +424,15 @@ export function PostSectionEditor({
419
424
 
420
425
  const selected = post.sections.find((s) => s.id === selectedId) ?? null
421
426
 
422
- // Section types the canvas renders as placeholders (custom + unmanaged).
423
- // When present the editor surfaces "structure view" messaging and promotes
424
- // "View on site" as the primary design preview.
427
+ // Section types the canvas renders as placeholders no built-in body
428
+ // component and no consumer-registered admin renderer. When present the
429
+ // editor surfaces "structure view" messaging and promotes "View on site"
430
+ // as the primary design preview.
425
431
  const structuralTypes = Array.from(
426
432
  new Set(
427
- post.sections.filter((s) => !hasCanvasRenderer(s.sectionType)).map((s) => s.sectionType),
433
+ post.sections
434
+ .filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
435
+ .map((s) => s.sectionType),
428
436
  ),
429
437
  )
430
438