@prenta/admin 1.9.2 → 1.10.1
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 +12 -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/AddSectionModal.d.ts.map +1 -1
- package/dist/views/page-editor/AddSectionModal.js +2 -2
- package/dist/views/page-editor/AddSectionModal.js.map +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/section-types.d.ts +1 -1
- package/dist/views/page-editor/section-types.d.ts.map +1 -1
- package/dist/views/page-editor/section-types.js +1 -1
- package/dist/views/page-editor/section-types.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/AddSectionModal.tsx +6 -2
- 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/section-types.ts +2 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|