@timmo001/groundwork-tokens 0.9.15 → 0.9.17
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/package.json +2 -2
- package/src/page.ts +106 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-tokens",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.17",
|
|
4
4
|
"description": "Groundwork's design tokens: the theme schema, the OKLCH solver and every theme as CSS and JSON",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"format:check": "prettier --check ."
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@types/culori": "4.0.1",
|
|
48
49
|
"culori": "4.0.2",
|
|
49
50
|
"effect": "4.1.1"
|
|
50
51
|
},
|
|
@@ -53,7 +54,6 @@
|
|
|
53
54
|
"@oxlint/plugins": "1.90.0",
|
|
54
55
|
"@timmo001/oxlint-rules": "0.6.3",
|
|
55
56
|
"@types/bun": "1.4.4",
|
|
56
|
-
"@types/culori": "4.0.1",
|
|
57
57
|
"oxlint": "1.90.0",
|
|
58
58
|
"oxlint-tsgolint": "7.0.2005",
|
|
59
59
|
"prettier": "3.9.10",
|
package/src/page.ts
CHANGED
|
@@ -30,8 +30,13 @@ export const customThemeStyleId = "gw-custom-theme";
|
|
|
30
30
|
|
|
31
31
|
const modes: readonly ThemeMode[] = ["auto", "light", "dark", "darker"];
|
|
32
32
|
|
|
33
|
+
/** Light or dark: the side of a mode a theme has to cover. Darker is dark. */
|
|
34
|
+
export type ThemeScheme = "light" | "dark";
|
|
35
|
+
|
|
33
36
|
export interface PageThemeState {
|
|
34
37
|
readonly mode: ThemeMode;
|
|
38
|
+
/** The scheme in use, with `auto` resolved from the system. */
|
|
39
|
+
readonly scheme: ThemeScheme;
|
|
35
40
|
/** The theme id in use: a built-in one, or `custom`. */
|
|
36
41
|
readonly theme: string;
|
|
37
42
|
/** The visitor's own theme's name, when they've imported one. */
|
|
@@ -47,12 +52,21 @@ export type ImportResult =
|
|
|
47
52
|
}
|
|
48
53
|
| { readonly ok: false; readonly error: string };
|
|
49
54
|
|
|
50
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* A theme to offer, by id and name. A `Theme` document is one. `mode` marks
|
|
57
|
+
* a theme with only one scheme, such as Catppuccin Latte: it's offered and
|
|
58
|
+
* kept for that scheme alone, beside a pick for the other.
|
|
59
|
+
*/
|
|
51
60
|
export interface ThemeChoice {
|
|
52
61
|
readonly id: string;
|
|
53
62
|
readonly name: string;
|
|
63
|
+
readonly mode?: ThemeScheme | undefined;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
/** Whether `choice` can show in `scheme`. */
|
|
67
|
+
export const themeCovers = (choice: ThemeChoice, scheme: ThemeScheme) =>
|
|
68
|
+
choice.mode === undefined || choice.mode === scheme;
|
|
69
|
+
|
|
56
70
|
/** The core themes by id and name, in the order the docs list them. */
|
|
57
71
|
export const coreThemeChoices: readonly ThemeChoice[] = [
|
|
58
72
|
base,
|
|
@@ -102,6 +116,7 @@ export interface PageTheme {
|
|
|
102
116
|
const keys = (key: string) => ({
|
|
103
117
|
mode: `${key}:mode`,
|
|
104
118
|
theme: `${key}:id`,
|
|
119
|
+
dark: `${key}:id-dark`,
|
|
105
120
|
document: `${key}:custom`,
|
|
106
121
|
css: `${key}:custom-css`,
|
|
107
122
|
name: `${key}:custom-name`,
|
|
@@ -142,7 +157,7 @@ export const themeBootScript = (
|
|
|
142
157
|
style: customThemeStyleId,
|
|
143
158
|
});
|
|
144
159
|
|
|
145
|
-
return `(()=>{const c=${config};let s;try{s=localStorage}catch{}const g=(k)=>{try{return s?.getItem(k)??null}catch{return null}};const r=document.documentElement;
|
|
160
|
+
return `(()=>{const c=${config};let s;try{s=localStorage}catch{}const g=(k)=>{try{return s?.getItem(k)??null}catch{return null}};const r=document.documentElement;const m=g(c.keys.mode)??c.mode;const d=m==="dark"||m==="darker"||(m==="auto"&&matchMedia("(prefers-color-scheme: dark)").matches);let t=(d?g(c.keys.dark):null)??g(c.keys.theme)??c.theme;if(t===c.custom){const css=g(c.keys.css);if(css){const e=document.createElement("style");e.id=c.style;e.textContent=css;document.head.append(e)}else t=c.theme}r.setAttribute("data-gw-theme",t);if(m==="auto")r.removeAttribute("data-gw-mode");else r.setAttribute("data-gw-mode",m)})();`;
|
|
146
161
|
};
|
|
147
162
|
|
|
148
163
|
/** Reads a theme from JSON text or an Omarchy `colors.toml`. */
|
|
@@ -171,21 +186,64 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
171
186
|
const known = (id: string) => options.themes.some((theme) => theme.id === id);
|
|
172
187
|
|
|
173
188
|
const storedMode = read(stored.mode);
|
|
174
|
-
const storedTheme = read(stored.theme);
|
|
175
189
|
const customName = read(stored.name);
|
|
176
190
|
const customCss = read(stored.css);
|
|
177
191
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
?
|
|
185
|
-
:
|
|
186
|
-
|
|
192
|
+
const system = matchMedia("(prefers-color-scheme: dark)");
|
|
193
|
+
|
|
194
|
+
const schemeOf = (mode: ThemeMode): ThemeScheme =>
|
|
195
|
+
mode === "light"
|
|
196
|
+
? "light"
|
|
197
|
+
: mode === "auto" && !system.matches
|
|
198
|
+
? "light"
|
|
199
|
+
: "dark";
|
|
200
|
+
|
|
201
|
+
/** Whether `id` can be the pick for `scheme`. */
|
|
202
|
+
const fits = (id: string | null, scheme: ThemeScheme, custom: boolean) =>
|
|
203
|
+
id !== null &&
|
|
204
|
+
((id === customThemeId && custom) ||
|
|
205
|
+
options.themes.some(
|
|
206
|
+
(theme) => theme.id === id && themeCovers(theme, scheme),
|
|
207
|
+
));
|
|
208
|
+
|
|
209
|
+
const fallback = (scheme: ThemeScheme) =>
|
|
210
|
+
options.themes.some(
|
|
211
|
+
(theme) =>
|
|
212
|
+
theme.id === options.defaultTheme && themeCovers(theme, scheme),
|
|
213
|
+
)
|
|
214
|
+
? options.defaultTheme
|
|
215
|
+
: (options.themes.find((theme) => themeCovers(theme, scheme))?.id ??
|
|
216
|
+
options.defaultTheme);
|
|
217
|
+
|
|
218
|
+
const storedLight = read(stored.theme);
|
|
219
|
+
const storedDark = read(stored.dark) ?? storedLight;
|
|
220
|
+
|
|
221
|
+
/** The theme kept for each scheme. */
|
|
222
|
+
let picks: Record<ThemeScheme, string> = {
|
|
223
|
+
light: fits(storedLight, "light", customCss !== null)
|
|
224
|
+
? (storedLight ?? options.defaultTheme)
|
|
225
|
+
: fallback("light"),
|
|
226
|
+
dark: fits(storedDark, "dark", customCss !== null)
|
|
227
|
+
? (storedDark ?? options.defaultTheme)
|
|
228
|
+
: fallback("dark"),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const savePicks = () => {
|
|
232
|
+
write(stored.theme, picks.light);
|
|
233
|
+
write(stored.dark, picks.dark);
|
|
187
234
|
};
|
|
188
235
|
|
|
236
|
+
const resolve = (mode: ThemeMode, custom: string | null): PageThemeState => {
|
|
237
|
+
const scheme = schemeOf(mode);
|
|
238
|
+
|
|
239
|
+
return { mode, scheme, theme: picks[scheme], custom };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let state: PageThemeState = resolve(
|
|
243
|
+
modes.find((mode) => mode === storedMode) ?? defaultMode,
|
|
244
|
+
customCss === null ? null : customName,
|
|
245
|
+
);
|
|
246
|
+
|
|
189
247
|
const setStyle = (css: string | null) => {
|
|
190
248
|
let style = document.getElementById(customThemeStyleId);
|
|
191
249
|
|
|
@@ -228,8 +286,9 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
228
286
|
write(stored.document, text);
|
|
229
287
|
write(stored.css, css);
|
|
230
288
|
write(stored.name, document.name);
|
|
231
|
-
|
|
232
|
-
|
|
289
|
+
picks = { light: customThemeId, dark: customThemeId };
|
|
290
|
+
savePicks();
|
|
291
|
+
commit(resolve(state.mode, document.name));
|
|
233
292
|
|
|
234
293
|
return runtime.checks
|
|
235
294
|
.filter((result) => !result.passes)
|
|
@@ -240,17 +299,44 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
240
299
|
};
|
|
241
300
|
|
|
242
301
|
const setTheme = (id: string) => {
|
|
243
|
-
if (
|
|
302
|
+
if (id === customThemeId) {
|
|
303
|
+
if (state.custom === null) return;
|
|
244
304
|
|
|
245
|
-
|
|
246
|
-
|
|
305
|
+
picks = { light: id, dark: id };
|
|
306
|
+
} else {
|
|
307
|
+
const choice = options.themes.find((theme) => theme.id === id);
|
|
308
|
+
|
|
309
|
+
if (choice === undefined) return;
|
|
310
|
+
|
|
311
|
+
picks =
|
|
312
|
+
choice.mode === undefined
|
|
313
|
+
? { light: id, dark: id }
|
|
314
|
+
: { ...picks, [choice.mode]: id };
|
|
315
|
+
|
|
316
|
+
// Picking a light-only theme in dark, or the reverse, shows it
|
|
317
|
+
if (choice.mode !== undefined && choice.mode !== state.scheme) {
|
|
318
|
+
savePicks();
|
|
319
|
+
write(stored.mode, choice.mode);
|
|
320
|
+
commit(resolve(choice.mode, state.custom));
|
|
321
|
+
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
savePicks();
|
|
327
|
+
commit(resolve(state.mode, state.custom));
|
|
247
328
|
};
|
|
248
329
|
|
|
330
|
+
// A system change moves `auto` between the light and dark picks
|
|
331
|
+
system.addEventListener("change", () => {
|
|
332
|
+
if (state.mode === "auto") commit(resolve(state.mode, state.custom));
|
|
333
|
+
});
|
|
334
|
+
|
|
249
335
|
const page: PageTheme = {
|
|
250
336
|
state: () => state,
|
|
251
337
|
setMode: (mode) => {
|
|
252
338
|
write(stored.mode, mode);
|
|
253
|
-
commit(
|
|
339
|
+
commit(resolve(mode, state.custom));
|
|
254
340
|
},
|
|
255
341
|
setTheme,
|
|
256
342
|
importTheme: async (text) => {
|
|
@@ -273,7 +359,8 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
273
359
|
|
|
274
360
|
for (const key of Object.values(stored)) write(key, null);
|
|
275
361
|
|
|
276
|
-
|
|
362
|
+
picks = { light: fallback("light"), dark: fallback("dark") };
|
|
363
|
+
commit(resolve(defaultMode, null));
|
|
277
364
|
},
|
|
278
365
|
subscribe: (listener) => {
|
|
279
366
|
listeners.add(listener);
|