@timmo001/groundwork-tokens 0.9.38 → 0.9.40

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 CHANGED
@@ -55,5 +55,16 @@ import {
55
55
  } from "@timmo001/groundwork-tokens/desktop-themes";
56
56
  ```
57
57
 
58
+ The `choices` export holds the built-in themes as choices, the optional
59
+ themes' documents and `themeBootScript`, with no DOM types, for a server or
60
+ CLI that writes a page. The `page` export re-exports it beside `pageTheme`:
61
+
62
+ ```ts
63
+ import {
64
+ optionalThemeChoices,
65
+ themeBootScript,
66
+ } from "@timmo001/groundwork-tokens/choices";
67
+ ```
68
+
58
69
  The themes, their roles and contrast targets are documented at
59
70
  [groundwork.timmo.dev/themes](https://groundwork.timmo.dev/themes).
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-tokens",
3
- "version": "0.9.38",
3
+ "version": "0.9.40",
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",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
9
  "./runtime": "./src/runtime.ts",
10
+ "./choices": "./src/choices.ts",
10
11
  "./page": "./src/page.ts",
11
12
  "./webmcp": "./src/webmcp.ts",
12
13
  "./omarchy-current": "./src/omarchy-current.ts",
package/src/choices.ts ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The built-in themes as choices, the optional themes' documents and the
3
+ * inline script that applies a stored pick before the first paint. It uses
4
+ * no DOM types, so a server or CLI building a page can import it without
5
+ * the DOM lib; `page` re-exports all of it for the browser.
6
+ *
7
+ * @module
8
+ */
9
+ import base from "../themes/base.json" with { type: "json" };
10
+ import bridge from "../themes/bridge.json" with { type: "json" };
11
+ import catppuccinFrappe from "../themes/catppuccin-frappe.json" with { type: "json" };
12
+ import catppuccinLatte from "../themes/catppuccin-latte.json" with { type: "json" };
13
+ import catppuccinMacchiato from "../themes/catppuccin-macchiato.json" with { type: "json" };
14
+ import catppuccinMocha from "../themes/catppuccin-mocha.json" with { type: "json" };
15
+ import githubDimmed from "../themes/github-dimmed.json" with { type: "json" };
16
+ import githubHighContrast from "../themes/github-high-contrast.json" with { type: "json" };
17
+ import github from "../themes/github.json" with { type: "json" };
18
+ import glazing from "../themes/glazing.json" with { type: "json" };
19
+ import slate from "../themes/slate.json" with { type: "json" };
20
+ import snag from "../themes/snag.json" with { type: "json" };
21
+ import survey from "../themes/survey.json" with { type: "json" };
22
+ import tokyoNightDay from "../themes/tokyo-night-day.json" with { type: "json" };
23
+ import tokyoNightMoon from "../themes/tokyo-night-moon.json" with { type: "json" };
24
+ import tokyoNightStorm from "../themes/tokyo-night-storm.json" with { type: "json" };
25
+ import tokyoNight from "../themes/tokyo-night.json" with { type: "json" };
26
+
27
+ /** How light the page is: `auto` follows the system. */
28
+ export type ThemeMode = "auto" | "light" | "dark" | "darker";
29
+
30
+ /** Light or dark: the side of a mode a theme has to cover. Darker is dark. */
31
+ export type ThemeScheme = "light" | "dark";
32
+
33
+ /** The theme id a visitor's own theme is solved under. */
34
+ export const customThemeId = "custom";
35
+
36
+ /** The id of the `<style>` holding a custom theme's CSS. */
37
+ export const customThemeStyleId = "gw-custom-theme";
38
+
39
+ /** The theme id the desktop's Omarchy theme is solved under. */
40
+ export const omarchyThemeId = "omarchy";
41
+
42
+ /** The id of the `<style>` holding the Omarchy theme's CSS. */
43
+ export const omarchyThemeStyleId = "gw-omarchy-theme";
44
+
45
+ /**
46
+ * A theme to offer, by id and name. A `Theme` document is one. `mode` marks
47
+ * a theme with only one scheme, such as Catppuccin Latte: it's offered and
48
+ * kept for that scheme alone, beside a pick for the other.
49
+ */
50
+ export interface ThemeChoice {
51
+ readonly id: string;
52
+ readonly name: string;
53
+ readonly mode?: ThemeScheme | undefined;
54
+ /** The palette an optional theme comes from, such as `Catppuccin`. */
55
+ readonly family?: string | undefined;
56
+ }
57
+
58
+ /** Whether `choice` can show in `scheme`. */
59
+ export const themeCovers = (choice: ThemeChoice, scheme: ThemeScheme) =>
60
+ choice.mode === undefined || choice.mode === scheme;
61
+
62
+ /** The core themes by id and name, in the order the docs list them. */
63
+ export const coreThemeChoices: readonly ThemeChoice[] = [
64
+ base,
65
+ glazing,
66
+ survey,
67
+ slate,
68
+ bridge,
69
+ snag,
70
+ ].map(({ id, name }) => ({ id, name }));
71
+
72
+ /**
73
+ * The optional themes' documents, by family and then from light to dark.
74
+ * They're bundled with the module, so an app compiled to one binary has them
75
+ * without reading `node_modules`. Decode one with the `MappedTheme` Schema.
76
+ */
77
+ export const optionalThemeDocuments = [
78
+ catppuccinLatte,
79
+ catppuccinFrappe,
80
+ catppuccinMacchiato,
81
+ catppuccinMocha,
82
+ tokyoNightDay,
83
+ tokyoNight,
84
+ tokyoNightStorm,
85
+ tokyoNightMoon,
86
+ github,
87
+ githubDimmed,
88
+ githubHighContrast,
89
+ ] as const;
90
+
91
+ /**
92
+ * The optional themes, mapped from other palettes, by family and then from
93
+ * light to dark. One with a single mode is offered for that scheme alone;
94
+ * one with both, such as GitHub's, for either.
95
+ */
96
+ export const optionalThemeChoices: readonly ThemeChoice[] =
97
+ optionalThemeDocuments.map((theme) => ({
98
+ id: theme.id,
99
+ name: theme.name,
100
+ family: theme.family,
101
+ mode:
102
+ "otherPalette" in theme
103
+ ? undefined
104
+ : theme.mode === "light"
105
+ ? "light"
106
+ : "dark",
107
+ }));
108
+
109
+ /** What the page's pick starts on, and where it's kept. */
110
+ export interface ThemeBootOptions {
111
+ /** The local storage key prefix, such as `my-site:theme`. */
112
+ readonly key: string;
113
+ /** The theme id to start with. */
114
+ readonly defaultTheme: string;
115
+ /** The mode to start with. Defaults to `auto`. */
116
+ readonly defaultMode?: ThemeMode;
117
+ /**
118
+ * Whether the Omarchy theme takes over while the visitor hasn't picked
119
+ * one. With `false` it's only offered, and `defaultTheme` stays in use.
120
+ * Defaults to `true`.
121
+ */
122
+ readonly followOmarchy?: boolean;
123
+ }
124
+
125
+ /** The local storage keys a pick is kept under. */
126
+ export const themeStorageKeys = (key: string) => ({
127
+ mode: `${key}:mode`,
128
+ theme: `${key}:id`,
129
+ dark: `${key}:id-dark`,
130
+ document: `${key}:custom`,
131
+ css: `${key}:custom-css`,
132
+ name: `${key}:custom-name`,
133
+ });
134
+
135
+ /** The Omarchy theme's cache, kept apart so a reset doesn't drop it. */
136
+ export const omarchyStorageKeys = (key: string) => ({
137
+ css: `${key}:omarchy-css`,
138
+ name: `${key}:omarchy-name`,
139
+ mode: `${key}:omarchy-mode`,
140
+ });
141
+
142
+ /**
143
+ * An inline script that applies the stored mode, theme and any custom or
144
+ * Omarchy theme's cached CSS before the first paint. Takes the same key and
145
+ * defaults as `pageTheme`.
146
+ */
147
+ export const themeBootScript = (options: ThemeBootOptions) => {
148
+ const config = JSON.stringify({
149
+ keys: themeStorageKeys(options.key),
150
+ omarchy: omarchyStorageKeys(options.key),
151
+ theme: options.defaultTheme,
152
+ mode: options.defaultMode ?? "auto",
153
+ follow: options.followOmarchy !== false,
154
+ custom: customThemeId,
155
+ style: customThemeStyleId,
156
+ omarchyId: omarchyThemeId,
157
+ omarchyStyle: omarchyThemeStyleId,
158
+ });
159
+
160
+ // With nothing picked, a cached Omarchy theme for the scheme is the
161
+ // default, unless it's only offered
162
+ 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)})();`;
163
+ };
package/src/page.ts CHANGED
@@ -9,49 +9,43 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- import base from "../themes/base.json" with { type: "json" };
13
- import bridge from "../themes/bridge.json" with { type: "json" };
14
- import catppuccinFrappe from "../themes/catppuccin-frappe.json" with { type: "json" };
15
- import catppuccinLatte from "../themes/catppuccin-latte.json" with { type: "json" };
16
- import catppuccinMacchiato from "../themes/catppuccin-macchiato.json" with { type: "json" };
17
- import catppuccinMocha from "../themes/catppuccin-mocha.json" with { type: "json" };
18
- import githubDimmed from "../themes/github-dimmed.json" with { type: "json" };
19
- import githubHighContrast from "../themes/github-high-contrast.json" with { type: "json" };
20
- import github from "../themes/github.json" with { type: "json" };
21
- import glazing from "../themes/glazing.json" with { type: "json" };
22
- import slate from "../themes/slate.json" with { type: "json" };
23
- import snag from "../themes/snag.json" with { type: "json" };
24
- import survey from "../themes/survey.json" with { type: "json" };
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
+ themeCovers,
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
- /** How light the page is: `auto` follows the system. */
36
- export type ThemeMode = "auto" | "light" | "dark" | "darker";
37
-
38
- /** The theme id a visitor's own theme is solved under. */
39
- export const customThemeId = "custom";
40
-
41
- /** The id of the `<style>` holding a custom theme's CSS. */
42
- export const customThemeStyleId = "gw-custom-theme";
43
-
44
- /** The theme id the desktop's Omarchy theme is solved under. */
45
- export const omarchyThemeId = "omarchy";
46
-
47
- /** The id of the `<style>` holding the Omarchy theme's CSS. */
48
- export const omarchyThemeStyleId = "gw-omarchy-theme";
31
+ export {
32
+ coreThemeChoices,
33
+ customThemeId,
34
+ customThemeStyleId,
35
+ omarchyThemeId,
36
+ omarchyThemeStyleId,
37
+ optionalThemeChoices,
38
+ optionalThemeDocuments,
39
+ themeBootScript,
40
+ themeCovers,
41
+ type ThemeBootOptions,
42
+ type ThemeChoice,
43
+ type ThemeMode,
44
+ type ThemeScheme,
45
+ } from "./choices.ts";
49
46
 
