@tenphi/renderer 0.25.2 → 0.25.4
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/dist/components/MainContent.styles.js +3 -0
- package/dist/components/Pagination.styles.js +1 -1
- package/dist/components/Sidebar.styles.js +6 -1
- package/dist/components/appearance-bootstrap.js +21 -13
- package/dist/components/appearance-preferences.test.ts +63 -27
- package/dist/components/appearance-preferences.ts +23 -16
- package/dist/overrides/ThemeSelect.astro +16 -9
- package/package.json +2 -2
|
@@ -10,6 +10,9 @@ export const MainContentRoot = defineComponent("MainContent", {
|
|
|
10
10
|
styles: {
|
|
11
11
|
"@property": TASTY_SPACING_PROPERTIES,
|
|
12
12
|
padding: "0 0 5rem",
|
|
13
|
+
// A focus target for the skip link and page swaps, not a control. Clicking
|
|
14
|
+
// its text focuses it, so keyboard scrolling would otherwise show a ring.
|
|
15
|
+
outline: "none",
|
|
13
16
|
// Layout spacing is a default; generated Markdown owns prose spacing.
|
|
14
17
|
ContentSpacing: {
|
|
15
18
|
$: ":where(Panel > Container) > * + *",
|
|
@@ -158,7 +158,9 @@ export const SidebarRoot = defineComponent("Sidebar", {
|
|
|
158
158
|
$: "ul > li + li",
|
|
159
159
|
blockMargin: {
|
|
160
160
|
"": null,
|
|
161
|
-
|
|
161
|
+
// States add no specificity, so leave sections to GroupSpacing rather
|
|
162
|
+
// than depending on the order of the two rules.
|
|
163
|
+
"@own(top-level & !section)": "0 start",
|
|
162
164
|
},
|
|
163
165
|
},
|
|
164
166
|
GroupSpacing: {
|
|
@@ -252,6 +254,9 @@ export const SidebarRoot = defineComponent("Sidebar", {
|
|
|
252
254
|
'@own((:hover | :focus-visible) & ![aria-current="page"])':
|
|
253
255
|
"#surface-2-hover",
|
|
254
256
|
},
|
|
257
|
+
// Lift the focus ring above the next item's fill.
|
|
258
|
+
position: { "": null, "@own(:focus-visible)": "relative" },
|
|
259
|
+
zIndex: { "": null, "@own(:focus-visible)": "1" },
|
|
255
260
|
},
|
|
256
261
|
SummaryMarker: {
|
|
257
262
|
$: "cookbook-sidebar summary::marker, summary::-webkit-details-marker",
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
// Kept dependency-free for the page shell's synchronous, pre-paint script.
|
|
2
|
+
// Reads the documented storage contract in appearance-preferences.ts.
|
|
2
3
|
(() => {
|
|
3
4
|
const root = globalThis.document.documentElement;
|
|
5
|
+
let appearance = {};
|
|
4
6
|
try {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// Without storage, the document's system appearance rules remain active.
|
|
10
|
-
}
|
|
11
|
-
try {
|
|
12
|
-
const appearance = JSON.parse(
|
|
13
|
-
globalThis.localStorage.getItem("cookbook-appearance") ?? "{}",
|
|
14
|
-
);
|
|
15
|
-
if (appearance?.contrast === "normal" || appearance?.contrast === "more")
|
|
16
|
-
root.dataset.contrast = appearance.contrast;
|
|
17
|
-
else delete root.dataset.contrast;
|
|
7
|
+
appearance =
|
|
8
|
+
JSON.parse(
|
|
9
|
+
globalThis.localStorage.getItem("cookbook-appearance") ?? "{}",
|
|
10
|
+
) ?? {};
|
|
18
11
|
} catch {
|
|
19
12
|
// Malformed or unavailable storage must not prevent the first paint.
|
|
20
13
|
}
|
|
14
|
+
let theme = appearance.theme;
|
|
15
|
+
if (theme === undefined) {
|
|
16
|
+
try {
|
|
17
|
+
// A scheme saved before the appearance object held it.
|
|
18
|
+
theme = globalThis.localStorage.getItem("cookbook-theme");
|
|
19
|
+
} catch {
|
|
20
|
+
// Without storage, the document's system appearance rules remain active.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (theme === "light" || theme === "dark") root.dataset.theme = theme;
|
|
24
|
+
else delete root.dataset.theme;
|
|
25
|
+
const contrast = appearance.contrast;
|
|
26
|
+
if (contrast === "normal" || contrast === "more")
|
|
27
|
+
root.dataset.contrast = contrast;
|
|
28
|
+
else delete root.dataset.contrast;
|
|
21
29
|
})();
|
|
@@ -7,71 +7,104 @@ function storage(values: Record<string, string> = {}) {
|
|
|
7
7
|
setItem: (key: string, value: string) => {
|
|
8
8
|
values[key] = value;
|
|
9
9
|
},
|
|
10
|
+
removeItem: (key: string) => {
|
|
11
|
+
delete values[key];
|
|
12
|
+
},
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
describe("appearance preferences", () => {
|
|
14
|
-
it("restores
|
|
17
|
+
it("restores independent theme and contrast choices from one object", () => {
|
|
15
18
|
const saved = storage({
|
|
16
|
-
"cookbook-
|
|
17
|
-
|
|
19
|
+
"cookbook-appearance": JSON.stringify({
|
|
20
|
+
theme: "dark",
|
|
21
|
+
contrast: "more",
|
|
22
|
+
}),
|
|
18
23
|
});
|
|
19
24
|
expect(loadPreferences(saved)).toEqual({
|
|
20
|
-
|
|
25
|
+
theme: "dark",
|
|
21
26
|
contrast: "more",
|
|
22
27
|
});
|
|
23
|
-
storePreferences({
|
|
28
|
+
storePreferences({ theme: "light", contrast: "more" }, saved);
|
|
24
29
|
expect(loadPreferences(saved)).toEqual({
|
|
25
|
-
|
|
30
|
+
theme: "light",
|
|
26
31
|
contrast: "more",
|
|
27
32
|
});
|
|
28
|
-
storePreferences({
|
|
29
|
-
expect(
|
|
30
|
-
|
|
33
|
+
storePreferences({ theme: "light", contrast: "normal" }, saved);
|
|
34
|
+
expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
|
|
35
|
+
theme: "light",
|
|
31
36
|
contrast: "normal",
|
|
32
37
|
});
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
it("persists
|
|
40
|
+
it("persists system choices without losing other fields", () => {
|
|
36
41
|
const saved = storage({
|
|
37
42
|
"cookbook-appearance": JSON.stringify({ contrast: "more", other: true }),
|
|
38
43
|
});
|
|
39
|
-
storePreferences({
|
|
40
|
-
expect(saved.getItem("cookbook-theme")).toBe("");
|
|
44
|
+
storePreferences({ theme: "system", contrast: "system" }, saved);
|
|
41
45
|
expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
|
|
46
|
+
theme: "system",
|
|
42
47
|
contrast: "system",
|
|
43
48
|
other: true,
|
|
44
49
|
});
|
|
45
50
|
expect(loadPreferences(saved)).toEqual({
|
|
46
|
-
|
|
51
|
+
theme: "system",
|
|
47
52
|
contrast: "system",
|
|
48
53
|
});
|
|
49
54
|
});
|
|
50
55
|
|
|
56
|
+
it("reads a scheme saved under the legacy key and removes it on save", () => {
|
|
57
|
+
const saved = storage({
|
|
58
|
+
"cookbook-theme": "dark",
|
|
59
|
+
"cookbook-appearance": JSON.stringify({ contrast: "more" }),
|
|
60
|
+
});
|
|
61
|
+
expect(loadPreferences(saved)).toEqual({
|
|
62
|
+
theme: "dark",
|
|
63
|
+
contrast: "more",
|
|
64
|
+
});
|
|
65
|
+
storePreferences(loadPreferences(saved), saved);
|
|
66
|
+
expect(saved.getItem("cookbook-theme")).toBeNull();
|
|
67
|
+
expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
|
|
68
|
+
theme: "dark",
|
|
69
|
+
contrast: "more",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("prefers the object's theme over the legacy key", () => {
|
|
74
|
+
const saved = storage({
|
|
75
|
+
"cookbook-theme": "dark",
|
|
76
|
+
"cookbook-appearance": JSON.stringify({ theme: "system" }),
|
|
77
|
+
});
|
|
78
|
+
expect(loadPreferences(saved).theme).toBe("system");
|
|
79
|
+
});
|
|
80
|
+
|
|
51
81
|
it.each(["{broken", "null", "[]", '"more"', '{"contrast":"invalid"}'])(
|
|
52
|
-
"recovers from malformed
|
|
82
|
+
"recovers from malformed appearance data: %s",
|
|
53
83
|
(value) => {
|
|
54
|
-
const saved = storage({
|
|
55
|
-
"cookbook-theme": "light",
|
|
56
|
-
"cookbook-appearance": value,
|
|
57
|
-
});
|
|
84
|
+
const saved = storage({ "cookbook-appearance": value });
|
|
58
85
|
expect(loadPreferences(saved)).toEqual({
|
|
59
|
-
|
|
86
|
+
theme: "system",
|
|
60
87
|
contrast: "system",
|
|
61
88
|
});
|
|
62
|
-
storePreferences({
|
|
89
|
+
storePreferences({ theme: "dark", contrast: "more" }, saved);
|
|
63
90
|
expect(loadPreferences(saved)).toEqual({
|
|
64
|
-
|
|
91
|
+
theme: "dark",
|
|
65
92
|
contrast: "more",
|
|
66
93
|
});
|
|
67
94
|
},
|
|
68
95
|
);
|
|
69
96
|
|
|
70
97
|
it("uses system defaults for unknown values or unavailable storage", () => {
|
|
71
|
-
expect(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
expect(
|
|
99
|
+
loadPreferences(
|
|
100
|
+
storage({
|
|
101
|
+
"cookbook-appearance": JSON.stringify({
|
|
102
|
+
theme: "invalid",
|
|
103
|
+
contrast: "invalid",
|
|
104
|
+
}),
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
).toEqual({ theme: "system", contrast: "system" });
|
|
75
108
|
const unavailable = {
|
|
76
109
|
getItem: () => {
|
|
77
110
|
throw new Error("Storage unavailable");
|
|
@@ -79,13 +112,16 @@ describe("appearance preferences", () => {
|
|
|
79
112
|
setItem: () => {
|
|
80
113
|
throw new Error("Storage unavailable");
|
|
81
114
|
},
|
|
115
|
+
removeItem: () => {
|
|
116
|
+
throw new Error("Storage unavailable");
|
|
117
|
+
},
|
|
82
118
|
};
|
|
83
119
|
expect(loadPreferences(unavailable)).toEqual({
|
|
84
|
-
|
|
120
|
+
theme: "system",
|
|
85
121
|
contrast: "system",
|
|
86
122
|
});
|
|
87
123
|
expect(() =>
|
|
88
|
-
storePreferences({
|
|
124
|
+
storePreferences({ theme: "dark", contrast: "more" }, unavailable),
|
|
89
125
|
).not.toThrow();
|
|
90
126
|
});
|
|
91
127
|
});
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
// `cookbook-appearance` is a documented contract that other apps on the same
|
|
2
|
+
// origin read and write (docs/theme-and-components.md, "Reader appearance").
|
|
3
|
+
// Keep its key and value format compatible; appearance-bootstrap.js reads it too.
|
|
4
|
+
export const APPEARANCE_KEY = "cookbook-appearance";
|
|
5
|
+
// The color scheme used to live in its own key. It is still read when the
|
|
6
|
+
// object has no `theme`, and removed on the next save.
|
|
7
|
+
export const LEGACY_THEME_KEY = "cookbook-theme";
|
|
8
|
+
|
|
9
|
+
type Theme = "light" | "dark" | "system";
|
|
2
10
|
type Contrast = "normal" | "more" | "system";
|
|
3
|
-
type Preferences = {
|
|
4
|
-
type PreferenceStorage = Pick<Storage, "getItem" | "setItem">;
|
|
11
|
+
type Preferences = { theme: Theme; contrast: Contrast };
|
|
12
|
+
type PreferenceStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
5
13
|
|
|
6
|
-
export const
|
|
7
|
-
value === "light" || value === "dark" ? value : "
|
|
14
|
+
export const parseTheme = (value: unknown): Theme =>
|
|
15
|
+
value === "light" || value === "dark" ? value : "system";
|
|
8
16
|
|
|
9
17
|
export const parseContrast = (value: unknown): Contrast =>
|
|
10
18
|
value === "normal" || value === "more" ? value : "system";
|
|
11
19
|
|
|
12
20
|
function storedAppearance(storage: PreferenceStorage): Record<string, unknown> {
|
|
13
21
|
try {
|
|
14
|
-
const value: unknown = JSON.parse(
|
|
15
|
-
storage.getItem("cookbook-appearance") ?? "{}",
|
|
16
|
-
);
|
|
22
|
+
const value: unknown = JSON.parse(storage.getItem(APPEARANCE_KEY) ?? "{}");
|
|
17
23
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
18
24
|
? (value as Record<string, unknown>)
|
|
19
25
|
: {};
|
|
@@ -23,11 +29,14 @@ function storedAppearance(storage: PreferenceStorage): Record<string, unknown> {
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function loadPreferences(storage?: PreferenceStorage): Preferences {
|
|
26
|
-
const preferences: Preferences = {
|
|
32
|
+
const preferences: Preferences = { theme: "system", contrast: "system" };
|
|
27
33
|
try {
|
|
28
34
|
storage ??= localStorage;
|
|
29
|
-
|
|
30
|
-
preferences.
|
|
35
|
+
const stored = storedAppearance(storage);
|
|
36
|
+
preferences.theme = parseTheme(
|
|
37
|
+
"theme" in stored ? stored.theme : storage.getItem(LEGACY_THEME_KEY),
|
|
38
|
+
);
|
|
39
|
+
preferences.contrast = parseContrast(stored.contrast);
|
|
31
40
|
} catch {
|
|
32
41
|
// System defaults work even when browser storage is unavailable.
|
|
33
42
|
}
|
|
@@ -41,16 +50,14 @@ export function storePreferences(
|
|
|
41
50
|
try {
|
|
42
51
|
storage ??= localStorage;
|
|
43
52
|
storage.setItem(
|
|
44
|
-
|
|
45
|
-
value.scheme === "auto" ? "" : value.scheme,
|
|
46
|
-
);
|
|
47
|
-
storage.setItem(
|
|
48
|
-
"cookbook-appearance",
|
|
53
|
+
APPEARANCE_KEY,
|
|
49
54
|
JSON.stringify({
|
|
50
55
|
...storedAppearance(storage),
|
|
56
|
+
theme: value.theme,
|
|
51
57
|
contrast: value.contrast,
|
|
52
58
|
}),
|
|
53
59
|
);
|
|
60
|
+
storage.removeItem(LEGACY_THEME_KEY);
|
|
54
61
|
} catch {
|
|
55
62
|
// The current page still updates when preferences cannot be persisted.
|
|
56
63
|
}
|
|
@@ -11,7 +11,11 @@ const id = `appearance-${crypto.randomUUID()}`;
|
|
|
11
11
|
const schemes = [
|
|
12
12
|
{ value: "light", label: Astro.locals.t("themeSelect.light"), icon: "sun" },
|
|
13
13
|
{ value: "dark", label: Astro.locals.t("themeSelect.dark"), icon: "moon" },
|
|
14
|
-
{
|
|
14
|
+
{
|
|
15
|
+
value: "system",
|
|
16
|
+
label: Astro.locals.t("themeSelect.auto"),
|
|
17
|
+
icon: "laptop",
|
|
18
|
+
},
|
|
15
19
|
] as const;
|
|
16
20
|
const contrasts = [
|
|
17
21
|
{ value: "normal", label: ui.normalContrast, icon: "normal" },
|
|
@@ -46,7 +50,7 @@ const contrasts = [
|
|
|
46
50
|
name={`${id}-scheme`}
|
|
47
51
|
value={value}
|
|
48
52
|
data-appearance-scheme
|
|
49
|
-
checked={value === "
|
|
53
|
+
checked={value === "system"}
|
|
50
54
|
/>
|
|
51
55
|
<Icon name={icon} />
|
|
52
56
|
<span>{label}</span>
|
|
@@ -83,9 +87,11 @@ const contrasts = [
|
|
|
83
87
|
|
|
84
88
|
<script>
|
|
85
89
|
import {
|
|
90
|
+
APPEARANCE_KEY,
|
|
91
|
+
LEGACY_THEME_KEY,
|
|
86
92
|
loadPreferences,
|
|
87
93
|
storePreferences,
|
|
88
|
-
|
|
94
|
+
parseTheme,
|
|
89
95
|
parseContrast,
|
|
90
96
|
} from "../components/appearance-preferences.js";
|
|
91
97
|
import { initializePopoverMotion } from "../client/popover-motion.js";
|
|
@@ -95,11 +101,11 @@ const contrasts = [
|
|
|
95
101
|
|
|
96
102
|
function applyPreferences(): void {
|
|
97
103
|
document.documentElement.dataset.theme =
|
|
98
|
-
current.
|
|
104
|
+
current.theme === "system"
|
|
99
105
|
? systemScheme.matches
|
|
100
106
|
? "light"
|
|
101
107
|
: "dark"
|
|
102
|
-
: current.
|
|
108
|
+
: current.theme;
|
|
103
109
|
if (current.contrast === "system") {
|
|
104
110
|
delete document.documentElement.dataset.contrast;
|
|
105
111
|
} else {
|
|
@@ -108,7 +114,7 @@ const contrasts = [
|
|
|
108
114
|
document
|
|
109
115
|
.querySelectorAll<HTMLInputElement>("[data-appearance-scheme]")
|
|
110
116
|
.forEach((input) => {
|
|
111
|
-
input.checked = input.value === current.
|
|
117
|
+
input.checked = input.value === current.theme;
|
|
112
118
|
});
|
|
113
119
|
document
|
|
114
120
|
.querySelectorAll<HTMLInputElement>("[data-appearance-contrast]")
|
|
@@ -118,12 +124,13 @@ const contrasts = [
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
systemScheme.addEventListener("change", () => {
|
|
121
|
-
if (current.
|
|
127
|
+
if (current.theme === "system") applyPreferences();
|
|
122
128
|
});
|
|
123
129
|
window.addEventListener("storage", (event) => {
|
|
124
130
|
if (
|
|
125
131
|
!event.key ||
|
|
126
|
-
|
|
132
|
+
event.key === APPEARANCE_KEY ||
|
|
133
|
+
event.key === LEGACY_THEME_KEY
|
|
127
134
|
) {
|
|
128
135
|
current = loadPreferences();
|
|
129
136
|
applyPreferences();
|
|
@@ -139,7 +146,7 @@ const contrasts = [
|
|
|
139
146
|
const input = event.target;
|
|
140
147
|
if (!(input instanceof HTMLInputElement)) return;
|
|
141
148
|
if (input.hasAttribute("data-appearance-scheme")) {
|
|
142
|
-
current.
|
|
149
|
+
current.theme = parseTheme(input.value);
|
|
143
150
|
} else if (input.hasAttribute("data-appearance-contrast")) {
|
|
144
151
|
current.contrast = parseContrast(input.value);
|
|
145
152
|
} else return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/renderer",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.4",
|
|
4
4
|
"description": "Astro renderer and Tasty theme for Cookbook",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
62
62
|
"@fontsource-variable/onest": "^5.3.0",
|
|
63
63
|
"@pagefind/default-ui": "1.5.2",
|
|
64
|
-
"@tenphi/docs": "0.25.
|
|
64
|
+
"@tenphi/docs": "0.25.4",
|
|
65
65
|
"@tenphi/eslint-plugin-tasty": "1.6.0",
|
|
66
66
|
"@tenphi/glaze": "2.0.1",
|
|
67
67
|
"@tenphi/tasty": "3.9.10",
|