@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
@@ -0,0 +1,235 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useLayoutEffect, useRef, useState, type MouseEvent } from 'react'
4
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
5
+ import { Pencil } from 'lucide-react'
6
+ import { ErrorBoundary } from '../../../components/ErrorBoundary.js'
7
+ import { adminPortalContainer } from '../../../lib/portal-container.js'
8
+ import {
9
+ closestAnchor,
10
+ isSameOriginPreviewSrc,
11
+ type AdminThemeChrome,
12
+ type AdminThemeChromeProps,
13
+ type ThemeChromeSlotStatus,
14
+ } from '../../../lib/theme-chrome.js'
15
+ import { ThemeChromeRenderProvider } from './ThemeChromeProvider.js'
16
+
17
+ interface ThemeChromeSlotProps {
18
+ region: 'header' | 'footer'
19
+ spec: AdminThemeChrome
20
+ chromeProps: AdminThemeChromeProps
21
+ onOutlineIssue: (hasH1: boolean) => void
22
+ onStatus: (status: ThemeChromeSlotStatus) => void
23
+ }
24
+
25
+ export function ThemeChromeSlot({
26
+ region,
27
+ spec,
28
+ chromeProps,
29
+ onOutlineIssue,
30
+ onStatus,
31
+ }: ThemeChromeSlotProps) {
32
+ const rootRef = useRef<HTMLDivElement>(null)
33
+ const [menuOpen, setMenuOpen] = useState(false)
34
+
35
+ useLayoutEffect(() => {
36
+ onOutlineIssue(Boolean(rootRef.current?.querySelector('h1')))
37
+ })
38
+
39
+ const openEditMenu = useCallback(() => {
40
+ setMenuOpen(true)
41
+ }, [])
42
+
43
+ const onClickCapture = useCallback(
44
+ (event: MouseEvent) => {
45
+ const target = event.target
46
+ if (target instanceof Element && target.closest('[data-prenta-theme-chrome-edit]')) {
47
+ return
48
+ }
49
+ if (closestAnchor(target)) {
50
+ event.preventDefault()
51
+ event.stopPropagation()
52
+ }
53
+ openEditMenu()
54
+ },
55
+ [openEditMenu],
56
+ )
57
+
58
+ const onAuxClickCapture = useCallback((event: MouseEvent) => {
59
+ if (closestAnchor(event.target)) {
60
+ event.preventDefault()
61
+ event.stopPropagation()
62
+ }
63
+ }, [])
64
+
65
+ const label = region === 'header' ? 'Site header' : 'Site footer'
66
+ const renderer = region === 'header' ? spec.header : spec.footer
67
+ const previewSrc = region === 'header' ? spec.previewSrc?.header : spec.previewSrc?.footer
68
+
69
+ return (
70
+ <ThemeChromeRenderProvider value={chromeProps}>
71
+ <div
72
+ ref={rootRef}
73
+ data-prenta-theme-chrome={region}
74
+ aria-label={label}
75
+ className="relative"
76
+ onClickCapture={onClickCapture}
77
+ onAuxClickCapture={onAuxClickCapture}
78
+ >
79
+ <ErrorBoundary
80
+ fallback={
81
+ <ChromeFallback
82
+ region={region}
83
+ spec={spec}
84
+ chromeProps={chromeProps}
85
+ previewSrc={previewSrc}
86
+ onStatus={onStatus}
87
+ />
88
+ }
89
+ >
90
+ <ChromeBody
91
+ region={region}
92
+ renderer={renderer}
93
+ fallback={spec.fallback}
94
+ previewSrc={previewSrc}
95
+ chromeProps={chromeProps}
96
+ onStatus={onStatus}
97
+ />
98
+ </ErrorBoundary>
99
+
100
+ <DropdownMenu.Root open={menuOpen} onOpenChange={setMenuOpen}>
101
+ <DropdownMenu.Trigger asChild>
102
+ <button
103
+ type="button"
104
+ data-prenta-theme-chrome-edit=""
105
+ aria-label={`Edit ${label.toLowerCase()}`}
106
+ title={`Edit ${label.toLowerCase()}`}
107
+ className="border-border bg-card text-foreground hover:bg-accent focus-visible:outline-ring absolute top-2 right-2 z-20 inline-flex h-9 w-9 items-center justify-center rounded-md border shadow-sm"
108
+ >
109
+ <Pencil size={16} aria-hidden />
110
+ </button>
111
+ </DropdownMenu.Trigger>
112
+ <ChromeEditMenu onEditChrome={chromeProps.onEditChrome} />
113
+ </DropdownMenu.Root>
114
+ </div>
115
+ </ThemeChromeRenderProvider>
116
+ )
117
+ }
118
+
119
+ function ChromeEditMenu({ onEditChrome }: { onEditChrome: AdminThemeChromeProps['onEditChrome'] }) {
120
+ const menu = (
121
+ <DropdownMenu.Content
122
+ align="end"
123
+ sideOffset={6}
124
+ className="border-border bg-popover z-[120] min-w-[12rem] rounded-[10px] border p-[5px] shadow-[0_12px_34px_rgba(26,24,39,0.17)]"
125
+ >
126
+ <ChromeEditItem onSelect={() => onEditChrome('navigations')} label="Edit navigation" />
127
+ <ChromeEditItem onSelect={() => onEditChrome('appearance')} label="Edit appearance" />
128
+ <ChromeEditItem onSelect={() => onEditChrome('settings')} label="Edit site settings" />
129
+ </DropdownMenu.Content>
130
+ )
131
+ const portalRoot = adminPortalContainer()
132
+ return portalRoot ? (
133
+ <DropdownMenu.Portal container={portalRoot}>{menu}</DropdownMenu.Portal>
134
+ ) : (
135
+ menu
136
+ )
137
+ }
138
+
139
+ function ChromeEditItem({ onSelect, label }: { onSelect: () => void; label: string }) {
140
+ return (
141
+ <DropdownMenu.Item
142
+ onSelect={onSelect}
143
+ className="hover:bg-accent focus:bg-accent cursor-pointer rounded-md px-2.5 py-1.5 text-sm outline-none"
144
+ >
145
+ {label}
146
+ </DropdownMenu.Item>
147
+ )
148
+ }
149
+
150
+ function ChromeBody({
151
+ region,
152
+ renderer,
153
+ fallback,
154
+ previewSrc,
155
+ chromeProps,
156
+ onStatus,
157
+ }: {
158
+ region: 'header' | 'footer'
159
+ renderer?: AdminThemeChrome['header']
160
+ fallback?: AdminThemeChrome['fallback']
161
+ previewSrc?: string
162
+ chromeProps: AdminThemeChromeProps
163
+ onStatus: (status: ThemeChromeSlotStatus) => void
164
+ }) {
165
+ useLayoutEffect(() => {
166
+ if (renderer) onStatus('ok')
167
+ }, [renderer, onStatus])
168
+
169
+ if (renderer) {
170
+ return <>{renderer(chromeProps)}</>
171
+ }
172
+ return (
173
+ <ChromeFallback
174
+ region={region}
175
+ spec={{ fallback, previewSrc: previewSrc ? { [region]: previewSrc } : undefined }}
176
+ chromeProps={chromeProps}
177
+ previewSrc={previewSrc}
178
+ onStatus={onStatus}
179
+ />
180
+ )
181
+ }
182
+
183
+ function ChromeFallback({
184
+ region,
185
+ spec,
186
+ chromeProps,
187
+ previewSrc,
188
+ onStatus,
189
+ }: {
190
+ region: 'header' | 'footer'
191
+ spec: Pick<AdminThemeChrome, 'fallback' | 'previewSrc'>
192
+ chromeProps: AdminThemeChromeProps
193
+ previewSrc?: string
194
+ onStatus: (status: ThemeChromeSlotStatus) => void
195
+ }) {
196
+ const usePreview = Boolean(previewSrc && isSameOriginPreviewSrc(previewSrc))
197
+ const useFallback = Boolean(!usePreview && spec.fallback)
198
+
199
+ useLayoutEffect(() => {
200
+ if (usePreview) onStatus('preview')
201
+ else if (useFallback) onStatus('fallback')
202
+ else onStatus('placeholder')
203
+ }, [usePreview, useFallback, onStatus])
204
+
205
+ if (usePreview && previewSrc) {
206
+ return (
207
+ <iframe
208
+ src={previewSrc}
209
+ title={region === 'header' ? 'Site header preview' : 'Site footer preview'}
210
+ sandbox="allow-same-origin"
211
+ className="block w-full border-0"
212
+ />
213
+ )
214
+ }
215
+ if (useFallback && spec.fallback) {
216
+ return <>{spec.fallback(chromeProps)}</>
217
+ }
218
+ return <ChromePlaceholder region={region} />
219
+ }
220
+
221
+ function ChromePlaceholder({ region }: { region: 'header' | 'footer' }) {
222
+ const label = region === 'header' ? 'Site header' : 'Site footer'
223
+ return (
224
+ <div
225
+ data-prenta-theme-chrome-placeholder={region}
226
+ className="border-border bg-muted text-muted-foreground flex flex-col items-start gap-1 border-b px-6 py-4 text-left text-sm"
227
+ >
228
+ <p className="text-foreground font-medium">{label}</p>
229
+ <p>
230
+ This site has not supplied a client-safe {region} for the canvas. Use{' '}
231
+ <span className="text-foreground">View site</span> to see the live page.
232
+ </p>
233
+ </div>
234
+ )
235
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ ThemeChromeProvider,
3
+ useThemeChromeSpec,
4
+ useThemeChromeContext,
5
+ } from './ThemeChromeProvider.js'
6
+ export { ThemeChromeFrame } from './ThemeChromeFrame.js'
7
+ export type { ThemeChromeFrameProps, ThemeChromeHealthState } from './ThemeChromeFrame.js'
8
+ export { ThemeChromeHealth } from './ThemeChromeHealth.js'
9
+ export type { ThemeChromeHealthProps } from './ThemeChromeHealth.js'
10
+ export { ThemeChromeModeToggle } from './ThemeChromeModeToggle.js'
11
+ export type { ThemeChromeModeToggleProps } from './ThemeChromeModeToggle.js'
12
+ export { useThemeChromeMode } from './useThemeChromeMode.js'
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import {
5
+ readStoredChromeMode,
6
+ writeStoredChromeMode,
7
+ type ThemeChromeMode,
8
+ } from '../../../lib/theme-chrome.js'
9
+
10
+ export function useThemeChromeMode(defaultMode: ThemeChromeMode = 'flow') {
11
+ const [mode, setModeState] = useState<ThemeChromeMode>(defaultMode)
12
+
13
+ useEffect(() => {
14
+ const stored = readStoredChromeMode()
15
+ if (stored) setModeState(stored)
16
+ }, [])
17
+
18
+ const setMode = useCallback((next: ThemeChromeMode) => {
19
+ setModeState(next)
20
+ writeStoredChromeMode(next)
21
+ }, [])
22
+
23
+ return [mode, setMode] as const
24
+ }