@timmo001/groundwork-tokens 0.9.39 → 0.10.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/README.md +11 -0
- package/generated/Themes.qml +1 -1
- package/generated/opentui.ts +96 -4
- package/generated/themes.css +261 -196
- package/generated/themes.json +97 -82
- package/generated/themes.ts +97 -82
- package/package.json +2 -1
- package/src/choices.ts +180 -0
- package/src/generate.ts +8 -8
- package/src/page.ts +195 -315
- package/src/schema.ts +3 -2
- package/themes/tokyo-night-moon.json +14 -0
- package/themes/tokyo-night-storm.json +14 -0
- package/themes/tokyo-night.json +14 -0
- package/themes/tokyo-night-day.json +0 -21
package/src/page.ts
CHANGED
|
@@ -9,55 +9,57 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import tokyoNightDay from "../themes/tokyo-night-day.json" with { type: "json" };
|
|
26
|
-
import tokyoNightMoon from "../themes/tokyo-night-moon.json" with { type: "json" };
|
|
27
|
-
import tokyoNightStorm from "../themes/tokyo-night-storm.json" with { type: "json" };
|
|
28
|
-
import tokyoNight from "../themes/tokyo-night.json" with { type: "json" };
|
|
12
|
+
import {
|
|
13
|
+
customThemeId,
|
|
14
|
+
customThemeStyleId,
|
|
15
|
+
omarchyStorageKeys,
|
|
16
|
+
omarchyThemeId,
|
|
17
|
+
omarchyThemeStyleId,
|
|
18
|
+
themeAllows,
|
|
19
|
+
themeStorageKeys,
|
|
20
|
+
type ThemeBootOptions,
|
|
21
|
+
type ThemeChoice,
|
|
22
|
+
type ThemeMode,
|
|
23
|
+
type ThemeScheme,
|
|
24
|
+
} from "./choices.ts";
|
|
29
25
|
import type { OmarchyCurrentTheme } from "./omarchy.ts";
|
|
30
26
|
import type { ThemeStore } from "./runtime.ts";
|
|
31
27
|
import type { Theme } from "./schema.ts";
|
|
32
28
|
// Only for `document.modelContext`'s type; the tools themselves load lazily
|
|
33
29
|
import type {} from "./webmcp.ts";
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
31
|
+
export {
|
|
32
|
+
coreThemeChoices,
|
|
33
|
+
customThemeId,
|
|
34
|
+
customThemeStyleId,
|
|
35
|
+
omarchyThemeId,
|
|
36
|
+
omarchyThemeStyleId,
|
|
37
|
+
optionalThemeChoices,
|
|
38
|
+
optionalThemeDocuments,
|
|
39
|
+
themeAllows,
|
|
40
|
+
themeBootScript,
|
|
41
|
+
themeCovers,
|
|
42
|
+
themeOptions,
|
|
43
|
+
type ThemeBootOptions,
|
|
44
|
+
type ThemeChoice,
|
|
45
|
+
type ThemeMode,
|
|
46
|
+
type ThemeScheme,
|
|
47
|
+
} from "./choices.ts";
|
|
49
48
|
|
|
50
49
|
const modes: readonly ThemeMode[] = ["auto", "light", "dark", "darker"];
|
|
51
50
|
|
|
52
|
-
/** Light or dark: the side of a mode a theme has to cover. Darker is dark. */
|
|
53
|
-
export type ThemeScheme = "light" | "dark";
|
|
54
|
-
|
|
55
51
|
export interface PageThemeState {
|
|
56
52
|
readonly mode: ThemeMode;
|
|
57
53
|
/** The scheme in use, with `auto` resolved from the system. */
|
|
58
54
|
readonly scheme: ThemeScheme;
|
|
59
|
-
/** The theme id in use: a built-in one, or `custom`. */
|
|
55
|
+
/** The theme id in use: a built-in one, `omarchy` or `custom`. */
|
|
60
56
|
readonly theme: string;
|
|
57
|
+
/**
|
|
58
|
+
* The one scheme the theme in use covers, such as `light` for Catppuccin
|
|
59
|
+
* Latte, or `null` when it has both. Pass it to the picker's `scheme`, so
|
|
60
|
+
* the other modes can't be picked.
|
|
61
|
+
*/
|
|
62
|
+
readonly only: ThemeScheme | null;
|
|
61
63
|
/** The visitor's own theme's name, when they've imported one. */
|
|
62
64
|
readonly custom: string | null;
|
|
63
65
|
/** The desktop's Omarchy theme as a choice, when the app passed one. */
|
|
@@ -73,73 +75,7 @@ export type ImportResult =
|
|
|
73
75
|
}
|
|
74
76
|
| { readonly ok: false; readonly error: string };
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
* A theme to offer, by id and name. A `Theme` document is one. `mode` marks
|
|
78
|
-
* a theme with only one scheme, such as Catppuccin Latte: it's offered and
|
|
79
|
-
* kept for that scheme alone, beside a pick for the other.
|
|
80
|
-
*/
|
|
81
|
-
export interface ThemeChoice {
|
|
82
|
-
readonly id: string;
|
|
83
|
-
readonly name: string;
|
|
84
|
-
readonly mode?: ThemeScheme | undefined;
|
|
85
|
-
/** The palette an optional theme comes from, such as `Catppuccin`. */
|
|
86
|
-
readonly family?: string | undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Whether `choice` can show in `scheme`. */
|
|
90
|
-
export const themeCovers = (choice: ThemeChoice, scheme: ThemeScheme) =>
|
|
91
|
-
choice.mode === undefined || choice.mode === scheme;
|
|
92
|
-
|
|
93
|
-
/** The core themes by id and name, in the order the docs list them. */
|
|
94
|
-
export const coreThemeChoices: readonly ThemeChoice[] = [
|
|
95
|
-
base,
|
|
96
|
-
glazing,
|
|
97
|
-
survey,
|
|
98
|
-
slate,
|
|
99
|
-
bridge,
|
|
100
|
-
snag,
|
|
101
|
-
].map(({ id, name }) => ({ id, name }));
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The optional themes' documents, by family and then from light to dark.
|
|
105
|
-
* They're bundled with the module, so an app compiled to one binary has them
|
|
106
|
-
* without reading `node_modules`. Decode one with the `MappedTheme` Schema.
|
|
107
|
-
*/
|
|
108
|
-
export const optionalThemeDocuments = [
|
|
109
|
-
catppuccinLatte,
|
|
110
|
-
catppuccinFrappe,
|
|
111
|
-
catppuccinMacchiato,
|
|
112
|
-
catppuccinMocha,
|
|
113
|
-
tokyoNightDay,
|
|
114
|
-
tokyoNight,
|
|
115
|
-
tokyoNightStorm,
|
|
116
|
-
tokyoNightMoon,
|
|
117
|
-
github,
|
|
118
|
-
githubDimmed,
|
|
119
|
-
githubHighContrast,
|
|
120
|
-
] as const;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The optional themes, mapped from other palettes, by family and then from
|
|
124
|
-
* light to dark. One with a single mode is offered for that scheme alone;
|
|
125
|
-
* one with both, such as GitHub's, for either.
|
|
126
|
-
*/
|
|
127
|
-
export const optionalThemeChoices: readonly ThemeChoice[] =
|
|
128
|
-
optionalThemeDocuments.map((theme) => ({
|
|
129
|
-
id: theme.id,
|
|
130
|
-
name: theme.name,
|
|
131
|
-
family: theme.family,
|
|
132
|
-
mode:
|
|
133
|
-
"otherPalette" in theme
|
|
134
|
-
? undefined
|
|
135
|
-
: theme.mode === "light"
|
|
136
|
-
? "light"
|
|
137
|
-
: "dark",
|
|
138
|
-
}));
|
|
139
|
-
|
|
140
|
-
export interface PageThemeOptions {
|
|
141
|
-
/** The local storage key prefix, such as `my-site:theme`. */
|
|
142
|
-
readonly key: string;
|
|
78
|
+
export interface PageThemeOptions extends ThemeBootOptions {
|
|
143
79
|
/** The built-in themes to offer, their CSS already on the page. */
|
|
144
80
|
readonly themes: readonly ThemeChoice[];
|
|
145
81
|
/**
|
|
@@ -147,16 +83,6 @@ export interface PageThemeOptions {
|
|
|
147
83
|
* register. Defaults to the core themes.
|
|
148
84
|
*/
|
|
149
85
|
readonly documents?: () => Promise<readonly Theme[]>;
|
|
150
|
-
/** The theme id to start with. */
|
|
151
|
-
readonly defaultTheme: string;
|
|
152
|
-
/** The mode to start with. Defaults to `auto`. */
|
|
153
|
-
readonly defaultMode?: ThemeMode;
|
|
154
|
-
/**
|
|
155
|
-
* Whether the Omarchy theme takes over while the visitor hasn't picked
|
|
156
|
-
* one. With `false` it's only offered, and `defaultTheme` stays in use.
|
|
157
|
-
* Defaults to `true`.
|
|
158
|
-
*/
|
|
159
|
-
readonly followOmarchy?: boolean;
|
|
160
86
|
/** The element the attributes go on. Defaults to `<html>`. */
|
|
161
87
|
readonly root?: HTMLElement;
|
|
162
88
|
/**
|
|
@@ -176,7 +102,9 @@ export interface PageThemeOptions {
|
|
|
176
102
|
|
|
177
103
|
export interface PageTheme {
|
|
178
104
|
readonly state: () => PageThemeState;
|
|
105
|
+
/** Sets the mode, unless the theme in use doesn't come in it. */
|
|
179
106
|
readonly setMode: (mode: ThemeMode) => void;
|
|
107
|
+
/** Picks a theme, moving to its scheme when it has only one. */
|
|
180
108
|
readonly setTheme: (id: string) => void;
|
|
181
109
|
/** Solves a theme document's JSON or an Omarchy `colors.toml` and applies it. */
|
|
182
110
|
readonly importTheme: (text: string) => Promise<ImportResult>;
|
|
@@ -184,9 +112,9 @@ export interface PageTheme {
|
|
|
184
112
|
readonly reset: () => void;
|
|
185
113
|
/**
|
|
186
114
|
* Offers the Omarchy theme the app runs under, or drops it with `null`.
|
|
187
|
-
* It's
|
|
188
|
-
* switch on the desktop follows through
|
|
189
|
-
*
|
|
115
|
+
* It's only ever a choice, never the default; while it's picked, a theme
|
|
116
|
+
* switch on the desktop follows through. Apps that can read it, such as
|
|
117
|
+
* desktop apps, pass it on start and on every switch.
|
|
190
118
|
*/
|
|
191
119
|
readonly setOmarchyTheme: (
|
|
192
120
|
theme: OmarchyCurrentTheme | null,
|
|
@@ -195,22 +123,6 @@ export interface PageTheme {
|
|
|
195
123
|
readonly subscribe: (listener: (state: PageThemeState) => void) => () => void;
|
|
196
124
|
}
|
|
197
125
|
|
|
198
|
-
const keys = (key: string) => ({
|
|
199
|
-
mode: `${key}:mode`,
|
|
200
|
-
theme: `${key}:id`,
|
|
201
|
-
dark: `${key}:id-dark`,
|
|
202
|
-
document: `${key}:custom`,
|
|
203
|
-
css: `${key}:custom-css`,
|
|
204
|
-
name: `${key}:custom-name`,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
/** The Omarchy theme's cache, kept apart so a reset doesn't drop it. */
|
|
208
|
-
const omarchyKeys = (key: string) => ({
|
|
209
|
-
css: `${key}:omarchy-css`,
|
|
210
|
-
name: `${key}:omarchy-name`,
|
|
211
|
-
mode: `${key}:omarchy-mode`,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
126
|
const read = (key: string) => {
|
|
215
127
|
try {
|
|
216
128
|
return localStorage.getItem(key);
|
|
@@ -228,34 +140,6 @@ const write = (key: string, value: string | null) => {
|
|
|
228
140
|
}
|
|
229
141
|
};
|
|
230
142
|
|
|
231
|
-
/**
|
|
232
|
-
* An inline script that applies the stored mode, theme and any custom or
|
|
233
|
-
* Omarchy theme's cached CSS before the first paint. Takes the same key and
|
|
234
|
-
* defaults as `pageTheme`.
|
|
235
|
-
*/
|
|
236
|
-
export const themeBootScript = (
|
|
237
|
-
options: Pick<
|
|
238
|
-
PageThemeOptions,
|
|
239
|
-
"key" | "defaultTheme" | "defaultMode" | "followOmarchy"
|
|
240
|
-
>,
|
|
241
|
-
) => {
|
|
242
|
-
const config = JSON.stringify({
|
|
243
|
-
keys: keys(options.key),
|
|
244
|
-
omarchy: omarchyKeys(options.key),
|
|
245
|
-
theme: options.defaultTheme,
|
|
246
|
-
mode: options.defaultMode ?? "auto",
|
|
247
|
-
follow: options.followOmarchy !== false,
|
|
248
|
-
custom: customThemeId,
|
|
249
|
-
style: customThemeStyleId,
|
|
250
|
-
omarchyId: omarchyThemeId,
|
|
251
|
-
omarchyStyle: omarchyThemeStyleId,
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// With nothing picked, a cached Omarchy theme for the scheme is the
|
|
255
|
-
// default, unless it's only offered
|
|
256
|
-
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 a=(id,css)=>{const e=document.createElement("style");e.id=id;e.textContent=css;document.head.append(e)};const m=g(c.keys.mode)??c.mode;const d=m==="dark"||m==="darker"||(m==="auto"&&matchMedia("(prefers-color-scheme: dark)").matches);const o=g(c.omarchy.css);let t=(d?g(c.keys.dark):null)??g(c.keys.theme)??(c.follow&&o&&g(c.omarchy.mode)===(d?"dark":"light")?c.omarchyId:c.theme);if(t===c.custom){const css=g(c.keys.css);if(css)a(c.style,css);else t=c.theme}else if(t===c.omarchyId){if(o)a(c.omarchyStyle,o);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)})();`;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
143
|
/** Reads a theme from JSON text or an Omarchy `colors.toml`. */
|
|
260
144
|
const readTheme = async (text: string) => {
|
|
261
145
|
const { Result } = await import("effect");
|
|
@@ -269,23 +153,27 @@ const readTheme = async (text: string) => {
|
|
|
269
153
|
return omarchyTheme(text);
|
|
270
154
|
};
|
|
271
155
|
|
|
156
|
+
const schemeValue = (value: string | null): ThemeScheme | undefined =>
|
|
157
|
+
value === "light" || value === "dark" ? value : undefined;
|
|
158
|
+
|
|
272
159
|
/**
|
|
273
160
|
* Sets up the page's theme from what's stored, falling back to the defaults,
|
|
274
161
|
* and registers the WebMCP theme tools where the browser offers them.
|
|
162
|
+
*
|
|
163
|
+
* There's one pick. A theme with one scheme keeps the page in it, so its
|
|
164
|
+
* other modes can't be set. An imported or Omarchy theme is offered once
|
|
165
|
+
* there is one, but is only used when it's picked: the default is always
|
|
166
|
+
* `defaultTheme`.
|
|
275
167
|
*/
|
|
276
168
|
export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
277
|
-
const stored =
|
|
278
|
-
const storedOmarchy =
|
|
169
|
+
const stored = themeStorageKeys(options.key);
|
|
170
|
+
const storedOmarchy = omarchyStorageKeys(options.key);
|
|
279
171
|
const root = options.root ?? document.documentElement;
|
|
280
172
|
const defaultMode = options.defaultMode ?? "auto";
|
|
281
173
|
const listeners = new Set<(state: PageThemeState) => void>();
|
|
282
174
|
|
|
283
175
|
const known = (id: string) => options.themes.some((theme) => theme.id === id);
|
|
284
176
|
|
|
285
|
-
const storedMode = read(stored.mode);
|
|
286
|
-
const customName = read(stored.name);
|
|
287
|
-
const customCss = read(stored.css);
|
|
288
|
-
|
|
289
177
|
const setStyle = (id: string, css: string | null) => {
|
|
290
178
|
let style = document.getElementById(id);
|
|
291
179
|
|
|
@@ -310,13 +198,9 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
310
198
|
{
|
|
311
199
|
const css = read(storedOmarchy.css);
|
|
312
200
|
const name = read(storedOmarchy.name);
|
|
313
|
-
const mode = read(storedOmarchy.mode);
|
|
201
|
+
const mode = schemeValue(read(storedOmarchy.mode));
|
|
314
202
|
|
|
315
|
-
if (
|
|
316
|
-
css !== null &&
|
|
317
|
-
name !== null &&
|
|
318
|
-
(mode === "light" || mode === "dark")
|
|
319
|
-
) {
|
|
203
|
+
if (css !== null && name !== null && mode !== undefined) {
|
|
320
204
|
omarchy = { id: omarchyThemeId, name, mode };
|
|
321
205
|
|
|
322
206
|
// The boot script may have put it on already
|
|
@@ -326,8 +210,24 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
326
210
|
}
|
|
327
211
|
}
|
|
328
212
|
|
|
329
|
-
|
|
330
|
-
|
|
213
|
+
/** The visitor's own theme as a choice, when there's one. */
|
|
214
|
+
let custom: ThemeChoice | null =
|
|
215
|
+
read(stored.css) === null
|
|
216
|
+
? null
|
|
217
|
+
: {
|
|
218
|
+
id: customThemeId,
|
|
219
|
+
name: read(stored.name) ?? "Custom",
|
|
220
|
+
mode: schemeValue(read(stored.scheme)),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const choiceOf = (id: string | null) =>
|
|
224
|
+
id === null
|
|
225
|
+
? undefined
|
|
226
|
+
: [
|
|
227
|
+
...options.themes,
|
|
228
|
+
...(omarchy === null ? [] : [omarchy]),
|
|
229
|
+
...(custom === null ? [] : [custom]),
|
|
230
|
+
].find((theme) => theme.id === id);
|
|
331
231
|
|
|
332
232
|
const system = matchMedia("(prefers-color-scheme: dark)");
|
|
333
233
|
|
|
@@ -338,59 +238,46 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
338
238
|
? "light"
|
|
339
239
|
: "dark";
|
|
340
240
|
|
|
341
|
-
/**
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
? options.defaultTheme
|
|
360
|
-
: (options.themes.find((theme) => themeCovers(theme, scheme))?.id ??
|
|
361
|
-
options.defaultTheme);
|
|
362
|
-
|
|
363
|
-
const storedLight = read(stored.theme);
|
|
364
|
-
const storedDark = read(stored.dark) ?? storedLight;
|
|
365
|
-
|
|
366
|
-
/** The theme kept for each scheme. */
|
|
367
|
-
let picks: Record<ThemeScheme, string> = {
|
|
368
|
-
light: fits(storedLight, "light", customCss !== null)
|
|
369
|
-
? (storedLight ?? options.defaultTheme)
|
|
370
|
-
: fallback("light"),
|
|
371
|
-
dark: fits(storedDark, "dark", customCss !== null)
|
|
372
|
-
? (storedDark ?? options.defaultTheme)
|
|
373
|
-
: fallback("dark"),
|
|
241
|
+
/** `mode`, or the theme's own scheme when it doesn't come in `mode`. */
|
|
242
|
+
const fit = (choice: ThemeChoice | undefined, mode: ThemeMode): ThemeMode =>
|
|
243
|
+
choice === undefined || themeAllows(choice, mode)
|
|
244
|
+
? mode
|
|
245
|
+
: (choice.mode ?? mode);
|
|
246
|
+
|
|
247
|
+
const resolve = (theme: string, mode: ThemeMode): PageThemeState => {
|
|
248
|
+
const choice = choiceOf(theme);
|
|
249
|
+
const fitted = fit(choice, mode);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
mode: fitted,
|
|
253
|
+
scheme: schemeOf(fitted),
|
|
254
|
+
theme,
|
|
255
|
+
only: choice?.mode ?? null,
|
|
256
|
+
custom: custom?.name ?? null,
|
|
257
|
+
omarchy,
|
|
258
|
+
};
|
|
374
259
|
};
|
|
375
260
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
write(stored.dark, picks.dark);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const resolve = (mode: ThemeMode, custom: string | null): PageThemeState => {
|
|
382
|
-
const scheme = schemeOf(mode);
|
|
261
|
+
/** The default, as `reset` and a vanished pick go back to. */
|
|
262
|
+
const fallback = () => resolve(options.defaultTheme, defaultMode);
|
|
383
263
|
|
|
384
|
-
|
|
385
|
-
|
|
264
|
+
let state: PageThemeState = (() => {
|
|
265
|
+
const theme = read(stored.theme);
|
|
266
|
+
const mode = modes.find((entry) => entry === read(stored.mode));
|
|
386
267
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
);
|
|
268
|
+
return theme !== null && choiceOf(theme) !== undefined
|
|
269
|
+
? resolve(theme, mode ?? defaultMode)
|
|
270
|
+
: resolve(options.defaultTheme, mode ?? defaultMode);
|
|
271
|
+
})();
|
|
391
272
|
|
|
392
|
-
const commit = (next: PageThemeState) => {
|
|
273
|
+
const commit = (next: PageThemeState, save = true) => {
|
|
393
274
|
state = next;
|
|
275
|
+
|
|
276
|
+
if (save) {
|
|
277
|
+
write(stored.theme, state.theme);
|
|
278
|
+
write(stored.mode, state.mode);
|
|
279
|
+
}
|
|
280
|
+
|
|
394
281
|
root.setAttribute("data-gw-theme", state.theme);
|
|
395
282
|
|
|
396
283
|
if (state.mode === "auto") root.removeAttribute("data-gw-mode");
|
|
@@ -399,8 +286,15 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
399
286
|
for (const listener of listeners) listener(state);
|
|
400
287
|
};
|
|
401
288
|
|
|
402
|
-
/**
|
|
403
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Solves a document under the custom id and keeps it. `pick` uses it,
|
|
291
|
+
* as an import does; a theme from the store is only offered.
|
|
292
|
+
*/
|
|
293
|
+
const applyCustom = async (
|
|
294
|
+
document: Theme,
|
|
295
|
+
text: string,
|
|
296
|
+
{ pick, keep }: { pick: boolean; keep: boolean },
|
|
297
|
+
) => {
|
|
404
298
|
const { appTonesCss, runtimeTheme } = await import("./runtime.ts");
|
|
405
299
|
const runtime = runtimeTheme(document, customThemeId);
|
|
406
300
|
|
|
@@ -409,13 +303,26 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
409
303
|
? runtime.css
|
|
410
304
|
: `${runtime.css}\n\n${appTonesCss(options.tones, runtime.theme)}`;
|
|
411
305
|
|
|
306
|
+
const scheme =
|
|
307
|
+
runtime.theme.modes.light === undefined
|
|
308
|
+
? "dark"
|
|
309
|
+
: runtime.theme.modes.dark === undefined
|
|
310
|
+
? "light"
|
|
311
|
+
: undefined;
|
|
312
|
+
|
|
412
313
|
setStyle(customThemeStyleId, css);
|
|
413
314
|
write(stored.document, text);
|
|
414
315
|
write(stored.css, css);
|
|
415
316
|
write(stored.name, document.name);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
317
|
+
write(stored.scheme, scheme ?? null);
|
|
318
|
+
custom = { id: customThemeId, name: document.name, mode: scheme };
|
|
319
|
+
|
|
320
|
+
commit(
|
|
321
|
+
resolve(
|
|
322
|
+
pick || state.theme === customThemeId ? customThemeId : state.theme,
|
|
323
|
+
state.mode,
|
|
324
|
+
),
|
|
325
|
+
);
|
|
419
326
|
|
|
420
327
|
if (keep) await options.store?.save(text).catch(() => undefined);
|
|
421
328
|
|
|
@@ -427,45 +334,41 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
427
334
|
);
|
|
428
335
|
};
|
|
429
336
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
? { light: id, dark: id }
|
|
443
|
-
: { ...picks, [choice.mode]: id };
|
|
337
|
+
/** Drops the visitor's own theme, leaving it if it was in use. */
|
|
338
|
+
const dropCustom = () => {
|
|
339
|
+
setStyle(customThemeStyleId, null);
|
|
340
|
+
|
|
341
|
+
for (const key of [
|
|
342
|
+
stored.document,
|
|
343
|
+
stored.css,
|
|
344
|
+
stored.name,
|
|
345
|
+
stored.scheme,
|
|
346
|
+
]) {
|
|
347
|
+
write(key, null);
|
|
348
|
+
}
|
|
444
349
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
savePicks();
|
|
448
|
-
write(stored.mode, choice.mode);
|
|
449
|
-
commit(resolve(choice.mode, state.custom));
|
|
350
|
+
custom = null;
|
|
351
|
+
};
|
|
450
352
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
353
|
+
const setTheme = (id: string) => {
|
|
354
|
+
if (choiceOf(id) === undefined) return;
|
|
454
355
|
|
|
455
|
-
|
|
456
|
-
commit(resolve(state.mode, state.custom));
|
|
356
|
+
commit(resolve(id, state.mode));
|
|
457
357
|
};
|
|
458
358
|
|
|
459
|
-
// A system change moves `auto` between
|
|
359
|
+
// A system change moves `auto` between light and dark
|
|
460
360
|
system.addEventListener("change", () => {
|
|
461
|
-
if (state.mode === "auto") commit(resolve(state.
|
|
361
|
+
if (state.mode === "auto") commit(resolve(state.theme, state.mode), false);
|
|
462
362
|
});
|
|
463
363
|
|
|
464
364
|
const page: PageTheme = {
|
|
465
365
|
state: () => state,
|
|
466
366
|
setMode: (mode) => {
|
|
467
|
-
|
|
468
|
-
|
|
367
|
+
const choice = choiceOf(state.theme);
|
|
368
|
+
|
|
369
|
+
if (choice !== undefined && !themeAllows(choice, mode)) return;
|
|
370
|
+
|
|
371
|
+
commit(resolve(state.theme, mode));
|
|
469
372
|
},
|
|
470
373
|
setTheme,
|
|
471
374
|
importTheme: async (text) => {
|
|
@@ -479,32 +382,22 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
479
382
|
};
|
|
480
383
|
}
|
|
481
384
|
|
|
482
|
-
const misses = await applyCustom(parsed.success, text
|
|
385
|
+
const misses = await applyCustom(parsed.success, text, {
|
|
386
|
+
pick: true,
|
|
387
|
+
keep: true,
|
|
388
|
+
});
|
|
483
389
|
|
|
484
390
|
return { ok: true, name: parsed.success.name, misses };
|
|
485
391
|
},
|
|
486
392
|
reset: () => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
picks = { light: fallback("light"), dark: fallback("dark") };
|
|
492
|
-
commit(resolve(defaultMode, null));
|
|
393
|
+
dropCustom();
|
|
394
|
+
write(stored.theme, null);
|
|
395
|
+
write(stored.mode, null);
|
|
396
|
+
commit(fallback(), false);
|
|
493
397
|
void options.store?.clear().catch(() => undefined);
|
|
494
398
|
},
|
|
495
399
|
setOmarchyTheme: async (theme) => {
|
|
496
|
-
|
|
497
|
-
const followed = [
|
|
498
|
-
picks.light,
|
|
499
|
-
picks.dark,
|
|
500
|
-
read(stored.theme),
|
|
501
|
-
read(stored.dark),
|
|
502
|
-
].includes(omarchyThemeId);
|
|
503
|
-
|
|
504
|
-
const unpicked =
|
|
505
|
-
options.followOmarchy !== false && read(stored.theme) === null;
|
|
506
|
-
|
|
507
|
-
let mode: ThemeScheme | undefined;
|
|
400
|
+
let next: ThemeChoice | null = null;
|
|
508
401
|
|
|
509
402
|
if (theme !== null) {
|
|
510
403
|
const { Result } = await import("effect");
|
|
@@ -523,42 +416,32 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
523
416
|
? runtime.css
|
|
524
417
|
: `${runtime.css}\n\n${appTonesCss(options.tones, runtime.theme)}`;
|
|
525
418
|
|
|
526
|
-
mode = parsed.success.mode;
|
|
527
419
|
setStyle(omarchyThemeStyleId, css);
|
|
528
420
|
write(storedOmarchy.css, css);
|
|
529
421
|
write(storedOmarchy.name, name);
|
|
530
|
-
write(storedOmarchy.mode, mode);
|
|
531
|
-
|
|
422
|
+
write(storedOmarchy.mode, parsed.success.mode);
|
|
423
|
+
next = { id: omarchyThemeId, name, mode: parsed.success.mode };
|
|
532
424
|
}
|
|
533
425
|
}
|
|
534
426
|
|
|
535
|
-
if (
|
|
536
|
-
// Gone, or a colors.toml that doesn't map
|
|
537
|
-
// pick so it comes back with the theme
|
|
427
|
+
if (next === null) {
|
|
428
|
+
// Gone, or a colors.toml that doesn't map
|
|
538
429
|
setStyle(omarchyThemeStyleId, null);
|
|
539
430
|
|
|
540
431
|
for (const key of Object.values(storedOmarchy)) write(key, null);
|
|
541
|
-
|
|
542
|
-
omarchy = null;
|
|
543
|
-
picks = {
|
|
544
|
-
light:
|
|
545
|
-
picks.light === omarchyThemeId ? fallback("light") : picks.light,
|
|
546
|
-
dark: picks.dark === omarchyThemeId ? fallback("dark") : picks.dark,
|
|
547
|
-
};
|
|
548
|
-
} else if (followed || unpicked) {
|
|
549
|
-
// A switch between a light and a dark Omarchy theme moves it across
|
|
550
|
-
const keep = (scheme: ThemeScheme) =>
|
|
551
|
-
picks[scheme] === omarchyThemeId ? fallback(scheme) : picks[scheme];
|
|
552
|
-
|
|
553
|
-
picks =
|
|
554
|
-
mode === "dark"
|
|
555
|
-
? { light: keep("light"), dark: omarchyThemeId }
|
|
556
|
-
: { light: omarchyThemeId, dark: keep("dark") };
|
|
557
|
-
|
|
558
|
-
if (!unpicked) savePicks();
|
|
559
432
|
}
|
|
560
433
|
|
|
561
|
-
|
|
434
|
+
omarchy = next;
|
|
435
|
+
|
|
436
|
+
// While it's picked, a switch between a light and a dark Omarchy
|
|
437
|
+
// theme moves the page across; once it's gone, the default takes over
|
|
438
|
+
commit(
|
|
439
|
+
state.theme !== omarchyThemeId
|
|
440
|
+
? resolve(state.theme, state.mode)
|
|
441
|
+
: next === null
|
|
442
|
+
? fallback()
|
|
443
|
+
: resolve(omarchyThemeId, state.mode),
|
|
444
|
+
);
|
|
562
445
|
},
|
|
563
446
|
subscribe: (listener) => {
|
|
564
447
|
listeners.add(listener);
|
|
@@ -569,10 +452,14 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
569
452
|
},
|
|
570
453
|
};
|
|
571
454
|
|
|
572
|
-
|
|
455
|
+
// A stored pick is saved back, in case its mode had to move to fit the
|
|
456
|
+
// theme; with nothing stored, the defaults stay unsaved so a new default
|
|
457
|
+
// reaches the visitor
|
|
458
|
+
commit(state, read(stored.theme) !== null);
|
|
573
459
|
|
|
574
|
-
// The store's theme wins over this browser's
|
|
575
|
-
// changed or dropped it
|
|
460
|
+
// The store's theme wins over this browser's, since another client may
|
|
461
|
+
// have changed or dropped it. It's offered, and used only when this
|
|
462
|
+
// browser had the visitor's own theme picked
|
|
576
463
|
if (options.store !== undefined) {
|
|
577
464
|
void options.store
|
|
578
465
|
.load()
|
|
@@ -580,19 +467,12 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
580
467
|
if (text === read(stored.document)) return;
|
|
581
468
|
|
|
582
469
|
if (text === null) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
picks = {
|
|
590
|
-
light:
|
|
591
|
-
picks.light === customThemeId ? fallback("light") : picks.light,
|
|
592
|
-
dark: picks.dark === customThemeId ? fallback("dark") : picks.dark,
|
|
593
|
-
};
|
|
594
|
-
savePicks();
|
|
595
|
-
commit(resolve(state.mode, null));
|
|
470
|
+
dropCustom();
|
|
471
|
+
commit(
|
|
472
|
+
state.theme === customThemeId
|
|
473
|
+
? fallback()
|
|
474
|
+
: resolve(state.theme, state.mode),
|
|
475
|
+
);
|
|
596
476
|
|
|
597
477
|
return;
|
|
598
478
|
}
|
|
@@ -601,7 +481,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
601
481
|
const parsed = await readTheme(text);
|
|
602
482
|
|
|
603
483
|
if (Result.isSuccess(parsed)) {
|
|
604
|
-
await applyCustom(parsed.success, text, false);
|
|
484
|
+
await applyCustom(parsed.success, text, { pick: false, keep: false });
|
|
605
485
|
}
|
|
606
486
|
})
|
|
607
487
|
.catch(() => undefined);
|
|
@@ -628,7 +508,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
628
508
|
},
|
|
629
509
|
apply: ({ document: theme, text }) => {
|
|
630
510
|
if (documents.includes(theme)) setTheme(theme.id);
|
|
631
|
-
else void applyCustom(theme, text);
|
|
511
|
+
else void applyCustom(theme, text, { pick: true, keep: true });
|
|
632
512
|
},
|
|
633
513
|
});
|
|
634
514
|
},
|