@prenta/admin 1.9.2 → 1.10.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 (85) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/AdminRoot.d.ts +10 -0
  3. package/dist/AdminRoot.d.ts.map +1 -1
  4. package/dist/AdminRoot.js +2 -1
  5. package/dist/AdminRoot.js.map +1 -1
  6. package/dist/__tests__/lib/theme-chrome.test.d.ts +2 -0
  7. package/dist/__tests__/lib/theme-chrome.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/theme-chrome.test.js +104 -0
  9. package/dist/__tests__/lib/theme-chrome.test.js.map +1 -0
  10. package/dist/__tests__/views/editor-top-bar.render.test.js +11 -0
  11. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  12. package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts +2 -0
  13. package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts.map +1 -0
  14. package/dist/__tests__/views/theme-chrome-frame.render.test.js +236 -0
  15. package/dist/__tests__/views/theme-chrome-frame.render.test.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/theme-chrome.d.ts +87 -0
  21. package/dist/lib/theme-chrome.d.ts.map +1 -0
  22. package/dist/lib/theme-chrome.js +117 -0
  23. package/dist/lib/theme-chrome.js.map +1 -0
  24. package/dist/prenta-admin.css +1 -1
  25. package/dist/views/page-editor/CanvasSurface.d.ts +14 -2
  26. package/dist/views/page-editor/CanvasSurface.d.ts.map +1 -1
  27. package/dist/views/page-editor/CanvasSurface.js +30 -14
  28. package/dist/views/page-editor/CanvasSurface.js.map +1 -1
  29. package/dist/views/page-editor/EditorCanvas.d.ts +12 -1
  30. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  31. package/dist/views/page-editor/EditorCanvas.js +22 -5
  32. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  33. package/dist/views/page-editor/EditorTopBar.d.ts +5 -1
  34. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  35. package/dist/views/page-editor/EditorTopBar.js +3 -2
  36. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  37. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  38. package/dist/views/page-editor/PageSectionEditor.js +30 -2
  39. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  40. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts +21 -0
  41. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts.map +1 -0
  42. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js +34 -0
  43. package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js.map +1 -0
  44. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts +12 -0
  45. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts.map +1 -0
  46. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js +12 -0
  47. package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js.map +1 -0
  48. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts +11 -0
  49. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts.map +1 -0
  50. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js +15 -0
  51. package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js.map +1 -0
  52. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts +18 -0
  53. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts.map +1 -0
  54. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js +23 -0
  55. package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js.map +1 -0
  56. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts +11 -0
  57. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts.map +1 -0
  58. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js +82 -0
  59. package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js.map +1 -0
  60. package/dist/views/page-editor/theme-chrome/index.d.ts +9 -0
  61. package/dist/views/page-editor/theme-chrome/index.d.ts.map +1 -0
  62. package/dist/views/page-editor/theme-chrome/index.js +6 -0
  63. package/dist/views/page-editor/theme-chrome/index.js.map +1 -0
  64. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts +3 -0
  65. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts.map +1 -0
  66. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js +17 -0
  67. package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js.map +1 -0
  68. package/package.json +4 -4
  69. package/src/AdminRoot.tsx +14 -1
  70. package/src/__tests__/lib/theme-chrome.test.ts +132 -0
  71. package/src/__tests__/views/editor-top-bar.render.test.tsx +13 -0
  72. package/src/__tests__/views/theme-chrome-frame.render.test.tsx +304 -0
  73. package/src/index.ts +17 -0
  74. package/src/lib/theme-chrome.ts +172 -0
  75. package/src/views/page-editor/CanvasSurface.tsx +70 -24
  76. package/src/views/page-editor/EditorCanvas.tsx +89 -15
  77. package/src/views/page-editor/EditorTopBar.tsx +10 -0
  78. package/src/views/page-editor/PageSectionEditor.tsx +42 -0
  79. package/src/views/page-editor/theme-chrome/ThemeChromeFrame.tsx +119 -0
  80. package/src/views/page-editor/theme-chrome/ThemeChromeHealth.tsx +66 -0
  81. package/src/views/page-editor/theme-chrome/ThemeChromeModeToggle.tsx +63 -0
  82. package/src/views/page-editor/theme-chrome/ThemeChromeProvider.tsx +43 -0
  83. package/src/views/page-editor/theme-chrome/ThemeChromeSlot.tsx +235 -0
  84. package/src/views/page-editor/theme-chrome/index.ts +12 -0
  85. package/src/views/page-editor/theme-chrome/useThemeChromeMode.ts +24 -0
