@timmo001/groundwork-tokens 0.9.31 → 0.9.33
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/package.json +4 -3
- package/src/generate.ts +2 -1
- package/src/omarchy-current.ts +110 -0
- package/src/omarchy.ts +9 -0
- package/src/page.ts +161 -40
- package/src/runtime.ts +3 -1
package/README.md
CHANGED
|
@@ -31,5 +31,16 @@ their own:
|
|
|
31
31
|
import { parseTheme, runtimeTheme } from "@timmo001/groundwork-tokens/runtime";
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
The `omarchy-current` export reads and watches the Omarchy theme a machine
|
|
35
|
+
is using, for apps outside a browser, such as a desktop app's main process or
|
|
36
|
+
a terminal UI. `pageTheme`'s `setOmarchyTheme` offers it in the view:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import {
|
|
40
|
+
readOmarchyTheme,
|
|
41
|
+
watchOmarchyTheme,
|
|
42
|
+
} from "@timmo001/groundwork-tokens/omarchy-current";
|
|
43
|
+
```
|
|
44
|
+
|
|
34
45
|
The themes, their roles and contrast targets are documented at
|
|
35
46
|
[groundwork.timmo.dev/themes](https://groundwork.timmo.dev/themes).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-tokens",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.33",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./runtime": "./src/runtime.ts",
|
|
10
10
|
"./page": "./src/page.ts",
|
|
11
11
|
"./webmcp": "./src/webmcp.ts",
|
|
12
|
+
"./omarchy-current": "./src/omarchy-current.ts",
|
|
12
13
|
"./opentui": "./generated/opentui.ts",
|
|
13
14
|
"./themes.css": "./generated/themes.css",
|
|
14
15
|
"./themes.json": "./generated/themes.json",
|
|
@@ -47,10 +48,10 @@
|
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@types/culori": "4.0.1",
|
|
49
50
|
"culori": "4.0.2",
|
|
50
|
-
"effect": "4.1.
|
|
51
|
+
"effect": "4.1.2"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@effect/platform-node": "4.1.
|
|
54
|
+
"@effect/platform-node": "4.1.2",
|
|
54
55
|
"@oxlint/plugins": "1.90.0",
|
|
55
56
|
"@timmo001/oxlint-rules": "0.6.3",
|
|
56
57
|
"@types/bun": "1.4.4",
|
package/src/generate.ts
CHANGED
|
@@ -554,7 +554,8 @@ const hexRoles = (roles: Roles) =>
|
|
|
554
554
|
Object.entries(roles).map(([role, color]) => [role, toHex(color)]),
|
|
555
555
|
);
|
|
556
556
|
|
|
557
|
-
|
|
557
|
+
/** A solved theme's roles as hex in each mode it has, as terminals take them. */
|
|
558
|
+
export const hexModes = (theme: ResolvedTheme) =>
|
|
558
559
|
Object.fromEntries(
|
|
559
560
|
modes.flatMap((mode) => {
|
|
560
561
|
const roles = theme.modes[mode];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Omarchy theme a machine is using, for apps that run outside a browser,
|
|
3
|
+
* such as a desktop app's main process or a terminal UI. It reads the theme's
|
|
4
|
+
* name and `colors.toml` from Omarchy's state folder and watches for a
|
|
5
|
+
* switch. It needs a file system, so it's an entry of its own; map the colors
|
|
6
|
+
* with `omarchyTheme` from the runtime entry.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { watch, type FSWatcher } from "node:fs";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import type { OmarchyCurrentTheme } from "./omarchy.ts";
|
|
16
|
+
|
|
17
|
+
export type { OmarchyCurrentTheme } from "./omarchy.ts";
|
|
18
|
+
|
|
19
|
+
/** Omarchy's `current` folder, in `$XDG_STATE_HOME`, by default `~/.local/state`. */
|
|
20
|
+
export const omarchyCurrentPath = () =>
|
|
21
|
+
join(
|
|
22
|
+
process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"),
|
|
23
|
+
"omarchy",
|
|
24
|
+
"current",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// `tokyo-night` reads as `Tokyo Night`, as `omarchy theme current` shows it
|
|
28
|
+
const displayName = (slug: string) =>
|
|
29
|
+
slug
|
|
30
|
+
.trim()
|
|
31
|
+
.split("-")
|
|
32
|
+
.filter((word) => word !== "")
|
|
33
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
34
|
+
.join(" ");
|
|
35
|
+
|
|
36
|
+
/** The Omarchy theme in use, or `null` where there's no Omarchy. */
|
|
37
|
+
export const readOmarchyTheme =
|
|
38
|
+
async (): Promise<OmarchyCurrentTheme | null> => {
|
|
39
|
+
const current = omarchyCurrentPath();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const colors = await readFile(
|
|
43
|
+
join(current, "theme", "colors.toml"),
|
|
44
|
+
"utf8",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const slug = await readFile(join(current, "theme.name"), "utf8").catch(
|
|
48
|
+
() => "",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return { name: displayName(slug) || "Omarchy", colors };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const watchFolder = (path: string, onEvent: () => void) => {
|
|
58
|
+
try {
|
|
59
|
+
return watch(path, onEvent).on("error", () => undefined);
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calls `onChange` with the theme each time Omarchy switches it, or with
|
|
67
|
+
* `null` when it goes. Returns a function that stops watching.
|
|
68
|
+
*/
|
|
69
|
+
export const watchOmarchyTheme = (
|
|
70
|
+
onChange: (theme: OmarchyCurrentTheme | null) => void,
|
|
71
|
+
): (() => void) => {
|
|
72
|
+
const current = omarchyCurrentPath();
|
|
73
|
+
let last: string | undefined;
|
|
74
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
75
|
+
let themeWatcher: FSWatcher | undefined;
|
|
76
|
+
|
|
77
|
+
void readOmarchyTheme().then((theme) => {
|
|
78
|
+
last ??= JSON.stringify(theme);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// A switch replaces the theme folder, so its watcher is armed again each time
|
|
82
|
+
const armTheme = () => {
|
|
83
|
+
themeWatcher?.close();
|
|
84
|
+
themeWatcher = watchFolder(join(current, "theme"), changed);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// A switch writes several files; read once they've settled
|
|
88
|
+
function changed() {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
timer = setTimeout(async () => {
|
|
91
|
+
armTheme();
|
|
92
|
+
const theme = await readOmarchyTheme();
|
|
93
|
+
const key = JSON.stringify(theme);
|
|
94
|
+
|
|
95
|
+
if (key === last) return;
|
|
96
|
+
last = key;
|
|
97
|
+
onChange(theme);
|
|
98
|
+
}, 250);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const currentWatcher = watchFolder(current, changed);
|
|
102
|
+
|
|
103
|
+
armTheme();
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
currentWatcher?.close();
|
|
108
|
+
themeWatcher?.close();
|
|
109
|
+
};
|
|
110
|
+
};
|
package/src/omarchy.ts
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
import { Schema } from "effect";
|
|
9
9
|
import { MappedTheme } from "./schema.ts";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The Omarchy theme a machine is using: its name, such as `Tokyo Night`,
|
|
13
|
+
* and the text of its `colors.toml`.
|
|
14
|
+
*/
|
|
15
|
+
export interface OmarchyCurrentTheme {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly colors: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
/** The `key = "value"` pairs of a flat TOML file, ignoring comments. */
|
|
12
21
|
const readPairs = (text: string): ReadonlyMap<string, string> => {
|
|
13
22
|
const pairs = new Map<string, string>();
|
package/src/page.ts
CHANGED
|
@@ -15,6 +15,7 @@ import glazing from "../themes/glazing.json" with { type: "json" };
|
|
|
15
15
|
import slate from "../themes/slate.json" with { type: "json" };
|
|
16
16
|
import snag from "../themes/snag.json" with { type: "json" };
|
|
17
17
|
import survey from "../themes/survey.json" with { type: "json" };
|
|
18
|
+
import type { OmarchyCurrentTheme } from "./omarchy.ts";
|
|
18
19
|
import type { ThemeStore } from "./runtime.ts";
|
|
19
20
|
import type { Theme } from "./schema.ts";
|
|
20
21
|
// Only for `document.modelContext`'s type; the tools themselves load lazily
|
|
@@ -29,6 +30,12 @@ export const customThemeId = "custom";
|
|
|
29
30
|
/** The id of the `<style>` holding a custom theme's CSS. */
|
|
30
31
|
export const customThemeStyleId = "gw-custom-theme";
|
|
31
32
|
|
|
33
|
+
/** The theme id the desktop's Omarchy theme is solved under. */
|
|
34
|
+
export const omarchyThemeId = "omarchy";
|
|
35
|
+
|
|
36
|
+
/** The id of the `<style>` holding the Omarchy theme's CSS. */
|
|
37
|
+
export const omarchyThemeStyleId = "gw-omarchy-theme";
|
|
38
|
+
|
|
32
39
|
const modes: readonly ThemeMode[] = ["auto", "light", "dark", "darker"];
|
|
33
40
|
|
|
34
41
|
/** Light or dark: the side of a mode a theme has to cover. Darker is dark. */
|
|
@@ -42,6 +49,8 @@ export interface PageThemeState {
|
|
|
42
49
|
readonly theme: string;
|
|
43
50
|
/** The visitor's own theme's name, when they've imported one. */
|
|
44
51
|
readonly custom: string | null;
|
|
52
|
+
/** The desktop's Omarchy theme as a choice, when the app passed one. */
|
|
53
|
+
readonly omarchy: ThemeChoice | null;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/** What `importTheme` reports: the theme's name and any contrast misses, or why it failed. */
|
|
@@ -117,6 +126,15 @@ export interface PageTheme {
|
|
|
117
126
|
readonly importTheme: (text: string) => Promise<ImportResult>;
|
|
118
127
|
/** Drops the visitor's own theme and goes back to the defaults. */
|
|
119
128
|
readonly reset: () => void;
|
|
129
|
+
/**
|
|
130
|
+
* Offers the Omarchy theme the app runs under, or drops it with `null`.
|
|
131
|
+
* It's used for its scheme until the visitor picks another, and a theme
|
|
132
|
+
* switch on the desktop follows through while it's picked. Apps that can
|
|
133
|
+
* read it, such as desktop apps, pass it on start and on every switch.
|
|
134
|
+
*/
|
|
135
|
+
readonly setOmarchyTheme: (
|
|
136
|
+
theme: OmarchyCurrentTheme | null,
|
|
137
|
+
) => Promise<void>;
|
|
120
138
|
/** Calls `listener` on every change; returns a function that stops it. */
|
|
121
139
|
readonly subscribe: (listener: (state: PageThemeState) => void) => () => void;
|
|
122
140
|
}
|
|
@@ -130,6 +148,13 @@ const keys = (key: string) => ({
|
|
|
130
148
|
name: `${key}:custom-name`,
|
|
131
149
|
});
|
|
132
150
|
|
|
151
|
+
/** The Omarchy theme's cache, kept apart so a reset doesn't drop it. */
|
|
152
|
+
const omarchyKeys = (key: string) => ({
|
|
153
|
+
css: `${key}:omarchy-css`,
|
|
154
|
+
name: `${key}:omarchy-name`,
|
|
155
|
+
mode: `${key}:omarchy-mode`,
|
|
156
|
+
});
|
|
157
|
+
|
|
133
158
|
const read = (key: string) => {
|
|
134
159
|
try {
|
|
135
160
|
return localStorage.getItem(key);
|
|
@@ -148,24 +173,26 @@ const write = (key: string, value: string | null) => {
|
|
|
148
173
|
};
|
|
149
174
|
|
|
150
175
|
/**
|
|
151
|
-
* An inline script that applies the stored mode, theme and any custom
|
|
152
|
-
* cached CSS before the first paint. Takes the same key and
|
|
153
|
-
* `pageTheme`.
|
|
176
|
+
* An inline script that applies the stored mode, theme and any custom or
|
|
177
|
+
* Omarchy theme's cached CSS before the first paint. Takes the same key and
|
|
178
|
+
* defaults as `pageTheme`.
|
|
154
179
|
*/
|
|
155
180
|
export const themeBootScript = (
|
|
156
181
|
options: Pick<PageThemeOptions, "key" | "defaultTheme" | "defaultMode">,
|
|
157
182
|
) => {
|
|
158
|
-
const stored = keys(options.key);
|
|
159
|
-
|
|
160
183
|
const config = JSON.stringify({
|
|
161
|
-
keys:
|
|
184
|
+
keys: keys(options.key),
|
|
185
|
+
omarchy: omarchyKeys(options.key),
|
|
162
186
|
theme: options.defaultTheme,
|
|
163
187
|
mode: options.defaultMode ?? "auto",
|
|
164
188
|
custom: customThemeId,
|
|
165
189
|
style: customThemeStyleId,
|
|
190
|
+
omarchyId: omarchyThemeId,
|
|
191
|
+
omarchyStyle: omarchyThemeStyleId,
|
|
166
192
|
});
|
|
167
193
|
|
|
168
|
-
|
|
194
|
+
// With nothing picked, a cached Omarchy theme for the scheme is the default
|
|
195
|
+
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)??(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)})();`;
|
|
169
196
|
};
|
|
170
197
|
|
|
171
198
|
/** Reads a theme from JSON text or an Omarchy `colors.toml`. */
|
|
@@ -187,6 +214,7 @@ const readTheme = async (text: string) => {
|
|
|
187
214
|
*/
|
|
188
215
|
export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
189
216
|
const stored = keys(options.key);
|
|
217
|
+
const storedOmarchy = omarchyKeys(options.key);
|
|
190
218
|
const root = options.root ?? document.documentElement;
|
|
191
219
|
const defaultMode = options.defaultMode ?? "auto";
|
|
192
220
|
const listeners = new Set<(state: PageThemeState) => void>();
|
|
@@ -197,6 +225,49 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
197
225
|
const customName = read(stored.name);
|
|
198
226
|
const customCss = read(stored.css);
|
|
199
227
|
|
|
228
|
+
const setStyle = (id: string, css: string | null) => {
|
|
229
|
+
let style = document.getElementById(id);
|
|
230
|
+
|
|
231
|
+
if (css === null) {
|
|
232
|
+
style?.remove();
|
|
233
|
+
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (style === null) {
|
|
238
|
+
style = document.createElement("style");
|
|
239
|
+
style.id = id;
|
|
240
|
+
document.head.append(style);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
style.textContent = css;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** The Omarchy theme as a choice, from the last one the app passed. */
|
|
247
|
+
let omarchy: ThemeChoice | null = null;
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
const css = read(storedOmarchy.css);
|
|
251
|
+
const name = read(storedOmarchy.name);
|
|
252
|
+
const mode = read(storedOmarchy.mode);
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
css !== null &&
|
|
256
|
+
name !== null &&
|
|
257
|
+
(mode === "light" || mode === "dark")
|
|
258
|
+
) {
|
|
259
|
+
omarchy = { id: omarchyThemeId, name, mode };
|
|
260
|
+
|
|
261
|
+
// The boot script may have put it on already
|
|
262
|
+
if (document.getElementById(omarchyThemeStyleId) === null) {
|
|
263
|
+
setStyle(omarchyThemeStyleId, css);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const choices = () =>
|
|
269
|
+
omarchy === null ? options.themes : [...options.themes, omarchy];
|
|
270
|
+
|
|
200
271
|
const system = matchMedia("(prefers-color-scheme: dark)");
|
|
201
272
|
|
|
202
273
|
const schemeOf = (mode: ThemeMode): ThemeScheme =>
|
|
@@ -210,18 +281,20 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
210
281
|
const fits = (id: string | null, scheme: ThemeScheme, custom: boolean) =>
|
|
211
282
|
id !== null &&
|
|
212
283
|
((id === customThemeId && custom) ||
|
|
213
|
-
|
|
214
|
-
(theme) => theme.id === id && themeCovers(theme, scheme),
|
|
215
|
-
));
|
|
284
|
+
choices().some((theme) => theme.id === id && themeCovers(theme, scheme)));
|
|
216
285
|
|
|
286
|
+
// The Omarchy theme comes first where it covers the scheme, so the app
|
|
287
|
+
// matches the desktop until the visitor picks another
|
|
217
288
|
const fallback = (scheme: ThemeScheme) =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
options.defaultTheme
|
|
289
|
+
omarchy !== null && themeCovers(omarchy, scheme)
|
|
290
|
+
? omarchy.id
|
|
291
|
+
: options.themes.some(
|
|
292
|
+
(theme) =>
|
|
293
|
+
theme.id === options.defaultTheme && themeCovers(theme, scheme),
|
|
294
|
+
)
|
|
295
|
+
? options.defaultTheme
|
|
296
|
+
: (options.themes.find((theme) => themeCovers(theme, scheme))?.id ??
|
|
297
|
+
options.defaultTheme);
|
|
225
298
|
|
|
226
299
|
const storedLight = read(stored.theme);
|
|
227
300
|
const storedDark = read(stored.dark) ?? storedLight;
|
|
@@ -244,7 +317,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
244
317
|
const resolve = (mode: ThemeMode, custom: string | null): PageThemeState => {
|
|
245
318
|
const scheme = schemeOf(mode);
|
|
246
319
|
|
|
247
|
-
return { mode, scheme, theme: picks[scheme], custom };
|
|
320
|
+
return { mode, scheme, theme: picks[scheme], custom, omarchy };
|
|
248
321
|
};
|
|
249
322
|
|
|
250
323
|
let state: PageThemeState = resolve(
|
|
@@ -252,24 +325,6 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
252
325
|
customCss === null ? null : customName,
|
|
253
326
|
);
|
|
254
327
|
|
|
255
|
-
const setStyle = (css: string | null) => {
|
|
256
|
-
let style = document.getElementById(customThemeStyleId);
|
|
257
|
-
|
|
258
|
-
if (css === null) {
|
|
259
|
-
style?.remove();
|
|
260
|
-
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (style === null) {
|
|
265
|
-
style = document.createElement("style");
|
|
266
|
-
style.id = customThemeStyleId;
|
|
267
|
-
document.head.append(style);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
style.textContent = css;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
328
|
const commit = (next: PageThemeState) => {
|
|
274
329
|
state = next;
|
|
275
330
|
root.setAttribute("data-gw-theme", state.theme);
|
|
@@ -290,7 +345,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
290
345
|
? runtime.css
|
|
291
346
|
: `${runtime.css}\n\n${appTonesCss(options.tones, runtime.theme)}`;
|
|
292
347
|
|
|
293
|
-
setStyle(css);
|
|
348
|
+
setStyle(customThemeStyleId, css);
|
|
294
349
|
write(stored.document, text);
|
|
295
350
|
write(stored.css, css);
|
|
296
351
|
write(stored.name, document.name);
|
|
@@ -314,7 +369,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
314
369
|
|
|
315
370
|
picks = { light: id, dark: id };
|
|
316
371
|
} else {
|
|
317
|
-
const choice =
|
|
372
|
+
const choice = choices().find((theme) => theme.id === id);
|
|
318
373
|
|
|
319
374
|
if (choice === undefined) return;
|
|
320
375
|
|
|
@@ -365,7 +420,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
365
420
|
return { ok: true, name: parsed.success.name, misses };
|
|
366
421
|
},
|
|
367
422
|
reset: () => {
|
|
368
|
-
setStyle(null);
|
|
423
|
+
setStyle(customThemeStyleId, null);
|
|
369
424
|
|
|
370
425
|
for (const key of Object.values(stored)) write(key, null);
|
|
371
426
|
|
|
@@ -373,6 +428,72 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
373
428
|
commit(resolve(defaultMode, null));
|
|
374
429
|
void options.store?.clear().catch(() => undefined);
|
|
375
430
|
},
|
|
431
|
+
setOmarchyTheme: async (theme) => {
|
|
432
|
+
// Stored as well as in use, so it's picked again when it comes back
|
|
433
|
+
const followed = [
|
|
434
|
+
picks.light,
|
|
435
|
+
picks.dark,
|
|
436
|
+
read(stored.theme),
|
|
437
|
+
read(stored.dark),
|
|
438
|
+
].includes(omarchyThemeId);
|
|
439
|
+
|
|
440
|
+
const unpicked = read(stored.theme) === null;
|
|
441
|
+
let mode: ThemeScheme | undefined;
|
|
442
|
+
|
|
443
|
+
if (theme !== null) {
|
|
444
|
+
const { Result } = await import("effect");
|
|
445
|
+
|
|
446
|
+
const { appTonesCss, omarchyTheme, runtimeTheme } =
|
|
447
|
+
await import("./runtime.ts");
|
|
448
|
+
|
|
449
|
+
const name = `Omarchy (${theme.name})`;
|
|
450
|
+
const parsed = omarchyTheme(theme.colors, name, omarchyThemeId);
|
|
451
|
+
|
|
452
|
+
if (Result.isSuccess(parsed)) {
|
|
453
|
+
const runtime = runtimeTheme(parsed.success);
|
|
454
|
+
|
|
455
|
+
const css =
|
|
456
|
+
options.tones === undefined
|
|
457
|
+
? runtime.css
|
|
458
|
+
: `${runtime.css}\n\n${appTonesCss(options.tones, runtime.theme)}`;
|
|
459
|
+
|
|
460
|
+
mode = parsed.success.mode;
|
|
461
|
+
setStyle(omarchyThemeStyleId, css);
|
|
462
|
+
write(storedOmarchy.css, css);
|
|
463
|
+
write(storedOmarchy.name, name);
|
|
464
|
+
write(storedOmarchy.mode, mode);
|
|
465
|
+
omarchy = { id: omarchyThemeId, name, mode };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (mode === undefined) {
|
|
470
|
+
// Gone, or a colors.toml that doesn't map: back off it, keeping the
|
|
471
|
+
// pick so it comes back with the theme
|
|
472
|
+
setStyle(omarchyThemeStyleId, null);
|
|
473
|
+
|
|
474
|
+
for (const key of Object.values(storedOmarchy)) write(key, null);
|
|
475
|
+
|
|
476
|
+
omarchy = null;
|
|
477
|
+
picks = {
|
|
478
|
+
light:
|
|
479
|
+
picks.light === omarchyThemeId ? fallback("light") : picks.light,
|
|
480
|
+
dark: picks.dark === omarchyThemeId ? fallback("dark") : picks.dark,
|
|
481
|
+
};
|
|
482
|
+
} else if (followed || unpicked) {
|
|
483
|
+
// A switch between a light and a dark Omarchy theme moves it across
|
|
484
|
+
const keep = (scheme: ThemeScheme) =>
|
|
485
|
+
picks[scheme] === omarchyThemeId ? fallback(scheme) : picks[scheme];
|
|
486
|
+
|
|
487
|
+
picks =
|
|
488
|
+
mode === "dark"
|
|
489
|
+
? { light: keep("light"), dark: omarchyThemeId }
|
|
490
|
+
: { light: omarchyThemeId, dark: keep("dark") };
|
|
491
|
+
|
|
492
|
+
if (!unpicked) savePicks();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
commit(resolve(state.mode, state.custom));
|
|
496
|
+
},
|
|
376
497
|
subscribe: (listener) => {
|
|
377
498
|
listeners.add(listener);
|
|
378
499
|
|
|
@@ -393,7 +514,7 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
393
514
|
if (text === read(stored.document)) return;
|
|
394
515
|
|
|
395
516
|
if (text === null) {
|
|
396
|
-
setStyle(null);
|
|
517
|
+
setStyle(customThemeStyleId, null);
|
|
397
518
|
|
|
398
519
|
for (const key of [stored.document, stored.css, stored.name]) {
|
|
399
520
|
write(key, null);
|
package/src/runtime.ts
CHANGED
|
@@ -12,7 +12,9 @@ import { SolvedTheme, Theme } from "./schema.ts";
|
|
|
12
12
|
import { check, resolve, type Check, type ResolvedTheme } from "./solve.ts";
|
|
13
13
|
import { appTonesCss, type AppTones } from "./tones.ts";
|
|
14
14
|
|
|
15
|
-
export { omarchyTheme } from "./omarchy.ts";
|
|
15
|
+
export { omarchyTheme, type OmarchyCurrentTheme } from "./omarchy.ts";
|
|
16
|
+
|
|
17
|
+
export { hexModes } from "./generate.ts";
|
|
16
18
|
|
|
17
19
|
export { appTonesCss, type AppTones } from "./tones.ts";
|
|
18
20
|
|