@timmo001/groundwork-tokens 0.9.39 → 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.39",
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,73 +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' 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;
70
+ export interface PageThemeOptions extends ThemeBootOptions {
143
71
  /** The built-in themes to offer, their CSS already on the page. */
144
72
  readonly themes: readonly ThemeChoice[];
145
73
  /**
@@ -147,16 +75,6 @@ export interface PageThemeOptions {
147
75
  * register. Defaults to the core themes.
148
76
  */
149
77
  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
78
  /** The element the attributes go on. Defaults to `<html>`. */
161
79
  readonly root?: HTMLElement;
162
80
  /**
@@ -195,22 +113,6 @@ export interface PageTheme {
195
113
  readonly subscribe: (listener: (state: PageThemeState) => void) => () => void;
196
114
  }
197
115
 
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
116
  const read = (key: string) => {
215
117
  try {
216
118
  return localStorage.getItem(key);
@@ -228,34 +130,6 @@ const write = (key: string, value: string | null) => {
228
130
  }
229
131
  };
230
132
 
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
133
  /** Reads a theme from JSON text or an Omarchy `colors.toml`. */
260
134
  const readTheme = async (text: string) => {
261
135
  const { Result } = await import("effect");
@@ -274,8 +148,8 @@ const readTheme = async (text: string) => {
274
148
  * and registers the WebMCP theme tools where the browser offers them.
275
149
  */
276
150
  export const pageTheme = (options: PageThemeOptions): PageTheme => {
277
- const stored = keys(options.key);
278
- const storedOmarchy = omarchyKeys(options.key);
151
+ const stored = themeStorageKeys(options.key);
152
+ const storedOmarchy = omarchyStorageKeys(options.key);
279
153
  const root = options.root ?? document.documentElement;
280
154
  const defaultMode = options.defaultMode ?? "auto";
281
155
  const listeners = new Set<(state: PageThemeState) => void>();