starlight-theme-bejamas 0.0.0-canary.0138fd3
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 +132 -0
- package/package.json +50 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +51 -0
- package/src/env.d.ts +16 -0
- package/src/global.d.ts +20 -0
- package/src/index.ts +152 -0
- package/src/lib/theme-choice.test.ts +251 -0
- package/src/lib/theme-choice.ts +346 -0
- package/src/lib/theme-stylesheet-runtime.test.ts +246 -0
- package/src/lib/theme-stylesheet.test.ts +16 -0
- package/src/lib/theme-stylesheet.ts +123 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vite.ts +62 -0
- package/src/overrides/Footer.astro +143 -0
- package/src/overrides/Header.astro +166 -0
- package/src/overrides/Hero.astro +175 -0
- package/src/overrides/MobileTableOfContents.astro +275 -0
- package/src/overrides/PageFrame.astro +287 -0
- package/src/overrides/PageTitle.astro +142 -0
- package/src/overrides/Pagination.astro +57 -0
- package/src/overrides/Sidebar.astro +54 -0
- package/src/overrides/SiteTitle.astro +59 -0
- package/src/overrides/TableOfContents/TableOfContentsList.astro +88 -0
- package/src/overrides/TableOfContents/starlight-toc.ts +125 -0
- package/src/overrides/ThemeProvider.astro +198 -0
- package/src/overrides/ThemeSelect.astro +110 -0
- package/src/styles/theme.css +776 -0
- package/src/virtual-internal.d.ts +40 -0
- package/src/virtual-modules.d.ts +28 -0
- package/src/virtual.d.ts +12 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
STARLIGHT_THEME_STORAGE_KEY,
|
|
4
|
+
THEME_TOGGLE_CHANGED_EVENT,
|
|
5
|
+
applyThemeModeToRoot,
|
|
6
|
+
buildThemeChoiceBootstrapInlineScript,
|
|
7
|
+
getThemeChoiceFromRoot,
|
|
8
|
+
getThemeModeFromRoot,
|
|
9
|
+
loadStoredThemeChoice,
|
|
10
|
+
parseStoredThemeChoice,
|
|
11
|
+
resolveThemeMode,
|
|
12
|
+
setThemeChoice,
|
|
13
|
+
storeThemeChoice,
|
|
14
|
+
syncThemeChoiceControls,
|
|
15
|
+
} from "./theme-choice";
|
|
16
|
+
|
|
17
|
+
function createThemeRoot() {
|
|
18
|
+
const classes = new Set<string>();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
classes,
|
|
22
|
+
root: {
|
|
23
|
+
dataset: {} as Record<string, string | undefined>,
|
|
24
|
+
classList: {
|
|
25
|
+
add: (...tokens: string[]) => {
|
|
26
|
+
for (const token of tokens) {
|
|
27
|
+
classes.add(token);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
remove: (...tokens: string[]) => {
|
|
31
|
+
for (const token of tokens) {
|
|
32
|
+
classes.delete(token);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
contains: (token: string) => classes.has(token),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("theme choice runtime", () => {
|
|
42
|
+
it("parses only supported stored theme choices", () => {
|
|
43
|
+
expect(parseStoredThemeChoice("light")).toBe("light");
|
|
44
|
+
expect(parseStoredThemeChoice("dark")).toBe("dark");
|
|
45
|
+
expect(parseStoredThemeChoice("auto")).toBe("auto");
|
|
46
|
+
expect(parseStoredThemeChoice("")).toBe("auto");
|
|
47
|
+
expect(parseStoredThemeChoice("system")).toBe("auto");
|
|
48
|
+
expect(parseStoredThemeChoice(undefined)).toBe("auto");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("loads the stored theme choice from localStorage-compatible storage", () => {
|
|
52
|
+
expect(
|
|
53
|
+
loadStoredThemeChoice({
|
|
54
|
+
getItem: (key) =>
|
|
55
|
+
key === STARLIGHT_THEME_STORAGE_KEY ? "dark" : null,
|
|
56
|
+
}),
|
|
57
|
+
).toBe("dark");
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
loadStoredThemeChoice({
|
|
61
|
+
getItem: () => "unexpected",
|
|
62
|
+
}),
|
|
63
|
+
).toBe("auto");
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
loadStoredThemeChoice({
|
|
67
|
+
getItem: () => {
|
|
68
|
+
throw new Error("storage unavailable");
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
).toBe("auto");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("resolves auto against the preferred color scheme", () => {
|
|
75
|
+
expect(resolveThemeMode("light", false)).toBe("light");
|
|
76
|
+
expect(resolveThemeMode("dark", true)).toBe("dark");
|
|
77
|
+
expect(resolveThemeMode("auto", true)).toBe("light");
|
|
78
|
+
expect(resolveThemeMode("auto", false)).toBe("dark");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("applies theme attributes and class state to the target root", () => {
|
|
82
|
+
const { root, classes } = createThemeRoot();
|
|
83
|
+
|
|
84
|
+
classes.add("dark");
|
|
85
|
+
applyThemeModeToRoot(root, "auto", "light");
|
|
86
|
+
|
|
87
|
+
expect(root.dataset.theme).toBe("light");
|
|
88
|
+
expect(root.dataset.themeChoice).toBe("auto");
|
|
89
|
+
expect(classes.has("light")).toBe(true);
|
|
90
|
+
expect(classes.has("dark")).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stores explicit theme choices and clears auto to the empty localStorage value", () => {
|
|
94
|
+
const stored = new Map<string, string>();
|
|
95
|
+
const storage = {
|
|
96
|
+
setItem: (key: string, value: string) => {
|
|
97
|
+
stored.set(key, value);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
storeThemeChoice(storage, "dark");
|
|
102
|
+
storeThemeChoice(storage, "auto");
|
|
103
|
+
|
|
104
|
+
expect(stored.get(STARLIGHT_THEME_STORAGE_KEY)).toBe("");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reads theme choice and effective mode from the document root", () => {
|
|
108
|
+
const { root, classes } = createThemeRoot();
|
|
109
|
+
|
|
110
|
+
root.dataset.themeChoice = "auto";
|
|
111
|
+
root.dataset.theme = "";
|
|
112
|
+
classes.add("dark");
|
|
113
|
+
|
|
114
|
+
expect(getThemeChoiceFromRoot(root)).toBe("auto");
|
|
115
|
+
expect(getThemeModeFromRoot(root, "light")).toBe("dark");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("syncs tabs-based and legacy select-based controls to the requested choice", () => {
|
|
119
|
+
let tabsValue = "";
|
|
120
|
+
let selectValue = "";
|
|
121
|
+
const nativeSelect = { value: "" };
|
|
122
|
+
const tabsRoot = {
|
|
123
|
+
dataset: {} as Record<string, string | undefined>,
|
|
124
|
+
dispatchEvent: (event: Event) => {
|
|
125
|
+
tabsValue =
|
|
126
|
+
(event as CustomEvent<{ value?: string }>).detail?.value ?? "";
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const selectRoot = {
|
|
131
|
+
dataset: {} as Record<string, string | undefined>,
|
|
132
|
+
dispatchEvent: (event: Event) => {
|
|
133
|
+
selectValue =
|
|
134
|
+
(event as CustomEvent<{ value?: string }>).detail?.value ?? "";
|
|
135
|
+
return true;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const picker = {
|
|
139
|
+
querySelector: (selector: string) => {
|
|
140
|
+
if (selector === '[data-slot="tabs"][data-theme-choice-tabs]') {
|
|
141
|
+
return tabsRoot;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (selector === "select") {
|
|
145
|
+
return nativeSelect;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (selector === '#starlight-theme-select, [data-slot="select"]') {
|
|
149
|
+
return selectRoot;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
querySelectorAll: () => [] as unknown as NodeListOf<Element>,
|
|
155
|
+
};
|
|
156
|
+
const root = {
|
|
157
|
+
querySelectorAll: (selector: string) => {
|
|
158
|
+
if (selector === "starlight-theme-select") {
|
|
159
|
+
return [picker] as unknown as NodeListOf<Element>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (selector === '[data-slot="tabs"][data-theme-choice-tabs]') {
|
|
163
|
+
return [tabsRoot] as unknown as NodeListOf<Element>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [] as unknown as NodeListOf<Element>;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
syncThemeChoiceControls("light", root as unknown as ParentNode);
|
|
171
|
+
|
|
172
|
+
expect(tabsRoot.dataset.defaultValue).toBe("light");
|
|
173
|
+
expect(tabsRoot.dataset.value).toBe("light");
|
|
174
|
+
expect(tabsValue).toBe("light");
|
|
175
|
+
expect(nativeSelect.value).toBe("light");
|
|
176
|
+
expect(selectRoot.dataset.defaultValue).toBe("light");
|
|
177
|
+
expect(selectRoot.dataset.value).toBe("light");
|
|
178
|
+
expect(selectValue).toBe("light");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("applies the shared global theme choice path including storage, controls, and event dispatch", () => {
|
|
182
|
+
const { root, classes } = createThemeRoot();
|
|
183
|
+
const stored = new Map<string, string>();
|
|
184
|
+
const dispatched: Array<{ theme: string; themeChoice: string }> = [];
|
|
185
|
+
let tabsValue = "";
|
|
186
|
+
const tabsRoot = {
|
|
187
|
+
dataset: {} as Record<string, string | undefined>,
|
|
188
|
+
dispatchEvent: (event: Event) => {
|
|
189
|
+
tabsValue =
|
|
190
|
+
(event as CustomEvent<{ value?: string }>).detail?.value ?? "";
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
const controlsRoot = {
|
|
195
|
+
querySelectorAll: (selector: string) => {
|
|
196
|
+
if (selector === '[data-slot="tabs"][data-theme-choice-tabs]') {
|
|
197
|
+
return [tabsRoot] as unknown as NodeListOf<Element>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [] as unknown as NodeListOf<Element>;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
const dispatchTarget = {
|
|
204
|
+
dispatchEvent: (event: Event) => {
|
|
205
|
+
const customEvent = event as CustomEvent<{
|
|
206
|
+
theme?: string;
|
|
207
|
+
themeChoice?: string;
|
|
208
|
+
}>;
|
|
209
|
+
|
|
210
|
+
if (event.type === THEME_TOGGLE_CHANGED_EVENT) {
|
|
211
|
+
dispatched.push({
|
|
212
|
+
theme: customEvent.detail?.theme ?? "",
|
|
213
|
+
themeChoice: customEvent.detail?.themeChoice ?? "",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const effectiveTheme = setThemeChoice("dark", {
|
|
222
|
+
root,
|
|
223
|
+
prefersLight: true,
|
|
224
|
+
storage: {
|
|
225
|
+
setItem: (key, value) => {
|
|
226
|
+
stored.set(key, value);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
controlsRoot: controlsRoot as unknown as ParentNode,
|
|
230
|
+
dispatchTarget: dispatchTarget as unknown as EventTarget,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(effectiveTheme).toBe("dark");
|
|
234
|
+
expect(root.dataset.themeChoice).toBe("dark");
|
|
235
|
+
expect(root.dataset.theme).toBe("dark");
|
|
236
|
+
expect(classes.has("dark")).toBe(true);
|
|
237
|
+
expect(stored.get(STARLIGHT_THEME_STORAGE_KEY)).toBe("dark");
|
|
238
|
+
expect(tabsValue).toBe("dark");
|
|
239
|
+
expect(dispatched).toEqual([{ theme: "dark", themeChoice: "dark" }]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("builds an inline bootstrap script that owns root state and control sync", () => {
|
|
243
|
+
const script = buildThemeChoiceBootstrapInlineScript();
|
|
244
|
+
|
|
245
|
+
expect(script).toContain(STARLIGHT_THEME_STORAGE_KEY);
|
|
246
|
+
expect(script).toContain("dataset.themeChoice");
|
|
247
|
+
expect(script).toContain('[data-slot="tabs"][data-theme-choice-tabs]');
|
|
248
|
+
expect(script).toContain(THEME_TOGGLE_CHANGED_EVENT);
|
|
249
|
+
expect(script).toContain("prefers-color-scheme: light");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
export const STARLIGHT_THEME_STORAGE_KEY = "starlight-theme";
|
|
2
|
+
export const THEME_TOGGLE_CHANGED_EVENT = "theme-toggle-changed";
|
|
3
|
+
|
|
4
|
+
export type ThemeChoice = "auto" | "dark" | "light";
|
|
5
|
+
export type ThemeMode = "dark" | "light";
|
|
6
|
+
|
|
7
|
+
export interface ThemeRoot {
|
|
8
|
+
dataset: Record<string, string | undefined>;
|
|
9
|
+
classList: {
|
|
10
|
+
add: (...tokens: string[]) => void;
|
|
11
|
+
remove: (...tokens: string[]) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ThemeClassList = ThemeRoot["classList"] & {
|
|
16
|
+
contains?: (token: string) => boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ThemeRootElement = ThemeRoot & {
|
|
20
|
+
classList: ThemeClassList;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ThemeStorageReader = Pick<Storage, "getItem"> | null | undefined;
|
|
24
|
+
type ThemeStorageWriter = Pick<Storage, "setItem"> | null | undefined;
|
|
25
|
+
|
|
26
|
+
const THEME_CHOICE_TAB_SELECTOR = '[data-slot="tabs"][data-theme-choice-tabs]';
|
|
27
|
+
|
|
28
|
+
function getElementsWithSelf<T extends Element>(
|
|
29
|
+
root: ParentNode,
|
|
30
|
+
selector: string,
|
|
31
|
+
): T[] {
|
|
32
|
+
const matches: T[] = [];
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
typeof Element !== "undefined" &&
|
|
36
|
+
root instanceof Element &&
|
|
37
|
+
root.matches(selector)
|
|
38
|
+
) {
|
|
39
|
+
matches.push(root as T);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
matches.push(...Array.from(root.querySelectorAll<T>(selector)));
|
|
43
|
+
|
|
44
|
+
return matches;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseStoredThemeChoice(value: unknown): ThemeChoice {
|
|
48
|
+
return value === "auto" || value === "dark" || value === "light"
|
|
49
|
+
? value
|
|
50
|
+
: "auto";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadStoredThemeChoice(storage: ThemeStorageReader): ThemeChoice {
|
|
54
|
+
try {
|
|
55
|
+
return parseStoredThemeChoice(
|
|
56
|
+
storage?.getItem(STARLIGHT_THEME_STORAGE_KEY),
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
return "auto";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveThemeMode(
|
|
64
|
+
themeChoice: ThemeChoice,
|
|
65
|
+
prefersLight: boolean,
|
|
66
|
+
): ThemeMode {
|
|
67
|
+
if (themeChoice === "light" || themeChoice === "dark") {
|
|
68
|
+
return themeChoice;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return prefersLight ? "light" : "dark";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function applyThemeModeToRoot(
|
|
75
|
+
root: ThemeRoot,
|
|
76
|
+
themeChoice: ThemeChoice,
|
|
77
|
+
themeMode: ThemeMode,
|
|
78
|
+
): void {
|
|
79
|
+
root.dataset.theme = themeMode;
|
|
80
|
+
root.dataset.themeChoice = themeChoice;
|
|
81
|
+
root.classList.remove("light", "dark");
|
|
82
|
+
root.classList.add(themeMode);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function storeThemeChoice(
|
|
86
|
+
storage: ThemeStorageWriter,
|
|
87
|
+
themeChoice: ThemeChoice,
|
|
88
|
+
): void {
|
|
89
|
+
try {
|
|
90
|
+
storage?.setItem(
|
|
91
|
+
STARLIGHT_THEME_STORAGE_KEY,
|
|
92
|
+
themeChoice === "light" || themeChoice === "dark" ? themeChoice : "",
|
|
93
|
+
);
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getThemeChoiceFromRoot(
|
|
98
|
+
root: Pick<ThemeRoot, "dataset">,
|
|
99
|
+
): ThemeChoice {
|
|
100
|
+
return parseStoredThemeChoice(root.dataset.themeChoice);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getThemeModeFromRoot(
|
|
104
|
+
root: ThemeRootElement,
|
|
105
|
+
fallbackMode: ThemeMode,
|
|
106
|
+
): ThemeMode {
|
|
107
|
+
if (root.dataset.theme === "light" || root.dataset.theme === "dark") {
|
|
108
|
+
return root.dataset.theme;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (root.classList.contains?.("dark")) {
|
|
112
|
+
return "dark";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (root.classList.contains?.("light")) {
|
|
116
|
+
return "light";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return fallbackMode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function syncThemeChoiceTabControls(
|
|
123
|
+
themeChoice: ThemeChoice,
|
|
124
|
+
root: ParentNode,
|
|
125
|
+
): void {
|
|
126
|
+
const tabsRoots = new Set<HTMLElement>();
|
|
127
|
+
|
|
128
|
+
for (const tabsRoot of getElementsWithSelf<HTMLElement>(
|
|
129
|
+
root,
|
|
130
|
+
THEME_CHOICE_TAB_SELECTOR,
|
|
131
|
+
)) {
|
|
132
|
+
tabsRoots.add(tabsRoot);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const picker of getElementsWithSelf<HTMLElement>(
|
|
136
|
+
root,
|
|
137
|
+
"starlight-theme-select",
|
|
138
|
+
)) {
|
|
139
|
+
const tabsRoot = picker.querySelector<HTMLElement>(THEME_CHOICE_TAB_SELECTOR);
|
|
140
|
+
if (tabsRoot) {
|
|
141
|
+
tabsRoots.add(tabsRoot);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const tabsRoot of tabsRoots) {
|
|
146
|
+
tabsRoot.dataset.defaultValue = themeChoice;
|
|
147
|
+
tabsRoot.dataset.value = themeChoice;
|
|
148
|
+
tabsRoot.dispatchEvent(
|
|
149
|
+
new CustomEvent("tabs:set", { detail: { value: themeChoice } }),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function syncLegacyThemeChoiceSelectControls(
|
|
155
|
+
themeChoice: ThemeChoice,
|
|
156
|
+
root: ParentNode,
|
|
157
|
+
): void {
|
|
158
|
+
for (const picker of getElementsWithSelf<HTMLElement>(
|
|
159
|
+
root,
|
|
160
|
+
"starlight-theme-select",
|
|
161
|
+
)) {
|
|
162
|
+
const nativeSelect = picker.querySelector<HTMLSelectElement>("select");
|
|
163
|
+
if (nativeSelect) {
|
|
164
|
+
nativeSelect.value = themeChoice;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const selectRoot = picker.querySelector<HTMLElement>(
|
|
168
|
+
'#starlight-theme-select, [data-slot="select"]',
|
|
169
|
+
);
|
|
170
|
+
if (!selectRoot) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
selectRoot.dataset.defaultValue = themeChoice;
|
|
175
|
+
selectRoot.dataset.value = themeChoice;
|
|
176
|
+
selectRoot.dispatchEvent(
|
|
177
|
+
new CustomEvent("select:set", { detail: { value: themeChoice } }),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function syncThemeChoiceControls(
|
|
183
|
+
themeChoice: ThemeChoice,
|
|
184
|
+
root: ParentNode = document,
|
|
185
|
+
): void {
|
|
186
|
+
syncThemeChoiceTabControls(themeChoice, root);
|
|
187
|
+
syncLegacyThemeChoiceSelectControls(themeChoice, root);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function dispatchThemeToggleChange(
|
|
191
|
+
target: EventTarget,
|
|
192
|
+
themeMode: ThemeMode,
|
|
193
|
+
themeChoice: ThemeChoice,
|
|
194
|
+
): void {
|
|
195
|
+
target.dispatchEvent(
|
|
196
|
+
new CustomEvent(THEME_TOGGLE_CHANGED_EVENT, {
|
|
197
|
+
detail: {
|
|
198
|
+
theme: themeMode,
|
|
199
|
+
themeChoice,
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function setThemeChoice(
|
|
206
|
+
themeChoice: ThemeChoice,
|
|
207
|
+
options: {
|
|
208
|
+
storage?: ThemeStorageWriter;
|
|
209
|
+
root?: ThemeRootElement;
|
|
210
|
+
prefersLight?: boolean;
|
|
211
|
+
controlsRoot?: ParentNode;
|
|
212
|
+
dispatchTarget?: EventTarget;
|
|
213
|
+
emitEvent?: boolean;
|
|
214
|
+
} = {},
|
|
215
|
+
): ThemeMode {
|
|
216
|
+
const root = options.root ?? (document.documentElement as HTMLElement);
|
|
217
|
+
const effectiveTheme = resolveThemeMode(
|
|
218
|
+
themeChoice,
|
|
219
|
+
options.prefersLight ??
|
|
220
|
+
window.matchMedia("(prefers-color-scheme: light)").matches,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
applyThemeModeToRoot(root, themeChoice, effectiveTheme);
|
|
224
|
+
storeThemeChoice(options.storage ?? localStorage, themeChoice);
|
|
225
|
+
syncThemeChoiceControls(themeChoice, options.controlsRoot ?? document);
|
|
226
|
+
|
|
227
|
+
if (options.emitEvent !== false) {
|
|
228
|
+
dispatchThemeToggleChange(
|
|
229
|
+
options.dispatchTarget ?? window,
|
|
230
|
+
effectiveTheme,
|
|
231
|
+
themeChoice,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return effectiveTheme;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function buildThemeChoiceBootstrapInlineScript(): string {
|
|
239
|
+
return `(function () {
|
|
240
|
+
const storageKey = ${JSON.stringify(STARLIGHT_THEME_STORAGE_KEY)};
|
|
241
|
+
const eventName = ${JSON.stringify(THEME_TOGGLE_CHANGED_EVENT)};
|
|
242
|
+
const parseStoredThemeChoice = (value) =>
|
|
243
|
+
value === "auto" || value === "dark" || value === "light" ? value : "auto";
|
|
244
|
+
const loadStoredThemeChoice = () => {
|
|
245
|
+
try {
|
|
246
|
+
return parseStoredThemeChoice(
|
|
247
|
+
typeof localStorage !== "undefined" ? localStorage.getItem(storageKey) : null,
|
|
248
|
+
);
|
|
249
|
+
} catch {
|
|
250
|
+
return "auto";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const resolveThemeMode = (themeChoice, prefersLight) => {
|
|
254
|
+
if (themeChoice === "light" || themeChoice === "dark") {
|
|
255
|
+
return themeChoice;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return prefersLight ? "light" : "dark";
|
|
259
|
+
};
|
|
260
|
+
const applyThemeModeToRoot = (themeChoice) => {
|
|
261
|
+
const themeMode = resolveThemeMode(
|
|
262
|
+
themeChoice,
|
|
263
|
+
matchMedia("(prefers-color-scheme: light)").matches,
|
|
264
|
+
);
|
|
265
|
+
const root = document.documentElement;
|
|
266
|
+
|
|
267
|
+
root.dataset.theme = themeMode;
|
|
268
|
+
root.dataset.themeChoice = themeChoice;
|
|
269
|
+
root.classList.remove("light", "dark");
|
|
270
|
+
root.classList.add(themeMode);
|
|
271
|
+
|
|
272
|
+
return themeMode;
|
|
273
|
+
};
|
|
274
|
+
const syncThemeChoiceControls = (themeChoice) => {
|
|
275
|
+
document
|
|
276
|
+
.querySelectorAll('[data-slot="tabs"][data-theme-choice-tabs]')
|
|
277
|
+
.forEach((tabsRoot) => {
|
|
278
|
+
tabsRoot.dataset.defaultValue = themeChoice;
|
|
279
|
+
tabsRoot.dataset.value = themeChoice;
|
|
280
|
+
tabsRoot.dispatchEvent(
|
|
281
|
+
new CustomEvent("tabs:set", { detail: { value: themeChoice } }),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
document.querySelectorAll("starlight-theme-select").forEach((picker) => {
|
|
286
|
+
const nativeSelect = picker.querySelector("select");
|
|
287
|
+
if (nativeSelect instanceof HTMLSelectElement) {
|
|
288
|
+
nativeSelect.value = themeChoice;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const selectRoot = picker.querySelector(
|
|
292
|
+
'#starlight-theme-select, [data-slot="select"]',
|
|
293
|
+
);
|
|
294
|
+
if (!(selectRoot instanceof HTMLElement)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
selectRoot.dataset.defaultValue = themeChoice;
|
|
299
|
+
selectRoot.dataset.value = themeChoice;
|
|
300
|
+
selectRoot.dispatchEvent(
|
|
301
|
+
new CustomEvent("select:set", { detail: { value: themeChoice } }),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
const dispatchThemeToggleChange = (themeMode, themeChoice) => {
|
|
306
|
+
window.dispatchEvent(
|
|
307
|
+
new CustomEvent(eventName, {
|
|
308
|
+
detail: {
|
|
309
|
+
theme: themeMode,
|
|
310
|
+
themeChoice,
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
const syncThemeChoice = (themeChoice, emitEvent) => {
|
|
316
|
+
const themeMode = applyThemeModeToRoot(themeChoice);
|
|
317
|
+
syncThemeChoiceControls(themeChoice);
|
|
318
|
+
if (emitEvent) {
|
|
319
|
+
dispatchThemeToggleChange(themeMode, themeChoice);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const syncStoredThemeChoice = (emitEvent) => {
|
|
323
|
+
syncThemeChoice(loadStoredThemeChoice(), emitEvent);
|
|
324
|
+
};
|
|
325
|
+
const mediaQuery = matchMedia("(prefers-color-scheme: light)");
|
|
326
|
+
const onMediaChange = () => {
|
|
327
|
+
if (loadStoredThemeChoice() === "auto") {
|
|
328
|
+
syncThemeChoice("auto", true);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
syncStoredThemeChoice(false);
|
|
333
|
+
|
|
334
|
+
window.addEventListener("storage", (event) => {
|
|
335
|
+
if (event.key === storageKey) {
|
|
336
|
+
syncStoredThemeChoice(true);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (typeof mediaQuery.addEventListener === "function") {
|
|
341
|
+
mediaQuery.addEventListener("change", onMediaChange);
|
|
342
|
+
} else if (typeof mediaQuery.addListener === "function") {
|
|
343
|
+
mediaQuery.addListener(onMediaChange);
|
|
344
|
+
}
|
|
345
|
+
})();`;
|
|
346
|
+
}
|