@timmo001/groundwork-tokens 0.0.0-stage → 0.0.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/LICENSE +201 -0
- package/README.md +34 -2
- package/generated/Themes.qml +7 -0
- package/generated/ansi.json +43 -0
- package/generated/opentui.ts +1536 -0
- package/generated/themes.css +3150 -0
- package/generated/themes.json +2604 -0
- package/generated/themes.ts +2606 -0
- package/package.json +55 -4
- package/src/color.ts +122 -0
- package/src/generate.ts +648 -0
- package/src/index.ts +11 -0
- package/src/runtime.ts +36 -0
- package/src/schema.ts +126 -0
- package/src/solve.ts +440 -0
- package/themes/base.json +20 -0
- package/themes/bridge.json +21 -0
- package/themes/catppuccin-frappe.json +21 -0
- package/themes/catppuccin-latte.json +21 -0
- package/themes/catppuccin-macchiato.json +21 -0
- package/themes/catppuccin-mocha.json +21 -0
- package/themes/glazing.json +20 -0
- package/themes/slate.json +20 -0
- package/themes/snag.json +21 -0
- package/themes/survey.json +20 -0
- package/themes/tokyo-night-day.json +21 -0
- package/themes/tokyo-night-moon.json +21 -0
- package/themes/tokyo-night-storm.json +21 -0
- package/themes/tokyo-night.json +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-tokens",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Groundwork's design tokens: the theme schema, the OKLCH solver and every theme as CSS and JSON",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./runtime": "./src/runtime.ts",
|
|
10
|
+
"./themes.css": "./generated/themes.css",
|
|
11
|
+
"./themes.json": "./generated/themes.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"!src/cli.ts",
|
|
16
|
+
"themes",
|
|
17
|
+
"generated",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"*.css"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/timmo001/groundwork.git",
|
|
30
|
+
"directory": "packages/tokens"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check": "bun run --parallel --no-exit-on-error lint typecheck format:check",
|
|
34
|
+
"gen": "bun run src/cli.ts gen",
|
|
35
|
+
"gen:check": "bun run src/cli.ts gen --check",
|
|
36
|
+
"contrast": "bun run src/cli.ts contrast",
|
|
37
|
+
"lint": "oxlint .",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"format": "prettier --write .",
|
|
40
|
+
"format:check": "prettier --check ."
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"culori": "4.0.2",
|
|
44
|
+
"effect": "4.1.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@effect/platform-node": "4.1.1",
|
|
48
|
+
"@oxlint/plugins": "1.90.0",
|
|
49
|
+
"@timmo001/oxlint-rules": "0.6.3",
|
|
50
|
+
"@types/bun": "1.4.4",
|
|
51
|
+
"@types/culori": "4.0.1",
|
|
52
|
+
"oxlint": "1.90.0",
|
|
53
|
+
"oxlint-tsgolint": "7.0.2005",
|
|
54
|
+
"prettier": "3.9.10",
|
|
55
|
+
"typescript": "7.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color math in OKLCH: parsing, gamut mapping, contrast and solving for a
|
|
3
|
+
* lightness that meets a contrast target.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
converter,
|
|
9
|
+
differenceEuclidean,
|
|
10
|
+
formatHex,
|
|
11
|
+
formatHex8,
|
|
12
|
+
parse,
|
|
13
|
+
toGamut,
|
|
14
|
+
wcagContrast,
|
|
15
|
+
} from "culori";
|
|
16
|
+
|
|
17
|
+
/** A color in OKLCH: lightness 0 to 1, chroma, hue in degrees, and alpha. */
|
|
18
|
+
export interface Oklch {
|
|
19
|
+
readonly l: number;
|
|
20
|
+
readonly c: number;
|
|
21
|
+
readonly h: number;
|
|
22
|
+
readonly alpha?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const toOklch = converter("oklch");
|
|
26
|
+
const toSrgbGamut = toGamut("rgb", "oklch");
|
|
27
|
+
|
|
28
|
+
export const fromHex = (hex: string): Oklch => {
|
|
29
|
+
const color = toOklch(parse(hex));
|
|
30
|
+
|
|
31
|
+
if (color === undefined) return { l: 0, c: 0, h: 0 };
|
|
32
|
+
|
|
33
|
+
return { l: color.l, c: color.c, h: color.h ?? 0 };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Bring a color into sRGB by reducing chroma, keeping lightness and hue. */
|
|
37
|
+
export const inGamut = (color: Oklch): Oklch => {
|
|
38
|
+
const mapped = toOklch(
|
|
39
|
+
toSrgbGamut({ mode: "oklch", l: color.l, c: color.c, h: color.h }),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return { ...color, c: mapped.c };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Six-digit sRGB hex, or eight digits when the color has alpha. */
|
|
46
|
+
export const toHex = (color: Oklch) => {
|
|
47
|
+
const mapped = toSrgbGamut({
|
|
48
|
+
mode: "oklch",
|
|
49
|
+
l: color.l,
|
|
50
|
+
c: color.c,
|
|
51
|
+
h: color.h,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return color.alpha === undefined || color.alpha === 1
|
|
55
|
+
? formatHex(mapped)
|
|
56
|
+
: formatHex8({ ...mapped, alpha: color.alpha });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const round = (value: number, places: number) => Number(value.toFixed(places));
|
|
60
|
+
|
|
61
|
+
export const toCss = (color: Oklch) => {
|
|
62
|
+
const { l, c, h } = inGamut(color);
|
|
63
|
+
|
|
64
|
+
const alpha =
|
|
65
|
+
color.alpha === undefined || color.alpha === 1
|
|
66
|
+
? ""
|
|
67
|
+
: ` / ${round(color.alpha, 2)}`;
|
|
68
|
+
|
|
69
|
+
return `oklch(${round(l * 100, 2)}% ${round(c, 4)} ${round(h, 2)}${alpha})`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const oklabDistance = differenceEuclidean("oklab");
|
|
73
|
+
|
|
74
|
+
/** How far apart two hex colors look, as the Euclidean distance in OKLab. */
|
|
75
|
+
export const distance = (a: string, b: string) => oklabDistance(a, b);
|
|
76
|
+
|
|
77
|
+
/** The WCAG 2 contrast ratio between two colors, ignoring alpha. */
|
|
78
|
+
export const contrast = (a: Oklch, b: Oklch) =>
|
|
79
|
+
wcagContrast(toHex({ ...a, alpha: 1 }), toHex({ ...b, alpha: 1 }));
|
|
80
|
+
|
|
81
|
+
export const withLightness = (color: Oklch, l: number): Oklch => ({
|
|
82
|
+
...color,
|
|
83
|
+
l: Math.min(1, Math.max(0, l)),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const shift = (color: Oklch, by: number): Oklch =>
|
|
87
|
+
withLightness(color, color.l + by);
|
|
88
|
+
|
|
89
|
+
export const withAlpha = (color: Oklch, alpha: number): Oklch => ({
|
|
90
|
+
...color,
|
|
91
|
+
alpha,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The lightness, moving away from `against` toward white or black, at which
|
|
96
|
+
* the color first reaches `target` contrast. Falls back to the extreme when
|
|
97
|
+
* the target can't be reached.
|
|
98
|
+
*/
|
|
99
|
+
export const solveLightness = (
|
|
100
|
+
color: Oklch,
|
|
101
|
+
against: Oklch,
|
|
102
|
+
target: number,
|
|
103
|
+
direction: "lighter" | "darker",
|
|
104
|
+
): Oklch => {
|
|
105
|
+
let low = direction === "lighter" ? against.l : 0;
|
|
106
|
+
let high = direction === "lighter" ? 1 : against.l;
|
|
107
|
+
|
|
108
|
+
for (let step = 0; step < 32; step++) {
|
|
109
|
+
const middle = (low + high) / 2;
|
|
110
|
+
|
|
111
|
+
const passes =
|
|
112
|
+
contrast(inGamut(withLightness(color, middle)), against) >= target;
|
|
113
|
+
|
|
114
|
+
if (direction === "lighter") {
|
|
115
|
+
if (passes) high = middle;
|
|
116
|
+
else low = middle;
|
|
117
|
+
} else if (passes) low = middle;
|
|
118
|
+
else high = middle;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return inGamut(withLightness(color, direction === "lighter" ? high : low));
|
|
122
|
+
};
|