@@ -1,12 +1,22 @@
1
1
  'use client'
2
2
 
3
+ import { useCallback, useMemo, useState } from 'react'
3
4
  import { LayoutTemplate } from 'lucide-react'
4
5
  import { CanvasEditTip } from '../../components/CanvasEditTip.js'
5
6
  import { CanvasScroll, CanvasSurface, useSectionRefs } from './CanvasSurface.js'
6
7
  import type { BreakpointId, Zoom } from '../../lib/breakpoints.js'
7
8
  import type { PageSection } from '../../lib/page-editor-service.js'
9
+ import {
10
+ themeChromeEditPath,
11
+ type AdminThemeChrome,
12
+ type ThemeChromeDocument,
13
+ type ThemeChromeEditTarget,
14
+ type ThemeChromeMode,
15
+ } from '../../lib/theme-chrome.js'
8
16
  import { SectionRenderer } from './sections/index.js'
9
17
  import { StructureNotice } from './StructureNotice.js'
18
+ import { ThemeChromeFrame, ThemeChromeHealth } from './theme-chrome/index.js'
19
+ import type { ThemeChromeHealthState } from './theme-chrome/ThemeChromeFrame.js'
10
20
 
11
21
  interface EditorCanvasProps {
12
22
  sections: PageSection[]
@@ -31,6 +41,16 @@ interface EditorCanvasProps {
31
41
  * to the editor's history state, which the canvas knows nothing about.
32
42
  */
33
43
  reviewBar?: React.ReactNode
44
+ /** When set and enabled, wraps bands in site header/footer. */
45
+ themeChrome?: {
46
+ spec: AdminThemeChrome
47
+ enabled: boolean
48
+ mode: ThemeChromeMode
49
+ locale: string
50
+ dir: 'ltr' | 'rtl'
51
+ document: ThemeChromeDocument
52
+ onNavigate: (path: string) => void
53
+ }
34
54
  }
35
55
 
36
56
  /**
@@ -52,13 +72,44 @@ export function EditorCanvas({
52
72
  reviewing = false,
53
73
  changedSectionIds,
54
74
  reviewBar,
75
+ themeChrome,
55
76
  }: EditorCanvasProps) {
56
77
  const registerRef = useSectionRefs(selectedId)
78
+ const [health, setHealth] = useState<ThemeChromeHealthState | null>(null)
79
+
80
+ const chromeOn = Boolean(themeChrome?.enabled)
81
+ const mode = themeChrome?.mode ?? 'flow'
82
+
83
+ const onEditChrome = useCallback(
84
+ (target: ThemeChromeEditTarget) => {
85
+ themeChrome?.onNavigate(themeChromeEditPath(target))
86
+ },
87
+ [themeChrome],
88
+ )
89
+
90
+ const documentIdentity = themeChrome?.document
91
+
92
+ const bands = useMemo(() => {
93
+ if (sections.length === 0) return <EmptyCanvas />
94
+ return sections.map((section) => (
95
+ <SectionRenderer
96
+ key={section.id}
97
+ section={section}
98
+ editable
99
+ reviewing={reviewing}
100
+ changed={changedSectionIds?.has(section.id) ?? false}
101
+ selected={section.id === selectedId}
102
+ onSelect={onSelect}
103
+ registerRef={registerRef}
104
+ />
105
+ ))
106
+ }, [sections, reviewing, changedSectionIds, selectedId, onSelect, registerRef])
57
107
 
58
108
  // A design-owned page always ships the template's sections, so an empty
59
109
  // canvas means the template resolved to nothing — a configuration problem,
60
- // not a page to scale and measure.
61
- if (sections.length === 0) {
110
+ // not a page to scale and measure. Theme chrome still mounts so editors
111
+ // can see and edit the site shell.
112
+ if (sections.length === 0 && !chromeOn) {
62
113
  return (
63
114
  <CanvasScroll>
64
115
  <EmptyCanvas />
@@ -66,13 +117,47 @@ export function EditorCanvas({
66
117
  )
67
118
  }
68
119
 
120
+ const inner =
121
+ chromeOn && themeChrome && documentIdentity ? (
122
+ <ThemeChromeFrame
123
+ spec={themeChrome.spec}
124
+ mode={mode}
125
+ breakpoint={breakpoint}
126
+ locale={themeChrome.locale}
127
+ dir={themeChrome.dir}
128
+ document={documentIdentity}
129
+ onEditChrome={onEditChrome}
130
+ onHealthChange={setHealth}
131
+ >
132
+ {bands}
133
+ </ThemeChromeFrame>
134
+ ) : (
135
+ bands
136
+ )
137
+
69
138
  return (
70
- <CanvasScroll>
139
+ <CanvasScroll layout={chromeOn && mode === 'frame' ? 'frame' : 'flow'}>
71
140
  <CanvasSurface
72
141
  breakpoint={breakpoint}
73
142
  zoom={zoom}
74
143
  onScaleChange={onScaleChange}
75
144
  onCanvasElement={onCanvasElement}
145
+ lang={chromeOn ? themeChrome?.locale : undefined}
146
+ dir={chromeOn ? themeChrome?.dir : undefined}
147
+ fillMode={chromeOn && mode === 'frame' ? 'frame' : 'flow'}
148
+ health={
149
+ chromeOn && themeChrome && health ? (
150
+ <ThemeChromeHealth
151
+ locale={themeChrome.locale}
152
+ dir={themeChrome.dir}
153
+ header={health.header}
154
+ footer={health.footer}
155
+ outlineWarning={health.outlineWarning}
156
+ navLinkCount={themeChrome.spec.diagnostics?.navLinkCount}
157
+ brandPresent={themeChrome.spec.diagnostics?.brandPresent}
158
+ />
159
+ ) : undefined
160
+ }
76
161
  chrome={
77
162
  // The review bar REPLACES the editing chrome rather than joining it:
78
163
  // a tip about clicking sections to edit them is actively wrong while
@@ -87,18 +172,7 @@ export function EditorCanvas({
87
172
  )
88
173
  }
89
174
  >
90
- {sections.map((section) => (
91
- <SectionRenderer
92
- key={section.id}
93
- section={section}
94
- editable
95
- reviewing={reviewing}
96
- changed={changedSectionIds?.has(section.id) ?? false}
97
- selected={section.id === selectedId}
98
- onSelect={onSelect}
99
- registerRef={registerRef}
100
- />
101
- ))}
175
+ {inner}
102
176
  </CanvasSurface>
103
177
  </CanvasScroll>
104
178
  )
@@ -30,6 +30,8 @@ import {
30
30
  type WorkflowStage,
31
31
  } from '../../lib/workflow-chrome.js'
32
32
  import { type ViewportId } from './viewports.js'
33
+ import type { ThemeChromeMode } from '../../lib/theme-chrome.js'
34
+ import { ThemeChromeModeToggle } from './theme-chrome/ThemeChromeModeToggle.js'
33
35
 
34
36
  export type SaveState = 'idle' | 'saving' | 'saved' | 'error'
35
37
 
@@ -56,6 +58,9 @@ interface EditorTopBarProps {
56
58
  /** Breadcrumb root label + back-button text (e.g. "Pages" or "Posts"). */
57
59
  backLabel?: string
58
60
  onViewport: (v: ViewportId) => void
61
+ /** Theme-chrome Page / Frame toggle. Hidden when theme chrome is off. */
62
+ chromeMode?: ThemeChromeMode
63
+ onChromeMode?: (mode: ThemeChromeMode) => void
59
64
  onBack: () => void
60
65
  /**
61
66
  * Open the public site in a new tab. Called with `preview-draft` or
@@ -129,6 +134,8 @@ export function EditorTopBar({
129
134
  onZoom,
130
135
  backLabel = 'Pages',
131
136
  onViewport,
137
+ chromeMode,
138
+ onChromeMode,
132
139
  onBack,
133
140
  onViewOnSite,
134
141
  onShare,
@@ -281,6 +288,9 @@ export function EditorTopBar({
281
288
  {zoom !== undefined && onZoom && (
282
289
  <ZoomChip zoom={zoom} scale={canvasScale ?? 1} onChange={onZoom} />
283
290
  )}
291
+ {chromeMode && onChromeMode ? (
292
+ <ThemeChromeModeToggle mode={chromeMode} onChange={onChromeMode} />
293
+ ) : null}
284
294
  </div>
285
295
 
286
296
  <button
@@ -55,6 +55,15 @@ import { hasCanvasRenderer, useAdminSectionRenderers } from './sections/index.js
55
55
  import { EditorTopBar, type SaveState } from './EditorTopBar.js'
56
56
  import { SectionsList } from './SectionsPanel.js'
57
57
  import { EditorCanvas } from './EditorCanvas.js'
58
+ import { useLocale } from '../../components/LocaleProvider.js'
59
+ import {
60
+ isThemeChromeEnabled,
61
+ resolveThemeChromeDir,
62
+ resolveThemeChromeLocale,
63
+ resolveThemeChromePath,
64
+ } from '../../lib/theme-chrome.js'
65
+ import { useThemeChromeSpec } from './theme-chrome/ThemeChromeProvider.js'
66
+ import { useThemeChromeMode } from './theme-chrome/useThemeChromeMode.js'
58
67
  import { ReflowContext } from './reflow-context.js'
59
68
  import { SectionEditForm } from './SectionEditForm.js'
60
69
  import { PageSettingsModal } from './PageSettingsModal.js'
@@ -135,6 +144,19 @@ export function PageSectionEditor({
135
144
  const [pendingFocusPath, setPendingFocusPath] = useState<string | null>(null)
136
145
  const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
137
146
  const [zoom, setZoom] = useState<Zoom>('fit')
147
+ const themeChromeSpec = useThemeChromeSpec()
148
+ const themeChromeEnabled = isThemeChromeEnabled(
149
+ themeChromeSpec,
150
+ config?.admin?.editor?.themeChrome,
151
+ 'pages',
152
+ )
153
+ const [chromeMode, setChromeMode] = useThemeChromeMode(themeChromeSpec?.defaultMode ?? 'flow')
154
+ const { activeLocale, locales } = useLocale()
155
+ const chromeLocale = resolveThemeChromeLocale({
156
+ activeLocale,
157
+ defaultLocale: config?.i18n?.defaultLocale,
158
+ })
159
+ const chromeDir = resolveThemeChromeDir(chromeLocale, locales)
138
160
  /**
139
161
  * Live audit reported by the SEO pane, built from unsaved form state. Null
140
162
  * until that pane has mounted — `audit` below falls back to the saved-record
@@ -881,6 +903,8 @@ export function PageSectionEditor({
881
903
  zoom={zoom}
882
904
  canvasScale={canvasScale}
883
905
  onZoom={setZoom}
906
+ chromeMode={themeChromeEnabled ? chromeMode : undefined}
907
+ onChromeMode={themeChromeEnabled ? setChromeMode : undefined}
884
908
  onBack={() => guardedNavigate('/pages')}
885
909
  onViewOnSite={page.id && page.slug ? handleViewOnSite : undefined}
886
910
  onShare={page.id ? handleShare : undefined}
@@ -1050,6 +1074,24 @@ export function PageSectionEditor({
1050
1074
  structuralTypes={structuralTypes}
1051
1075
  reviewing={reviewing}
1052
1076
  changedSectionIds={reviewChangedIds}
1077
+ themeChrome={
1078
+ themeChromeSpec
1079
+ ? {
1080
+ spec: themeChromeSpec,
1081
+ enabled: themeChromeEnabled,
1082
+ mode: chromeMode,
1083
+ locale: chromeLocale,
1084
+ dir: chromeDir,
1085
+ document: {
1086
+ id: page.id,
1087
+ title: page.title,
1088
+ path: resolveThemeChromePath(page.path, page.slug),
1089
+ collection: PAGES_COLLECTION,
1090
+ },
1091
+ onNavigate,
1092
+ }
1093
+ : undefined
1094
+ }
1053
1095
  reviewBar={
1054
1096
  reviewVersion ? (
1055
1097
  <ReviewBar
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
4
+ import {
5
+ THEME_CHROME_MAIN_ID,
6
+ type AdminThemeChrome,
7
+ type AdminThemeChromeProps,
8
+ type ThemeChromeDocument,
9
+ type ThemeChromeEditTarget,
10
+ type ThemeChromeMode,
11
+ type ThemeChromeSlotStatus,
12
+ } from '../../../lib/theme-chrome.js'
13
+ import type { BreakpointId } from '../../../lib/breakpoints.js'
14
+ import { ThemeChromeSlot } from './ThemeChromeSlot.js'
15
+
16
+ export interface ThemeChromeHealthState {
17
+ header: ThemeChromeSlotStatus
18
+ footer: ThemeChromeSlotStatus
19
+ outlineWarning: boolean
20
+ }
21
+
22
+ export interface ThemeChromeFrameProps {
23
+ spec: AdminThemeChrome
24
+ mode: ThemeChromeMode
25
+ breakpoint: BreakpointId
26
+ locale: string
27
+ dir: 'ltr' | 'rtl'
28
+ document: ThemeChromeDocument
29
+ onEditChrome: (target: ThemeChromeEditTarget) => void
30
+ onHealthChange?: (health: ThemeChromeHealthState) => void
31
+ children: ReactNode
32
+ }
33
+
34
+ export function ThemeChromeFrame({
35
+ spec,
36
+ mode,
37
+ breakpoint,
38
+ locale,
39
+ dir,
40
+ document,
41
+ onEditChrome,
42
+ onHealthChange,
43
+ children,
44
+ }: ThemeChromeFrameProps) {
45
+ const [headerStatus, setHeaderStatus] = useState<ThemeChromeSlotStatus>('placeholder')
46
+ const [footerStatus, setFooterStatus] = useState<ThemeChromeSlotStatus>('placeholder')
47
+ const [headerH1, setHeaderH1] = useState(false)
48
+ const [footerH1, setFooterH1] = useState(false)
49
+
50
+ const chromeProps = useMemo<Omit<AdminThemeChromeProps, 'region'>>(
51
+ () => ({
52
+ breakpoint,
53
+ mode,
54
+ preview: true,
55
+ locale,
56
+ dir,
57
+ document,
58
+ onEditChrome,
59
+ }),
60
+ [breakpoint, mode, locale, dir, document, onEditChrome],
61
+ )
62
+
63
+ const onHeaderOutline = useCallback((hasH1: boolean) => setHeaderH1(hasH1), [])
64
+ const onFooterOutline = useCallback((hasH1: boolean) => setFooterH1(hasH1), [])
65
+
66
+ useEffect(() => {
67
+ onHealthChange?.({
68
+ header: headerStatus,
69
+ footer: footerStatus,
70
+ outlineWarning: headerH1 || footerH1,
71
+ })
72
+ }, [headerStatus, footerStatus, headerH1, footerH1, onHealthChange])
73
+
74
+ const headerProps: AdminThemeChromeProps = { ...chromeProps, region: 'header' }
75
+ const footerProps: AdminThemeChromeProps = { ...chromeProps, region: 'footer' }
76
+
77
+ const frameClass = mode === 'frame' ? 'flex h-full min-h-0 flex-col' : 'flex min-h-0 flex-col'
78
+
79
+ return (
80
+ <div
81
+ className={frameClass}
82
+ data-prenta-theme-chrome-mode={mode}
83
+ data-prenta-theme-chrome-locale={locale}
84
+ data-prenta-theme-chrome-dir={dir}
85
+ data-prenta-theme-chrome-path={document.path}
86
+ >
87
+ <a
88
+ href={`#${THEME_CHROME_MAIN_ID}`}
89
+ className="bg-primary text-primary-foreground sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-30 focus:rounded-md focus:px-3 focus:py-1.5 focus:text-sm"
90
+ >
91
+ Skip to content
92
+ </a>
93
+ <div className={mode === 'frame' ? 'shrink-0' : undefined}>
94
+ <ThemeChromeSlot
95
+ region="header"
96
+ spec={spec}
97
+ chromeProps={headerProps}
98
+ onOutlineIssue={onHeaderOutline}
99
+ onStatus={setHeaderStatus}
100
+ />
101
+ </div>
102
+ <div
103
+ id={THEME_CHROME_MAIN_ID}
104
+ className={mode === 'frame' ? 'min-h-0 flex-1 overflow-auto' : undefined}
105
+ >
106
+ {children}
107
+ </div>
108
+ <div className={mode === 'frame' ? 'shrink-0' : undefined}>
109
+ <ThemeChromeSlot
110
+ region="footer"
111
+ spec={spec}
112
+ chromeProps={footerProps}
113
+ onOutlineIssue={onFooterOutline}
114
+ onStatus={setFooterStatus}
115
+ />
116
+ </div>
117
+ </div>
118
+ )
119
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import type { ThemeChromeSlotStatus } from '../../../lib/theme-chrome.js'
4
+
5
+ export interface ThemeChromeHealthProps {
6
+ locale: string
7
+ dir: 'ltr' | 'rtl'
8
+ header: ThemeChromeSlotStatus
9
+ footer: ThemeChromeSlotStatus
10
+ outlineWarning: boolean
11
+ navLinkCount?: number
12
+ brandPresent?: boolean
13
+ }
14
+
15
+ const STATUS_LABEL: Record<ThemeChromeSlotStatus, string> = {
16
+ ok: 'ok',
17
+ preview: 'preview',
18
+ fallback: 'fallback',
19
+ placeholder: 'View site',
20
+ }
21
+
22
+ export function ThemeChromeHealth({
23
+ locale,
24
+ dir,
25
+ header,
26
+ footer,
27
+ outlineWarning,
28
+ navLinkCount,
29
+ brandPresent,
30
+ }: ThemeChromeHealthProps) {
31
+ return (
32
+ <p
33
+ className="text-muted-foreground mb-3 flex flex-wrap items-center justify-center gap-x-1.5 gap-y-1 text-[11px] font-medium tracking-[0.3px]"
34
+ data-prenta-theme-chrome-health=""
35
+ aria-live="polite"
36
+ >
37
+ <span className="text-foreground/70 uppercase">
38
+ {locale} · {dir.toUpperCase()}
39
+ </span>
40
+ <span aria-hidden>|</span>
41
+ <span>Header {STATUS_LABEL[header]}</span>
42
+ <span aria-hidden>|</span>
43
+ <span>Footer {STATUS_LABEL[footer]}</span>
44
+ {typeof navLinkCount === 'number' ? (
45
+ <>
46
+ <span aria-hidden>|</span>
47
+ <span>Nav: {navLinkCount} links</span>
48
+ </>
49
+ ) : null}
50
+ {brandPresent === true ? (
51
+ <>
52
+ <span aria-hidden>|</span>
53
+ <span>Brand</span>
54
+ </>
55
+ ) : null}
56
+ {outlineWarning ? (
57
+ <>
58
+ <span aria-hidden>|</span>
59
+ <span className="text-destructive" data-prenta-theme-chrome-outline-warning="">
60
+ Chrome uses H1 — page outline will be wrong
61
+ </span>
62
+ </>
63
+ ) : null}
64
+ </p>
65
+ )
66
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import type { ThemeChromeMode } from '../../../lib/theme-chrome.js'
4
+
5
+ export interface ThemeChromeModeToggleProps {
6
+ mode: ThemeChromeMode
7
+ onChange: (mode: ThemeChromeMode) => void
8
+ }
9
+
10
+ /**
11
+ * Page = real document scroll (sellable preview). Frame = pinned chrome
12
+ * for authoring. Frame is a working view, not the live page.
13
+ */
14
+ export function ThemeChromeModeToggle({ mode, onChange }: ThemeChromeModeToggleProps) {
15
+ return (
16
+ <div
17
+ role="group"
18
+ aria-label="Canvas layout. Page scrolls like the live site. Frame pins the header and footer."
19
+ className="border-border bg-card inline-flex h-[34px] overflow-hidden rounded-lg border"
20
+ >
21
+ <ModeButton
22
+ label="Page"
23
+ pressed={mode === 'flow'}
24
+ title="Page — header, bands, and footer scroll together like the live site."
25
+ onClick={() => onChange('flow')}
26
+ />
27
+ <ModeButton
28
+ label="Frame"
29
+ pressed={mode === 'frame'}
30
+ title="Frame — header and footer stay pinned while bands scroll. Authoring view, not the live page."
31
+ onClick={() => onChange('frame')}
32
+ />
33
+ </div>
34
+ )
35
+ }
36
+
37
+ function ModeButton({
38
+ label,
39
+ pressed,
40
+ title,
41
+ onClick,
42
+ }: {
43
+ label: string
44
+ pressed: boolean
45
+ title: string
46
+ onClick: () => void
47
+ }) {
48
+ return (
49
+ <button
50
+ type="button"
51
+ aria-pressed={pressed}
52
+ title={title}
53
+ onClick={onClick}
54
+ className={`h-full px-2.5 text-[12.5px] font-medium transition-colors ${
55
+ pressed
56
+ ? 'bg-primary text-primary-foreground'
57
+ : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
58
+ }`}
59
+ >
60
+ {label}
61
+ </button>
62
+ )
63
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react'
4
+ import type { AdminThemeChrome, AdminThemeChromeProps } from '../../../lib/theme-chrome.js'
5
+
6
+ const ThemeChromeSpecContext = createContext<AdminThemeChrome | null>(null)
7
+
8
+ export function ThemeChromeProvider({
9
+ value,
10
+ children,
11
+ }: {
12
+ value: AdminThemeChrome | null
13
+ children: ReactNode
14
+ }) {
15
+ return <ThemeChromeSpecContext.Provider value={value}>{children}</ThemeChromeSpecContext.Provider>
16
+ }
17
+
18
+ /** The `themeChrome` prop passed to `<AdminRoot>`, or null when omitted. */
19
+ export function useThemeChromeSpec(): AdminThemeChrome | null {
20
+ return useContext(ThemeChromeSpecContext)
21
+ }
22
+
23
+ const ThemeChromeRenderContext = createContext<AdminThemeChromeProps | null>(null)
24
+
25
+ export function ThemeChromeRenderProvider({
26
+ value,
27
+ children,
28
+ }: {
29
+ value: AdminThemeChromeProps
30
+ children: ReactNode
31
+ }) {
32
+ return (
33
+ <ThemeChromeRenderContext.Provider value={value}>{children}</ThemeChromeRenderContext.Provider>
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Fields passed to a consumer `SiteHeader` / `SiteFooter` so they can
39
+ * highlight the current path and locale without prop-drilling.
40
+ */
41
+ export function useThemeChromeContext(): AdminThemeChromeProps | null {
42
+ return useContext(ThemeChromeRenderContext)
43
+ }