50
47
  const modes: readonly ThemeMode[] = ["auto", "light", "dark", "darker"];
51
48
 
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
49
  export interface PageThemeState {
56
50
  readonly mode: ThemeMode;
57
51
  /** The scheme in use, with `auto` resolved from the system. */
@@ -73,65 +67,7 @@ export type ImportResult =
73
67
  }
74
68
  | { readonly ok: false; readonly error: string };
75
69
 
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, mapped from other palettes, by family and then from
105
- * light to dark. One with a single mode is offered for that scheme alone;
106
- * one with both, such as GitHub's, for either.
107
- */
108
- export const optionalThemeChoices: readonly ThemeChoice[] = [
109
- catppuccinLatte,
110
- catppuccinFrappe,
111
- catppuccinMacchiato,
112
- catppuccinMocha,
113
- tokyoNightDay,
114
- tokyoNight,
115
- tokyoNightStorm,
116
- tokyoNightMoon,
117
- github,
118
- githubDimmed,
119
- githubHighContrast,
120
- ].map((theme) => ({
121
- id: theme.id,
122
- name: theme.name,
123
- family: theme.family,
124
- mode:
125
- "otherPalette" in theme
126
- ? undefined
127
- : theme.mode === "light"
128
- ? "light"
129
- : "dark",
130
- }));
131
-
132
- export interface PageThemeOptions {
133
- /** The local storage key prefix, such as `my-site:theme`. */
134
- readonly key: string;
70
+ export interface PageThemeOptions extends ThemeBootOptions {
135
71
  /** The built-in themes to offer, their CSS already on the page. */
136
72
  readonly themes: readonly ThemeChoice[];
137
73
  /**
@@ -139,16 +75,6 @@ export interface PageThemeOptions {
139
75
  * register. Defaults to the core themes.
140
76
  */
141
77
  readonly documents?: () => Promise<readonly Theme[]>;
142
- /** The theme id to start with. */
143
- readonly defaultTheme: string;
144
- /** The mode to start with. Defaults to `auto`. */
145
- readonly defaultMode?: ThemeMode;
146
- /**
147
- * Whether the Omarchy theme takes over while the visitor hasn't picked
148
- * one. With `false` it's only offered, and `defaultTheme` stays in use.
149
- * Defaults to `true`.
150
- */
151
- readonly followOmarchy?: boolean;
152
78
  /** The element the attributes go on. Defaults to `<html>`. */
153
79
  readonly root?: HTMLElement;
154
80
  /**
@@ -187,22 +113,6 @@ export interface PageTheme {
187
113
  readonly subscribe: (listener: (state: PageThemeState) => void) => () => void;
188
114
  }
189
115
 
190
- const keys = (key: string) => ({
191
- mode: `${key}:mode`,
192
- theme: `${key}:id`,
193
- dark: `${key}:id-dark`,
194
- document: `${key}:custom`,
195
- css: `${key}:custom-css`,
196
- name: `${key}:custom-name`,
197
- });
198
-
199
- /** The Omarchy theme's cache, kept apart so a reset doesn't drop it. */
200
- const omarchyKeys = (key: string) => ({
201
- css: `${key}:omarchy-css`,
202
- name: `${key}:omarchy-name`,
203
- mode: `${key}:omarchy-mode`,
204
- });
205
-
206
116
  const read = (key: string) => {
207
117
  try {
208
118
  return localStorage.getItem(key);
@@ -220,34 +130,6 @@ const write = (key: string, value: string | null) => {
220
130
  }
221
131
  };
222
132
 
223
- /**
224
- * An inline script that applies the stored mode, theme and any custom or
225
- * Omarchy theme's cached CSS before the first paint. Takes the same key and
226
- * defaults as `pageTheme`.
227
- */
228
- export const themeBootScript = (
229
- options: Pick<
230
- PageThemeOptions,
231
- "key" | "defaultTheme" | "defaultMode" | "followOmarchy"
232
- >,
233
- ) => {
234
- const config = JSON.stringify({
235
- keys: keys(options.key),
236
- omarchy: omarchyKeys(options.key),
237
- theme: options.defaultTheme,
238
- mode: options.defaultMode ?? "auto",
239
- follow: options.followOmarchy !== false,
240
- custom: customThemeId,
241
- style: customThemeStyleId,
242
- omarchyId: omarchyThemeId,
243
- omarchyStyle: omarchyThemeStyleId,
244
- });
245
-
246
- // With nothing picked, a cached Omarchy theme for the scheme is the
247
- // default, unless it's only offered
248
- 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)})();`;
249
- };
250
-
251
133
  /** Reads a theme from JSON text or an Omarchy `colors.toml`. */
252
134
  const readTheme = async (text: string) => {
253
135
  const { Result } = await import("effect");
@@ -266,8 +148,8 @@ const readTheme = async (text: string) => {
266
148
  * and registers the WebMCP theme tools where the browser offers them.
267
149
  */
268
150
  export const pageTheme = (options: PageThemeOptions): PageTheme => {
269
- const stored = keys(options.key);
270
- const storedOmarchy = omarchyKeys(options.key);
151
+ const stored = themeStorageKeys(options.key);
152
+ const storedOmarchy = omarchyStorageKeys(options.key);
271
153
  const root = options.root ?? document.documentElement;
272
154
  const defaultMode = options.defaultMode ?? "auto";
273
155
  const listeners = new Set<(state: PageThemeState) => void>();