@takazudo/zudo-doc 5.26.4 → 5.27.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 +22 -0
- package/dist/compiled.css +18 -0
- package/dist/extract-headings/index.d.ts +2 -0
- package/dist/extract-headings/index.js +40 -1
- package/dist/header/header.js +47 -19
- package/dist/header-with-defaults/index.js +5 -1
- package/dist/i18n-defaults/index.js +10 -0
- package/dist/plugins/routes.js +12 -2
- package/dist/safelist.css +1 -1
- package/dist/search-widget/index.js +1 -1
- package/dist/sidebar-toggle-island/index.d.ts +4 -1
- package/dist/sidebar-toggle-island/index.js +4 -0
- package/dist/sidebar-tree-island/index.d.ts +4 -1
- package/dist/sidebar-tree-island/index.js +5 -5
- package/dist/sidebar-with-defaults/index.js +3 -0
- package/dist/theme/color-scheme-provider.js +40 -10
- package/dist/theme/theme-toggle.d.ts +1 -1
- package/dist/theme-toggle/color-scheme-sync.d.ts +19 -39
- package/dist/theme-toggle/color-scheme-sync.js +81 -6
- package/dist/theme-toggle/index.d.ts +10 -1
- package/dist/theme-toggle/index.js +217 -58
- package/dist/theme-toggle/labels.d.ts +3 -0
- package/dist/theme-toggle/labels.js +12 -0
- package/eject/header/header.tsx +56 -19
- package/eject/sidebar-toggle-island/index.tsx +7 -0
- package/eject/sidebar-tree-island/index.tsx +8 -6
- package/eject/theme-toggle/color-scheme-sync.ts +108 -63
- package/eject/theme-toggle/index.tsx +204 -89
- package/eject/theme-toggle/labels.ts +15 -0
- package/package.json +8 -8
package/eject/header/header.tsx
CHANGED
|
@@ -294,6 +294,49 @@ export function Header(props: HeaderProps): JSX.Element {
|
|
|
294
294
|
|
|
295
295
|
const activeNavPath = computeActiveNavPath(headerNav, matchPath);
|
|
296
296
|
const rightItemDispatch = createRightItemDispatch(headerRightComponents);
|
|
297
|
+
const rightItems = headerRightItems
|
|
298
|
+
.map((item, i) => ({
|
|
299
|
+
item,
|
|
300
|
+
node: renderRightItem(
|
|
301
|
+
item,
|
|
302
|
+
i,
|
|
303
|
+
{
|
|
304
|
+
lang,
|
|
305
|
+
githubRepoUrl,
|
|
306
|
+
githubLabel,
|
|
307
|
+
themeToggle,
|
|
308
|
+
languageSwitcher,
|
|
309
|
+
versionSwitcher,
|
|
310
|
+
search,
|
|
311
|
+
colorModeEnabled,
|
|
312
|
+
hasLocales,
|
|
313
|
+
},
|
|
314
|
+
rightItemDispatch,
|
|
315
|
+
),
|
|
316
|
+
}))
|
|
317
|
+
.filter((entry): entry is typeof entry & { node: VNode } => entry.node !== null);
|
|
318
|
+
const rightGroups: VNode[] = [];
|
|
319
|
+
let iconGroup: VNode[] = [];
|
|
320
|
+
|
|
321
|
+
const flushIconGroup = () => {
|
|
322
|
+
if (iconGroup.length === 0) return;
|
|
323
|
+
rightGroups.push(
|
|
324
|
+
<div class="flex items-center" data-header-icon-group>
|
|
325
|
+
{iconGroup}
|
|
326
|
+
</div>,
|
|
327
|
+
);
|
|
328
|
+
iconGroup = [];
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
for (const { item, node } of rightItems) {
|
|
332
|
+
if (isHeaderIconItem(item)) {
|
|
333
|
+
iconGroup.push(node);
|
|
334
|
+
} else {
|
|
335
|
+
flushIconGroup();
|
|
336
|
+
rightGroups.push(node);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
flushIconGroup();
|
|
297
340
|
|
|
298
341
|
return (
|
|
299
342
|
<header
|
|
@@ -412,22 +455,7 @@ export function Header(props: HeaderProps): JSX.Element {
|
|
|
412
455
|
class="ml-auto flex shrink-0 items-center gap-x-hsp-md"
|
|
413
456
|
data-header-right
|
|
414
457
|
>
|
|
415
|
-
{
|
|
416
|
-
item,
|
|
417
|
-
i,
|
|
418
|
-
{
|
|
419
|
-
lang,
|
|
420
|
-
githubRepoUrl,
|
|
421
|
-
githubLabel,
|
|
422
|
-
themeToggle,
|
|
423
|
-
languageSwitcher,
|
|
424
|
-
versionSwitcher,
|
|
425
|
-
search,
|
|
426
|
-
colorModeEnabled,
|
|
427
|
-
hasLocales,
|
|
428
|
-
},
|
|
429
|
-
rightItemDispatch,
|
|
430
|
-
))}
|
|
458
|
+
{rightGroups}
|
|
431
459
|
</div>
|
|
432
460
|
|
|
433
461
|
<script dangerouslySetInnerHTML={{ __html: NAV_OVERFLOW_SCRIPT }} />
|
|
@@ -592,6 +620,13 @@ function stripCurrentVersionPrefix(path: string, currentVersion?: string): strin
|
|
|
592
620
|
|
|
593
621
|
type RightItemContext = Omit<HeaderRightComponentProps, "item" | "index">;
|
|
594
622
|
|
|
623
|
+
function isHeaderIconItem(item: HeaderRightItem): boolean {
|
|
624
|
+
if (item.type === "trigger") return true;
|
|
625
|
+
if (item.type === "link") return item.icon === "github";
|
|
626
|
+
if (item.type !== "component") return false;
|
|
627
|
+
return ["github-link", "theme-toggle", "search"].includes(item.component);
|
|
628
|
+
}
|
|
629
|
+
|
|
595
630
|
/**
|
|
596
631
|
* Shared trigger-button shell for header-right items that dispatch a
|
|
597
632
|
* CustomEvent on click. The legacy template used an inline `onclick`
|
|
@@ -620,7 +655,7 @@ function TriggerButton({
|
|
|
620
655
|
key={`right-${index}`}
|
|
621
656
|
id={id}
|
|
622
657
|
type="button"
|
|
623
|
-
class="flex items-center justify-center text-muted transition-colors hover:text-fg"
|
|
658
|
+
class="flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
624
659
|
aria-label={ariaLabel}
|
|
625
660
|
{...inlineOnclick}
|
|
626
661
|
>
|
|
@@ -730,7 +765,7 @@ const BASE_RIGHT_ITEM_DISPATCH: Readonly<Record<string, RightItemHandler>> = {
|
|
|
730
765
|
href={ctx.githubRepoUrl}
|
|
731
766
|
target="_blank"
|
|
732
767
|
rel="noopener noreferrer"
|
|
733
|
-
class="flex items-center justify-center text-muted transition-colors hover:text-fg"
|
|
768
|
+
class="flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
734
769
|
aria-label={ctx.githubLabel}
|
|
735
770
|
title={ctx.githubLabel}
|
|
736
771
|
>
|
|
@@ -782,7 +817,9 @@ const BASE_RIGHT_ITEM_DISPATCH: Readonly<Record<string, RightItemHandler>> = {
|
|
|
782
817
|
href={item.href}
|
|
783
818
|
target={isExternal ? "_blank" : undefined}
|
|
784
819
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
785
|
-
class=
|
|
820
|
+
class={item.icon === "github"
|
|
821
|
+
? "flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
822
|
+
: "flex items-center justify-center text-muted transition-colors hover:text-fg"}
|
|
786
823
|
aria-label={item.ariaLabel}
|
|
787
824
|
title={label}
|
|
788
825
|
>
|
|
@@ -14,6 +14,7 @@ import { useState, useEffect, useRef } from "preact/hooks";
|
|
|
14
14
|
// collapse into duplicate import statements in an ejected copy.
|
|
15
15
|
import { AFTER_NAVIGATE_EVENT, ensureNestedIslandPropsRefresh } from "../transitions/index.js";
|
|
16
16
|
import { SidebarTree } from "../sidebar-tree-island/index.js";
|
|
17
|
+
import type { ThemeToggleLabels } from "../theme-toggle/index.js";
|
|
17
18
|
import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink } from "../sidebar/types.js";
|
|
18
19
|
import type { ResolvedDateFormats } from "../settings.js";
|
|
19
20
|
|
|
@@ -57,6 +58,8 @@ export interface SidebarToggleProps {
|
|
|
57
58
|
locale?: string;
|
|
58
59
|
localeLinks?: SidebarLocaleLink[];
|
|
59
60
|
themeDefaultMode?: "light" | "dark";
|
|
61
|
+
themeLabels?: ThemeToggleLabels;
|
|
62
|
+
themeRespectSystem?: boolean;
|
|
60
63
|
/**
|
|
61
64
|
* Forwarded verbatim to the hosted `<SidebarTree>`. This island is the one
|
|
62
65
|
* nested under the persisted `<header>`, so on a same-locale soft navigation
|
|
@@ -79,6 +82,8 @@ export function SidebarToggle({
|
|
|
79
82
|
locale,
|
|
80
83
|
localeLinks,
|
|
81
84
|
themeDefaultMode,
|
|
85
|
+
themeLabels,
|
|
86
|
+
themeRespectSystem,
|
|
82
87
|
dateFormats,
|
|
83
88
|
}: SidebarToggleProps) {
|
|
84
89
|
// Initial state must match SSR (`open=false`) so the hydration DOM
|
|
@@ -255,6 +260,8 @@ export function SidebarToggle({
|
|
|
255
260
|
locale={locale}
|
|
256
261
|
localeLinks={localeLinks}
|
|
257
262
|
themeDefaultMode={themeDefaultMode}
|
|
263
|
+
themeLabels={themeLabels}
|
|
264
|
+
themeRespectSystem={themeRespectSystem}
|
|
258
265
|
dateFormats={dateFormats}
|
|
259
266
|
/>
|
|
260
267
|
</div>
|
|
@@ -12,7 +12,7 @@ import { INDENT, BASE_PAD, connectorLeft, ConnectorLines, CategoryLinkIcon } fro
|
|
|
12
12
|
import { ChevronRight, ChevronLeft, Search } from "../icons/index.js";
|
|
13
13
|
// BARE ThemeToggle — renders inside the SidebarToggle island, so it must
|
|
14
14
|
// NOT bring its own island wrapper.
|
|
15
|
-
import { ThemeToggle } from "../theme-toggle/index.js";
|
|
15
|
+
import { ThemeToggle, type ThemeToggleLabels } from "../theme-toggle/index.js";
|
|
16
16
|
import { smartBreakToHtml } from "../smart-break/index.js";
|
|
17
17
|
// After zudolab/zudo-doc#1335 the host components also pull lifecycle event
|
|
18
18
|
// names from the v2 transitions module rather than hard-coding literals.
|
|
@@ -184,6 +184,8 @@ export interface SidebarTreeProps {
|
|
|
184
184
|
locale?: string;
|
|
185
185
|
localeLinks?: SidebarLocaleLink[];
|
|
186
186
|
themeDefaultMode?: "light" | "dark";
|
|
187
|
+
themeLabels?: ThemeToggleLabels;
|
|
188
|
+
themeRespectSystem?: boolean;
|
|
187
189
|
/**
|
|
188
190
|
* Per-role date patterns already resolved for this page's locale, serialized
|
|
189
191
|
* into the island's `data-props` by the SSR wrappers (`sidebar-with-defaults`
|
|
@@ -197,13 +199,13 @@ export interface SidebarTreeProps {
|
|
|
197
199
|
dateFormats?: ResolvedDateFormats;
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[]; themeDefaultMode?: "light" | "dark" }) {
|
|
202
|
+
function SidebarFooter({ links, themeDefaultMode, themeLabels, themeRespectSystem }: { links?: SidebarLocaleLink[]; themeDefaultMode?: "light" | "dark"; themeLabels?: ThemeToggleLabels; themeRespectSystem?: boolean }) {
|
|
201
203
|
if (!links && !themeDefaultMode) return null;
|
|
202
204
|
return (
|
|
203
205
|
// pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
|
|
204
206
|
<div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
|
|
205
207
|
{themeDefaultMode && (
|
|
206
|
-
<ThemeToggle defaultMode={themeDefaultMode} pendingUntilHydrated={true} />
|
|
208
|
+
<ThemeToggle defaultMode={themeDefaultMode} labels={themeLabels} respectPrefersColorScheme={themeRespectSystem} pendingUntilHydrated={true} />
|
|
207
209
|
)}
|
|
208
210
|
{links && links.map((link, i) => (
|
|
209
211
|
<span key={link.href} className="flex items-center gap-hsp-xs">
|
|
@@ -221,7 +223,7 @@ function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[
|
|
|
221
223
|
);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, locale: localeProp, localeLinks, themeDefaultMode, dateFormats }: SidebarTreeProps) {
|
|
226
|
+
export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, locale: localeProp, localeLinks, themeDefaultMode, themeLabels, themeRespectSystem, dateFormats }: SidebarTreeProps) {
|
|
225
227
|
const activeSlug = useActiveSlug(nodes, currentSlug, currentPath);
|
|
226
228
|
const [query, setQuery] = useState("");
|
|
227
229
|
const [showingRootMenu, setShowingRootMenu] = useState(false);
|
|
@@ -257,8 +259,8 @@ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, ba
|
|
|
257
259
|
);
|
|
258
260
|
|
|
259
261
|
const footer = useMemo(
|
|
260
|
-
() => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} /> : null,
|
|
261
|
-
[localeLinks, themeDefaultMode],
|
|
262
|
+
() => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} themeLabels={themeLabels} themeRespectSystem={themeRespectSystem} /> : null,
|
|
263
|
+
[localeLinks, themeDefaultMode, themeLabels, themeRespectSystem],
|
|
262
264
|
);
|
|
263
265
|
|
|
264
266
|
const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : undefined;
|
|
@@ -1,81 +1,126 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Two ThemeToggle instances can be mounted at once (header + mobile
|
|
4
|
-
// sidebar footer). Each instance holds its own `mode` state, so a
|
|
5
|
-
// toggle in one used to leave the other's icon stale — the instances
|
|
6
|
-
// only read the DOM at mount. These helpers centralise the write path
|
|
7
|
-
// (`applyColorScheme`) and give every instance a subscription point
|
|
8
|
-
// (`subscribeColorSchemeChanged`) keyed on the `color-scheme-changed`
|
|
9
|
-
// window event, which `applyColorScheme` dispatches after mutating the
|
|
10
|
-
// DOM. The same event is already consumed by the zdtp design-token
|
|
11
|
-
// panel, so the event name is a cross-package contract — do not rename.
|
|
12
|
-
//
|
|
13
|
-
// This module is intentionally NOT marked "use client": zfb's island
|
|
14
|
-
// scanner registers every exported binding of a "use client" file as an
|
|
15
|
-
// island, and these helpers are plain functions, not components. They
|
|
16
|
-
// run in the browser only (called from the ThemeToggle island and
|
|
17
|
-
// unit tests).
|
|
18
|
-
|
|
1
|
+
// Browser color-scheme state shared by the pre-paint bootstrap and client islands.
|
|
2
|
+
// The selected preference is distinct from the effective light/dark appearance.
|
|
19
3
|
export type ColorSchemeMode = "light" | "dark";
|
|
4
|
+
export type ThemePreference = ColorSchemeMode | "system";
|
|
20
5
|
|
|
21
6
|
export const COLOR_SCHEME_CHANGED_EVENT = "color-scheme-changed";
|
|
7
|
+
export const THEME_PREFERENCE_CHANGED_EVENT = "theme-preference-changed";
|
|
8
|
+
export const COLOR_SCHEME_RUNTIME_GLOBAL = "__zudoDocColorScheme";
|
|
9
|
+
export const COLOR_SCHEME_STORAGE_KEY = "zudo-doc-theme";
|
|
22
10
|
|
|
23
|
-
|
|
11
|
+
export interface ColorSchemeRuntime {
|
|
12
|
+
// null means no valid persisted or live explicit choice.
|
|
13
|
+
choice: ThemePreference | null;
|
|
14
|
+
defaultMode: ColorSchemeMode;
|
|
15
|
+
respectPrefersColorScheme: boolean;
|
|
16
|
+
lastMode?: ColorSchemeMode;
|
|
17
|
+
cleanup?: () => void;
|
|
18
|
+
}
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
// Keep these pure functions self-contained: the provider serializes their JS
|
|
21
|
+
// bodies into the inline pre-paint script, so both paths use identical rules.
|
|
22
|
+
export function normalizeThemePreference(value: unknown): ThemePreference | null {
|
|
23
|
+
return value === "light" || value === "dark" || value === "system"
|
|
24
|
+
? value
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveThemePreference(
|
|
29
|
+
choice: ThemePreference | null,
|
|
31
30
|
defaultMode: ColorSchemeMode,
|
|
31
|
+
respectPrefersColorScheme: boolean,
|
|
32
|
+
): ThemePreference {
|
|
33
|
+
return choice ?? (respectPrefersColorScheme ? "system" : defaultMode);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveColorScheme(
|
|
37
|
+
preference: ThemePreference,
|
|
38
|
+
systemIsDark: boolean,
|
|
32
39
|
): ColorSchemeMode {
|
|
40
|
+
return preference === "system" ? (systemIsDark ? "dark" : "light") : preference;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runtime(): ColorSchemeRuntime | null {
|
|
44
|
+
return ((window as unknown as Record<string, unknown>)[COLOR_SCHEME_RUNTIME_GLOBAL] ??
|
|
45
|
+
null) as ColorSchemeRuntime | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readStorage(): ThemePreference | null {
|
|
49
|
+
try {
|
|
50
|
+
return normalizeThemePreference(localStorage.getItem(COLOR_SCHEME_STORAGE_KEY));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getRuntime(): ColorSchemeRuntime {
|
|
57
|
+
const existing = runtime();
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const initial: ColorSchemeRuntime = {
|
|
60
|
+
choice: readStorage(),
|
|
61
|
+
defaultMode: "dark",
|
|
62
|
+
respectPrefersColorScheme: true,
|
|
63
|
+
};
|
|
64
|
+
(window as unknown as Record<string, unknown>)[COLOR_SCHEME_RUNTIME_GLOBAL] = initial;
|
|
65
|
+
return initial;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readThemePreference(
|
|
69
|
+
defaultMode: ColorSchemeMode = "dark",
|
|
70
|
+
respectPrefersColorScheme = true,
|
|
71
|
+
): ThemePreference {
|
|
72
|
+
const state = runtime();
|
|
73
|
+
return resolveThemePreference(
|
|
74
|
+
state ? state.choice : readStorage(),
|
|
75
|
+
state?.defaultMode ?? defaultMode,
|
|
76
|
+
state?.respectPrefersColorScheme ?? respectPrefersColorScheme,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readColorSchemeFromDom(defaultMode: ColorSchemeMode): ColorSchemeMode {
|
|
33
81
|
const actual = document.documentElement.getAttribute("data-theme");
|
|
34
82
|
return actual === "light" || actual === "dark" ? actual : defaultMode;
|
|
35
83
|
}
|
|
36
84
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*/
|
|
85
|
+
/** Select a preference. Storage is best effort; the live choice survives errors. */
|
|
86
|
+
export function applyThemePreference(next: ThemePreference): void {
|
|
87
|
+
const state = getRuntime();
|
|
88
|
+
const previousPreference = resolveThemePreference(
|
|
89
|
+
state.choice,
|
|
90
|
+
state.defaultMode,
|
|
91
|
+
state.respectPrefersColorScheme,
|
|
92
|
+
);
|
|
93
|
+
const previousMode = state.lastMode ?? document.documentElement.getAttribute("data-theme");
|
|
94
|
+
state.choice = next;
|
|
95
|
+
try {
|
|
96
|
+
localStorage.setItem(COLOR_SCHEME_STORAGE_KEY, next);
|
|
97
|
+
} catch {
|
|
98
|
+
// Private browsing or disabled storage must not block theme changes.
|
|
99
|
+
}
|
|
100
|
+
const systemIsDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
|
101
|
+
const mode = resolveColorScheme(next, systemIsDark);
|
|
102
|
+
document.documentElement.setAttribute("data-theme", mode);
|
|
103
|
+
document.documentElement.style.colorScheme = mode;
|
|
104
|
+
state.lastMode = mode;
|
|
105
|
+
if (previousMode !== mode) {
|
|
106
|
+
window.dispatchEvent(new CustomEvent(COLOR_SCHEME_CHANGED_EVENT));
|
|
107
|
+
}
|
|
108
|
+
if (previousPreference !== next) {
|
|
109
|
+
window.dispatchEvent(new CustomEvent(THEME_PREFERENCE_CHANGED_EVENT));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Existing light/dark callers explicitly select the corresponding preference. */
|
|
67
114
|
export function applyColorScheme(next: ColorSchemeMode): void {
|
|
68
|
-
|
|
69
|
-
document.documentElement.style.colorScheme = next;
|
|
70
|
-
localStorage.setItem(STORAGE_KEY, next);
|
|
71
|
-
window.dispatchEvent(new CustomEvent(COLOR_SCHEME_CHANGED_EVENT));
|
|
115
|
+
applyThemePreference(next);
|
|
72
116
|
}
|
|
73
117
|
|
|
74
|
-
/**
|
|
75
|
-
* Subscribe to color-scheme changes. Returns an unsubscribe function
|
|
76
|
-
* (suitable as a `useEffect` cleanup).
|
|
77
|
-
*/
|
|
78
118
|
export function subscribeColorSchemeChanged(listener: () => void): () => void {
|
|
79
119
|
window.addEventListener(COLOR_SCHEME_CHANGED_EVENT, listener);
|
|
80
120
|
return () => window.removeEventListener(COLOR_SCHEME_CHANGED_EVENT, listener);
|
|
81
121
|
}
|
|
122
|
+
|
|
123
|
+
export function subscribeThemePreferenceChanged(listener: () => void): () => void {
|
|
124
|
+
window.addEventListener(THEME_PREFERENCE_CHANGED_EVENT, listener);
|
|
125
|
+
return () => window.removeEventListener(THEME_PREFERENCE_CHANGED_EVENT, listener);
|
|
126
|
+
}
|