@timmo001/groundwork-tokens 0.5.0 → 0.6.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/package.json +3 -2
- package/src/generate.ts +26 -13
- package/src/index.ts +2 -0
- package/src/page.ts +14 -3
- package/src/runtime.ts +18 -0
- package/src/tones.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"./webmcp": "./src/webmcp.ts",
|
|
12
12
|
"./opentui": "./generated/opentui.ts",
|
|
13
13
|
"./themes.css": "./generated/themes.css",
|
|
14
|
-
"./themes.json": "./generated/themes.json"
|
|
14
|
+
"./themes.json": "./generated/themes.json",
|
|
15
|
+
"./themes/*.json": "./themes/*.json"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"src",
|
package/src/generate.ts
CHANGED
|
@@ -151,40 +151,53 @@ const modeBlock = (roles: Roles, dark: boolean, indent = " ") =>
|
|
|
151
151
|
declarations({ ...colorProperties(roles), ...shadows(dark) }, indent),
|
|
152
152
|
].join("\n");
|
|
153
153
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
154
|
+
/**
|
|
155
|
+
* A rule under `theme`'s selector for each of its modes, matching how
|
|
156
|
+
* `data-gw-mode` and the system's scheme pick one. `block` writes a mode's
|
|
157
|
+
* declarations at the indent it's given.
|
|
158
|
+
*/
|
|
159
|
+
export const modeRules = (
|
|
160
|
+
theme: ResolvedTheme,
|
|
161
|
+
block: (roles: Roles, dark: boolean, indent: string) => string,
|
|
162
|
+
) => {
|
|
156
163
|
const selector = `[data-gw-theme="${theme.id}"]`;
|
|
157
|
-
const style = `${selector} {\n${declarations(styleProperties(theme.style))}\n}`;
|
|
158
164
|
const { light, dark, darker } = theme.modes;
|
|
159
|
-
const
|
|
165
|
+
const rules: string[] = [];
|
|
160
166
|
|
|
161
167
|
if (light !== undefined) {
|
|
162
|
-
|
|
168
|
+
rules.push(`${selector} {\n${block(light, false, " ")}\n}`);
|
|
163
169
|
} else if (dark !== undefined) {
|
|
164
|
-
|
|
170
|
+
rules.push(`${selector} {\n${block(dark, true, " ")}\n}`);
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
if (dark !== undefined) {
|
|
168
|
-
|
|
169
|
-
`${selector}[data-gw-mode="dark"] {\n${
|
|
174
|
+
rules.push(
|
|
175
|
+
`${selector}[data-gw-mode="dark"] {\n${block(dark, true, " ")}\n}`,
|
|
170
176
|
);
|
|
171
177
|
|
|
172
178
|
if (light !== undefined) {
|
|
173
|
-
|
|
174
|
-
`@media (prefers-color-scheme: dark) {\n ${selector}:not([data-gw-mode]) {\n${
|
|
179
|
+
rules.push(
|
|
180
|
+
`@media (prefers-color-scheme: dark) {\n ${selector}:not([data-gw-mode]) {\n${block(dark, true, " ")}\n }\n}`,
|
|
175
181
|
);
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
if (darker !== undefined) {
|
|
180
|
-
|
|
181
|
-
`${selector}[data-gw-mode="darker"] {\n${
|
|
186
|
+
rules.push(
|
|
187
|
+
`${selector}[data-gw-mode="darker"] {\n${block(darker, true, " ")}\n}`,
|
|
182
188
|
);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
return
|
|
191
|
+
return rules;
|
|
186
192
|
};
|
|
187
193
|
|
|
194
|
+
/** One theme's `--gw-*` rules under `[data-gw-theme]`, in each of its modes. */
|
|
195
|
+
export const themeCss = (theme: ResolvedTheme) =>
|
|
196
|
+
[
|
|
197
|
+
`[data-gw-theme="${theme.id}"] {\n${declarations(styleProperties(theme.style))}\n}`,
|
|
198
|
+
...modeRules(theme, modeBlock),
|
|
199
|
+
].join("\n\n");
|
|
200
|
+
|
|
188
201
|
/**
|
|
189
202
|
* What each `gw-` element looks like before its script defines it: the
|
|
190
203
|
* display and block size it will have, so nothing moves when it upgrades.
|
package/src/index.ts
CHANGED
package/src/page.ts
CHANGED
|
@@ -79,6 +79,12 @@ export interface PageThemeOptions {
|
|
|
79
79
|
readonly defaultMode?: ThemeMode;
|
|
80
80
|
/** The element the attributes go on. Defaults to `<html>`. */
|
|
81
81
|
readonly root?: HTMLElement;
|
|
82
|
+
/**
|
|
83
|
+
* The app's own tones, solved into an imported theme's CSS beside its
|
|
84
|
+
* roles. The built-in themes' tones come from `appTones` in the runtime
|
|
85
|
+
* entry, on the page with their CSS.
|
|
86
|
+
*/
|
|
87
|
+
readonly tones?: Readonly<Record<string, string>>;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export interface PageTheme {
|
|
@@ -210,12 +216,17 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
210
216
|
|
|
211
217
|
/** Solves a document under the custom id, applies it and keeps it. */
|
|
212
218
|
const applyCustom = async (document: Theme, text: string) => {
|
|
213
|
-
const { runtimeTheme } = await import("./runtime.ts");
|
|
219
|
+
const { appTonesCss, runtimeTheme } = await import("./runtime.ts");
|
|
214
220
|
const runtime = runtimeTheme(document, customThemeId);
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
const css =
|
|
223
|
+
options.tones === undefined
|
|
224
|
+
? runtime.css
|
|
225
|
+
: `${runtime.css}\n\n${appTonesCss(options.tones, runtime.theme)}`;
|
|
226
|
+
|
|
227
|
+
setStyle(css);
|
|
217
228
|
write(stored.document, text);
|
|
218
|
-
write(stored.css,
|
|
229
|
+
write(stored.css, css);
|
|
219
230
|
write(stored.name, document.name);
|
|
220
231
|
write(stored.theme, customThemeId);
|
|
221
232
|
commit({ ...state, theme: customThemeId, custom: document.name });
|
package/src/runtime.ts
CHANGED
|
@@ -10,9 +10,12 @@ import baseDocument from "../themes/base.json" with { type: "json" };
|
|
|
10
10
|
import { themeCss } from "./generate.ts";
|
|
11
11
|
import { SolvedTheme, Theme } from "./schema.ts";
|
|
12
12
|
import { check, resolve, type Check, type ResolvedTheme } from "./solve.ts";
|
|
13
|
+
import { appTonesCss, type AppTones } from "./tones.ts";
|
|
13
14
|
|
|
14
15
|
export { omarchyTheme } from "./omarchy.ts";
|
|
15
16
|
|
|
17
|
+
export { appTonesCss, type AppTones } from "./tones.ts";
|
|
18
|
+
|
|
16
19
|
const base = Schema.decodeUnknownSync(SolvedTheme)(baseDocument);
|
|
17
20
|
|
|
18
21
|
/** A theme solved at runtime, with its CSS and every contrast check. */
|
|
@@ -37,6 +40,21 @@ export const runtimeTheme = (theme: Theme, id = theme.id): RuntimeTheme => {
|
|
|
37
40
|
return { theme: resolved, css: themeCss(resolved), checks: check(resolved) };
|
|
38
41
|
};
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* An app's own tones as CSS for each of `themes`: `--app-tone-<name>`,
|
|
45
|
+
* `-fill` and `-border`, solved against each theme's surfaces in every mode
|
|
46
|
+
* so they meet the status colors' contrast. Put it beside the themes' CSS,
|
|
47
|
+
* such as at build time, then point a component's tone at one:
|
|
48
|
+
*
|
|
49
|
+
* ```css
|
|
50
|
+
* .regressed { --gw-tone: var(--app-tone-regressed); }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const appTones = (tones: AppTones, themes: readonly Theme[]) =>
|
|
54
|
+
themes
|
|
55
|
+
.map((theme) => appTonesCss(tones, resolve(theme, base.style)))
|
|
56
|
+
.join("\n\n");
|
|
57
|
+
|
|
40
58
|
/**
|
|
41
59
|
* Where an app keeps a user's theme document. The document is stored, not
|
|
42
60
|
* its CSS, so it's solved again when the solver changes. Local storage is
|
package/src/tones.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App tones: an app's own state colors, such as an issue's "regressed" or
|
|
3
|
+
* "muted", solved against each theme's surfaces the way the status colors
|
|
4
|
+
* are, so they read in every theme and mode.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
contrast,
|
|
10
|
+
fromHex,
|
|
11
|
+
solveLightness,
|
|
12
|
+
toCss,
|
|
13
|
+
withAlpha,
|
|
14
|
+
type Oklch,
|
|
15
|
+
} from "./color.ts";
|
|
16
|
+
import { modeRules } from "./generate.ts";
|
|
17
|
+
import { targets, type ResolvedTheme, type Roles } from "./solve.ts";
|
|
18
|
+
|
|
19
|
+
/** An app's tones: each name and the hex color it starts from. */
|
|
20
|
+
export type AppTones = Readonly<Record<string, string>>;
|
|
21
|
+
|
|
22
|
+
const textSurfaces = [
|
|
23
|
+
"surface-lower",
|
|
24
|
+
"surface-low",
|
|
25
|
+
"surface-default",
|
|
26
|
+
"surface-raised",
|
|
27
|
+
"surface-overlay",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Each tone in one mode: its text color, kept as given where it already meets
|
|
32
|
+
* the status contrast target on every surface and moved lighter or darker
|
|
33
|
+
* until it does where not, with a quiet fill and a border from it.
|
|
34
|
+
*/
|
|
35
|
+
export const appToneRoles = (
|
|
36
|
+
tones: AppTones,
|
|
37
|
+
roles: Roles,
|
|
38
|
+
dark: boolean,
|
|
39
|
+
): Roles =>
|
|
40
|
+
Object.fromEntries(
|
|
41
|
+
Object.entries(tones).flatMap(([name, hex]) => {
|
|
42
|
+
const seed = fromHex(hex);
|
|
43
|
+
|
|
44
|
+
const surfaces = textSurfaces.flatMap((surface) => {
|
|
45
|
+
const color = roles[surface];
|
|
46
|
+
|
|
47
|
+
return color === undefined ? [] : [color];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Where the tone reads worst
|
|
51
|
+
const ground = surfaces.reduce<Oklch | undefined>(
|
|
52
|
+
(worst, surface) =>
|
|
53
|
+
worst === undefined || contrast(seed, surface) < contrast(seed, worst)
|
|
54
|
+
? surface
|
|
55
|
+
: worst,
|
|
56
|
+
undefined,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const text =
|
|
60
|
+
ground === undefined || contrast(seed, ground) >= targets.status
|
|
61
|
+
? seed
|
|
62
|
+
: solveLightness(
|
|
63
|
+
seed,
|
|
64
|
+
ground,
|
|
65
|
+
targets.status,
|
|
66
|
+
dark ? "lighter" : "darker",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
[name, text],
|
|
71
|
+
[`${name}-fill`, withAlpha(text, 0.14)],
|
|
72
|
+
[`${name}-border`, withAlpha(text, 0.45)],
|
|
73
|
+
];
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `--app-tone-<name>`, `-fill` and `-border` for each tone, under the same
|
|
79
|
+
* selectors as `theme`'s own CSS. Set a component's `--gw-tone`,
|
|
80
|
+
* `--gw-tone-fill` and `--gw-tone-border` from them.
|
|
81
|
+
*/
|
|
82
|
+
export const appTonesCss = (tones: AppTones, theme: ResolvedTheme) =>
|
|
83
|
+
modeRules(theme, (roles, dark, indent) =>
|
|
84
|
+
Object.entries(appToneRoles(tones, roles, dark))
|
|
85
|
+
.map(([name, color]) => `${indent}--app-tone-${name}: ${toCss(color)};`)
|
|
86
|
+
.join("\n"),
|
|
87
|
+
).join("\n\n");
|