@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.
- package/CHANGELOG.md +6 -0
- package/dist/AdminRoot.d.ts +10 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +2 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/theme-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/theme-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/theme-chrome.test.js +104 -0
- package/dist/__tests__/lib/theme-chrome.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +11 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts +2 -0
- package/dist/__tests__/views/theme-chrome-frame.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/theme-chrome-frame.render.test.js +236 -0
- package/dist/__tests__/views/theme-chrome-frame.render.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/theme-chrome.d.ts +87 -0
- package/dist/lib/theme-chrome.d.ts.map +1 -0
- package/dist/lib/theme-chrome.js +117 -0
- package/dist/lib/theme-chrome.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasSurface.d.ts +14 -2
- package/dist/views/page-editor/CanvasSurface.d.ts.map +1 -1
- package/dist/views/page-editor/CanvasSurface.js +30 -14
- package/dist/views/page-editor/CanvasSurface.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +12 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +22 -5
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +5 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +3 -2
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +30 -2
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts +21 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js +34 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeFrame.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts +12 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js +12 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeHealth.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts +11 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js +15 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeModeToggle.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts +18 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js +23 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeProvider.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts +11 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js +82 -0
- package/dist/views/page-editor/theme-chrome/ThemeChromeSlot.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/index.d.ts +9 -0
- package/dist/views/page-editor/theme-chrome/index.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/index.js +6 -0
- package/dist/views/page-editor/theme-chrome/index.js.map +1 -0
- package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts +3 -0
- package/dist/views/page-editor/theme-chrome/useThemeChromeMode.d.ts.map +1 -0
- package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js +17 -0
- package/dist/views/page-editor/theme-chrome/useThemeChromeMode.js.map +1 -0
- package/package.json +4 -4
- package/src/AdminRoot.tsx +14 -1
- package/src/__tests__/lib/theme-chrome.test.ts +132 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +13 -0
- package/src/__tests__/views/theme-chrome-frame.render.test.tsx +304 -0
- package/src/index.ts +17 -0
- package/src/lib/theme-chrome.ts +172 -0
- package/src/views/page-editor/CanvasSurface.tsx +70 -24
- package/src/views/page-editor/EditorCanvas.tsx +89 -15
- package/src/views/page-editor/EditorTopBar.tsx +10 -0
- package/src/views/page-editor/PageSectionEditor.tsx +42 -0
- package/src/views/page-editor/theme-chrome/ThemeChromeFrame.tsx +119 -0
- package/src/views/page-editor/theme-chrome/ThemeChromeHealth.tsx +66 -0
- package/src/views/page-editor/theme-chrome/ThemeChromeModeToggle.tsx +63 -0
- package/src/views/page-editor/theme-chrome/ThemeChromeProvider.tsx +43 -0
- package/src/views/page-editor/theme-chrome/ThemeChromeSlot.tsx +235 -0
- package/src/views/page-editor/theme-chrome/index.ts +12 -0
- 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
|
-
|
|
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
|
-
{
|
|
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
|
+
}
|