@crewlethq/tokens 0.2.0 → 0.4.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/README.md +153 -20
- package/dist/css/base.css +194 -0
- package/dist/css/breakpoint.css +18 -0
- package/dist/css/density.css +23 -0
- package/dist/css/legacy.css +4 -6
- package/dist/css/material-symbols.css +5 -3
- package/dist/css/themes.css +338 -38
- package/dist/css/tokens.css +152 -38
- package/dist/index.d.ts +152 -12
- package/dist/index.js +152 -13
- package/dist/themes.d.ts +274 -0
- package/dist/themes.js +274 -0
- package/package.json +17 -5
- package/test/color.mjs +262 -0
- package/test/palette.d.mts +77 -0
- package/test/palette.mjs +867 -0
- package/test/palette.test.mjs +55 -0
package/dist/themes.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* Generated by style-dictionary. Do not edit. */
|
|
2
|
+
/**
|
|
3
|
+
* The two palettes, for JavaScript that cannot read a custom property: the
|
|
4
|
+
* tool chrome around a preview, a chart library that wants a series' colour
|
|
5
|
+
* as a string, a canvas that paints its own pixels.
|
|
6
|
+
*
|
|
7
|
+
* PREFER THE CUSTOM PROPERTY wherever CSS can reach: a value read here is a
|
|
8
|
+
* value taken at build time, so it does not follow a theme the reader
|
|
9
|
+
* changes. This is for the places where there is no element to read from.
|
|
10
|
+
*/
|
|
11
|
+
export const themes = {
|
|
12
|
+
"light": {
|
|
13
|
+
"color": {
|
|
14
|
+
"surface": {
|
|
15
|
+
"background": "#ffffff",
|
|
16
|
+
"subtle": "#f4f4f5",
|
|
17
|
+
"muted": "#f4f4f5",
|
|
18
|
+
"elevated": "#ebebee",
|
|
19
|
+
"topbar": "#ffffff",
|
|
20
|
+
"topbarLift": "#f4f4f5",
|
|
21
|
+
"topbarActive": "#ebebee",
|
|
22
|
+
"hover": "rgba(9, 9, 11, 0.04)",
|
|
23
|
+
"pressed": "rgba(9, 9, 11, 0.07)",
|
|
24
|
+
"inset": "rgba(9, 9, 11, 0.05)",
|
|
25
|
+
"glass": "rgba(244, 244, 245, 0.90)",
|
|
26
|
+
"scrim": "rgba(0, 0, 0, 0.6)",
|
|
27
|
+
"veil": "rgba(255, 255, 255, 0.72)",
|
|
28
|
+
"inverse": "#09090b"
|
|
29
|
+
},
|
|
30
|
+
"text": {
|
|
31
|
+
"primary": "#09090b",
|
|
32
|
+
"secondary": "#52525b",
|
|
33
|
+
"tertiary": "#5b5b64",
|
|
34
|
+
"muted": "#8f8f95",
|
|
35
|
+
"onBrand": "#ffffff",
|
|
36
|
+
"onAccent": "#ffffff",
|
|
37
|
+
"inverse": "#fafafa"
|
|
38
|
+
},
|
|
39
|
+
"brand": {
|
|
40
|
+
"primary": "#000000",
|
|
41
|
+
"primaryHover": "rgba(0, 0, 0, 0.85)",
|
|
42
|
+
"primaryActive": "rgba(0, 0, 0, 0.7)",
|
|
43
|
+
"accentInk": "#3e50b8",
|
|
44
|
+
"accentSoft": "rgba(84, 105, 212, 0.14)"
|
|
45
|
+
},
|
|
46
|
+
"border": {
|
|
47
|
+
"default": "#e4e4e7",
|
|
48
|
+
"hover": "#d4d4d8",
|
|
49
|
+
"strong": "#d4d4d8",
|
|
50
|
+
"strongHover": "#a1a1aa",
|
|
51
|
+
"control": "#71717a"
|
|
52
|
+
},
|
|
53
|
+
"focus": "#5469d4",
|
|
54
|
+
"feedback": {
|
|
55
|
+
"success": "#0f766e",
|
|
56
|
+
"successInk": "#115e59",
|
|
57
|
+
"warning": "#b45309",
|
|
58
|
+
"warningInk": "#92400e",
|
|
59
|
+
"danger": "#991b1b",
|
|
60
|
+
"dangerHover": "#7f1d1d",
|
|
61
|
+
"dangerInk": "#991b1b",
|
|
62
|
+
"info": "#075985",
|
|
63
|
+
"infoInk": "#075985",
|
|
64
|
+
"successSoft": "rgba(15, 118, 110, 0.12)",
|
|
65
|
+
"successLine": "rgba(15, 118, 110, 0.3)",
|
|
66
|
+
"warningSoft": "rgba(180, 83, 9, 0.12)",
|
|
67
|
+
"warningLine": "rgba(180, 83, 9, 0.3)",
|
|
68
|
+
"dangerSoft": "rgba(153, 27, 27, 0.12)",
|
|
69
|
+
"dangerLine": "rgba(153, 27, 27, 0.3)",
|
|
70
|
+
"infoSoft": "rgba(7, 89, 133, 0.12)",
|
|
71
|
+
"infoLine": "rgba(7, 89, 133, 0.3)"
|
|
72
|
+
},
|
|
73
|
+
"phase": {
|
|
74
|
+
"onboarding": "#980099",
|
|
75
|
+
"onboardingInk": "#8e0090",
|
|
76
|
+
"execute": "#5b37da",
|
|
77
|
+
"executeInk": "#552dd1",
|
|
78
|
+
"review": "#078543",
|
|
79
|
+
"reviewInk": "#006121",
|
|
80
|
+
"onboardingSoft": "rgba(152, 0, 153, 0.12)",
|
|
81
|
+
"executeSoft": "rgba(91, 55, 218, 0.12)",
|
|
82
|
+
"reviewSoft": "rgba(7, 133, 67, 0.12)"
|
|
83
|
+
},
|
|
84
|
+
"data": {
|
|
85
|
+
"1": "#3730a3",
|
|
86
|
+
"2": "#a16207",
|
|
87
|
+
"3": "#075985",
|
|
88
|
+
"4": "#1c9002",
|
|
89
|
+
"5": "#db2777",
|
|
90
|
+
"other": "#71717a"
|
|
91
|
+
},
|
|
92
|
+
"node": {
|
|
93
|
+
"purple": "#7150b3",
|
|
94
|
+
"purpleInk": "#573394",
|
|
95
|
+
"cyan": "#166b80",
|
|
96
|
+
"cyanInk": "#004f62",
|
|
97
|
+
"green": "#2c7046",
|
|
98
|
+
"greenInk": "#05532b",
|
|
99
|
+
"amber": "#865a28",
|
|
100
|
+
"amberInk": "#683e03",
|
|
101
|
+
"rose": "#984a55",
|
|
102
|
+
"roseInk": "#7a2f3c",
|
|
103
|
+
"blue": "#4062a7",
|
|
104
|
+
"blueInk": "#264588",
|
|
105
|
+
"purpleFill": "rgba(113, 80, 179, 0.12)",
|
|
106
|
+
"purpleLine": "rgba(113, 80, 179, 0.38)",
|
|
107
|
+
"purpleHalo": "rgba(113, 80, 179, 0.16)",
|
|
108
|
+
"cyanFill": "rgba(22, 107, 128, 0.12)",
|
|
109
|
+
"cyanLine": "rgba(22, 107, 128, 0.38)",
|
|
110
|
+
"cyanHalo": "rgba(22, 107, 128, 0.16)",
|
|
111
|
+
"greenFill": "rgba(44, 112, 70, 0.12)",
|
|
112
|
+
"greenLine": "rgba(44, 112, 70, 0.38)",
|
|
113
|
+
"greenHalo": "rgba(44, 112, 70, 0.16)",
|
|
114
|
+
"amberFill": "rgba(134, 90, 40, 0.12)",
|
|
115
|
+
"amberLine": "rgba(134, 90, 40, 0.38)",
|
|
116
|
+
"amberHalo": "rgba(134, 90, 40, 0.16)",
|
|
117
|
+
"roseFill": "rgba(152, 74, 85, 0.12)",
|
|
118
|
+
"roseLine": "rgba(152, 74, 85, 0.38)",
|
|
119
|
+
"roseHalo": "rgba(152, 74, 85, 0.16)",
|
|
120
|
+
"blueFill": "rgba(64, 98, 167, 0.12)",
|
|
121
|
+
"blueLine": "rgba(64, 98, 167, 0.38)",
|
|
122
|
+
"blueHalo": "rgba(64, 98, 167, 0.16)"
|
|
123
|
+
},
|
|
124
|
+
"scrollbar": "rgba(9, 9, 11, 0.2)",
|
|
125
|
+
"scrollbarHover": "rgba(9, 9, 11, 0.32)"
|
|
126
|
+
},
|
|
127
|
+
"shadow": {
|
|
128
|
+
"xs": "0 1px 2px rgba(9, 9, 11, 0.06)",
|
|
129
|
+
"sm": "0 2px 8px rgba(9, 9, 11, 0.07)",
|
|
130
|
+
"md": "0 4px 12px rgba(9, 9, 11, 0.08), 0 1px 3px rgba(9, 9, 11, 0.06)",
|
|
131
|
+
"lg": "0 8px 24px rgba(9, 9, 11, 0.12)",
|
|
132
|
+
"xl": "0 16px 40px rgba(9, 9, 11, 0.16), 0 2px 8px rgba(9, 9, 11, 0.08)",
|
|
133
|
+
"sheet": "-16px 0 40px rgba(9, 9, 11, 0.16), -2px 0 8px rgba(9, 9, 11, 0.08)",
|
|
134
|
+
"2xl": "0 16px 64px rgba(9, 9, 11, 0.20)",
|
|
135
|
+
"card": "0 8px 24px rgba(9, 9, 11, 0.07)",
|
|
136
|
+
"rimLit": "0 4px 24px rgba(9, 9, 11, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.6)",
|
|
137
|
+
"hairline": "none",
|
|
138
|
+
"focus": "0 0 0 2px var(--color-surface-background), 0 0 0 4px var(--color-focus)",
|
|
139
|
+
"selection": "inset 0 0 0 2px var(--color-brand-accent)",
|
|
140
|
+
"glow": "0 4px 24px rgba(84, 105, 212, 0.22)"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"dark": {
|
|
144
|
+
"color": {
|
|
145
|
+
"surface": {
|
|
146
|
+
"background": "#15171c",
|
|
147
|
+
"subtle": "#1c1e24",
|
|
148
|
+
"muted": "#1c1e24",
|
|
149
|
+
"elevated": "#22252b",
|
|
150
|
+
"topbar": "#15171c",
|
|
151
|
+
"topbarLift": "#1c1e24",
|
|
152
|
+
"topbarActive": "#22252b",
|
|
153
|
+
"hover": "rgba(255, 255, 255, 0.05)",
|
|
154
|
+
"pressed": "rgba(255, 255, 255, 0.08)",
|
|
155
|
+
"inset": "rgba(255, 255, 255, 0.05)",
|
|
156
|
+
"glass": "rgba(28, 30, 36, 0.90)",
|
|
157
|
+
"scrim": "rgba(0, 0, 0, 0.6)",
|
|
158
|
+
"veil": "rgba(21, 23, 28, 0.72)",
|
|
159
|
+
"inverse": "#ffffff"
|
|
160
|
+
},
|
|
161
|
+
"text": {
|
|
162
|
+
"primary": "#fafafa",
|
|
163
|
+
"secondary": "#c9ced8",
|
|
164
|
+
"tertiary": "#95a2b6",
|
|
165
|
+
"muted": "#686b70",
|
|
166
|
+
"onBrand": "#000000",
|
|
167
|
+
"onAccent": "#ffffff",
|
|
168
|
+
"inverse": "#09090b"
|
|
169
|
+
},
|
|
170
|
+
"brand": {
|
|
171
|
+
"primary": "#ffffff",
|
|
172
|
+
"primaryHover": "rgba(255, 255, 255, 0.9)",
|
|
173
|
+
"primaryActive": "rgba(255, 255, 255, 0.82)",
|
|
174
|
+
"accentInk": "#8b9ff5",
|
|
175
|
+
"accentSoft": "rgba(84, 105, 212, 0.18)"
|
|
176
|
+
},
|
|
177
|
+
"border": {
|
|
178
|
+
"default": "#3f4145",
|
|
179
|
+
"hover": "#4a4d52",
|
|
180
|
+
"strong": "#4a4d52",
|
|
181
|
+
"strongHover": "#5a5e65",
|
|
182
|
+
"control": "#7f848e"
|
|
183
|
+
},
|
|
184
|
+
"focus": "#8b9ff5",
|
|
185
|
+
"feedback": {
|
|
186
|
+
"success": "#14b8a6",
|
|
187
|
+
"successInk": "#5eead4",
|
|
188
|
+
"warning": "#f59e0b",
|
|
189
|
+
"warningInk": "#fde68a",
|
|
190
|
+
"danger": "#dc2626",
|
|
191
|
+
"dangerHover": "#b91c1c",
|
|
192
|
+
"dangerInk": "#fca5a5",
|
|
193
|
+
"info": "#38bdf8",
|
|
194
|
+
"infoInk": "#7dd3fc",
|
|
195
|
+
"successSoft": "rgba(20, 184, 166, 0.12)",
|
|
196
|
+
"successLine": "rgba(20, 184, 166, 0.3)",
|
|
197
|
+
"warningSoft": "rgba(245, 158, 11, 0.12)",
|
|
198
|
+
"warningLine": "rgba(245, 158, 11, 0.3)",
|
|
199
|
+
"dangerSoft": "rgba(220, 38, 38, 0.12)",
|
|
200
|
+
"dangerLine": "rgba(220, 38, 38, 0.3)",
|
|
201
|
+
"infoSoft": "rgba(56, 189, 248, 0.12)",
|
|
202
|
+
"infoLine": "rgba(56, 189, 248, 0.3)"
|
|
203
|
+
},
|
|
204
|
+
"phase": {
|
|
205
|
+
"onboarding": "#f77ae9",
|
|
206
|
+
"onboardingInk": "#ff82f1",
|
|
207
|
+
"execute": "#ac6bff",
|
|
208
|
+
"executeInk": "#d697ff",
|
|
209
|
+
"review": "#00a24f",
|
|
210
|
+
"reviewInk": "#46c872",
|
|
211
|
+
"onboardingSoft": "rgba(247, 122, 233, 0.12)",
|
|
212
|
+
"executeSoft": "rgba(172, 107, 255, 0.12)",
|
|
213
|
+
"reviewSoft": "rgba(0, 162, 79, 0.12)"
|
|
214
|
+
},
|
|
215
|
+
"data": {
|
|
216
|
+
"1": "#a5b4fc",
|
|
217
|
+
"2": "#facc15",
|
|
218
|
+
"3": "#38bdf8",
|
|
219
|
+
"4": "#86efac",
|
|
220
|
+
"5": "#ec4899",
|
|
221
|
+
"other": "#71717a"
|
|
222
|
+
},
|
|
223
|
+
"node": {
|
|
224
|
+
"purple": "#8e6ed4",
|
|
225
|
+
"purpleInk": "#cabae0",
|
|
226
|
+
"cyan": "#5ca8be",
|
|
227
|
+
"cyanInk": "#b3d7e0",
|
|
228
|
+
"green": "#62a578",
|
|
229
|
+
"greenInk": "#b8d8c0",
|
|
230
|
+
"amber": "#c89866",
|
|
231
|
+
"amberInk": "#e0c89e",
|
|
232
|
+
"rose": "#c8757f",
|
|
233
|
+
"roseInk": "#e0bfc4",
|
|
234
|
+
"blue": "#5b7fc7",
|
|
235
|
+
"blueInk": "#b9c5e0",
|
|
236
|
+
"purpleFill": "rgba(142, 110, 212, 0.12)",
|
|
237
|
+
"purpleLine": "rgba(142, 110, 212, 0.38)",
|
|
238
|
+
"purpleHalo": "rgba(142, 110, 212, 0.16)",
|
|
239
|
+
"cyanFill": "rgba(92, 168, 190, 0.12)",
|
|
240
|
+
"cyanLine": "rgba(92, 168, 190, 0.38)",
|
|
241
|
+
"cyanHalo": "rgba(92, 168, 190, 0.16)",
|
|
242
|
+
"greenFill": "rgba(98, 165, 120, 0.12)",
|
|
243
|
+
"greenLine": "rgba(98, 165, 120, 0.38)",
|
|
244
|
+
"greenHalo": "rgba(98, 165, 120, 0.16)",
|
|
245
|
+
"amberFill": "rgba(200, 152, 102, 0.12)",
|
|
246
|
+
"amberLine": "rgba(200, 152, 102, 0.38)",
|
|
247
|
+
"amberHalo": "rgba(200, 152, 102, 0.16)",
|
|
248
|
+
"roseFill": "rgba(200, 117, 127, 0.12)",
|
|
249
|
+
"roseLine": "rgba(200, 117, 127, 0.38)",
|
|
250
|
+
"roseHalo": "rgba(200, 117, 127, 0.16)",
|
|
251
|
+
"blueFill": "rgba(91, 127, 199, 0.12)",
|
|
252
|
+
"blueLine": "rgba(91, 127, 199, 0.38)",
|
|
253
|
+
"blueHalo": "rgba(91, 127, 199, 0.16)"
|
|
254
|
+
},
|
|
255
|
+
"scrollbar": "rgba(255, 255, 255, 0.16)",
|
|
256
|
+
"scrollbarHover": "rgba(255, 255, 255, 0.26)"
|
|
257
|
+
},
|
|
258
|
+
"shadow": {
|
|
259
|
+
"xs": "0 1px 2px rgba(0, 0, 0, 0.2)",
|
|
260
|
+
"sm": "0 2px 8px rgba(0, 0, 0, 0.15)",
|
|
261
|
+
"md": "0 4px 12px rgba(0, 0, 0, 0.25)",
|
|
262
|
+
"lg": "0 8px 24px rgba(0, 0, 0, 0.3)",
|
|
263
|
+
"xl": "0 12px 32px rgba(0, 0, 0, 0.4)",
|
|
264
|
+
"sheet": "-8px 0 32px rgba(0, 0, 0, 0.4)",
|
|
265
|
+
"2xl": "0 16px 64px rgba(0, 0, 0, 0.6)",
|
|
266
|
+
"card": "0 8px 24px rgba(0, 0, 0, 0.1)",
|
|
267
|
+
"rimLit": "0 4px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.04)",
|
|
268
|
+
"hairline": "inset 0 1px 0 rgba(255, 255, 255, 0.04)",
|
|
269
|
+
"focus": "0 0 0 2px var(--color-surface-background), 0 0 0 4px var(--color-focus)",
|
|
270
|
+
"selection": "inset 0 0 0 2px var(--color-brand-accent)",
|
|
271
|
+
"glow": "0 4px 24px rgba(84, 105, 212, 0.3)"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewlethq/tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Crewlet design tokens. Single JSON source compiled to CSS variables and typed JS modules.",
|
|
5
5
|
"license": "MIT AND OFL-1.1",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,34 @@
|
|
|
25
25
|
"import": "./dist/index.js"
|
|
26
26
|
},
|
|
27
27
|
"./css": "./dist/css/tokens.css",
|
|
28
|
+
"./css/base": "./dist/css/base.css",
|
|
29
|
+
"./css/breakpoint": "./dist/css/breakpoint.css",
|
|
30
|
+
"./css/density": "./dist/css/density.css",
|
|
28
31
|
"./css/fonts": "./dist/css/fonts.css",
|
|
29
32
|
"./css/legacy": "./dist/css/legacy.css",
|
|
30
33
|
"./css/material-symbols": "./dist/css/material-symbols.css",
|
|
31
34
|
"./css/themes": "./dist/css/themes.css",
|
|
32
|
-
"./fonts/*": "./fonts/*"
|
|
35
|
+
"./fonts/*": "./fonts/*",
|
|
36
|
+
"./test/palette": {
|
|
37
|
+
"types": "./test/palette.d.mts",
|
|
38
|
+
"import": "./test/palette.mjs"
|
|
39
|
+
}
|
|
33
40
|
},
|
|
34
41
|
"files": [
|
|
35
42
|
"dist",
|
|
36
|
-
"fonts"
|
|
43
|
+
"fonts",
|
|
44
|
+
"test/color.mjs",
|
|
45
|
+
"test/palette.mjs",
|
|
46
|
+
"test/palette.d.mts",
|
|
47
|
+
"test/palette.test.mjs"
|
|
37
48
|
],
|
|
38
49
|
"scripts": {
|
|
39
50
|
"build": "node scripts/build.mjs && tsc -p tsconfig.json",
|
|
40
51
|
"dev": "node scripts/build.mjs --watch",
|
|
41
|
-
"lint": "eslint scripts",
|
|
52
|
+
"lint": "eslint scripts test",
|
|
42
53
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
-
"
|
|
54
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
55
|
+
"clean": "rm -rf dist src/index.ts src/themes.ts"
|
|
44
56
|
},
|
|
45
57
|
"devDependencies": {
|
|
46
58
|
"style-dictionary": "^5.5.3"
|
package/test/color.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour maths for the palette suite: sRGB, WCAG, OKLab and dichromat
|
|
3
|
+
* simulation, with no dependencies.
|
|
4
|
+
*
|
|
5
|
+
* This exists because the token files make promises that cannot be checked by
|
|
6
|
+
* reading them: that every text step clears contrast on the WORST surface it
|
|
7
|
+
* can land on (not the panel it was designed against), that a fill step is
|
|
8
|
+
* never used where a text step belongs, and that the categorical hues stay
|
|
9
|
+
* separable, including for red-green colour vision. Measured once by hand and
|
|
10
|
+
* written into a comment, those numbers rot silently. Here they are computed
|
|
11
|
+
* from the stylesheets that actually ship.
|
|
12
|
+
*
|
|
13
|
+
* It is published as part of `@crewlethq/tokens/test/palette`, so a consumer
|
|
14
|
+
* runs the same maths over the version it installed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @typedef {{ r: number, g: number, b: number }} RGB */
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Stylesheet parsing
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every custom-property block in a stylesheet, in source order, as
|
|
25
|
+
* `{ atRule, selector, declarations }`.
|
|
26
|
+
*
|
|
27
|
+
* A real (if small) parser rather than a regex over `([^{}]+)\{([^{}]*)\}`:
|
|
28
|
+
* the theme sheet nests its dark block inside `@media`, and a flat regex can
|
|
29
|
+
* only reach that block by failing to match the at-rule and picking the inner
|
|
30
|
+
* one up on a later pass. That works until an at-rule holds two blocks, or a
|
|
31
|
+
* selector repeats inside and outside one, and then it mis-reports rather than
|
|
32
|
+
* failing. `atRule` is the enclosing at-rule prelude, or null at the top level.
|
|
33
|
+
*/
|
|
34
|
+
export function parseBlocks(css) {
|
|
35
|
+
const clean = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
36
|
+
const blocks = [];
|
|
37
|
+
const stack = [];
|
|
38
|
+
let start = 0;
|
|
39
|
+
for (let i = 0; i < clean.length; i += 1) {
|
|
40
|
+
const ch = clean[i];
|
|
41
|
+
if (ch === '{') {
|
|
42
|
+
stack.push(clean.slice(start, i).trim());
|
|
43
|
+
start = i + 1;
|
|
44
|
+
} else if (ch === '}') {
|
|
45
|
+
const prelude = stack.pop();
|
|
46
|
+
if (prelude === undefined) throw new Error('unbalanced "}" in stylesheet');
|
|
47
|
+
const body = clean.slice(start, i);
|
|
48
|
+
// A block that holds another block is a wrapper (an at-rule); only the
|
|
49
|
+
// innermost blocks carry declarations.
|
|
50
|
+
if (!body.includes('{')) {
|
|
51
|
+
const enclosing = stack.at(-1);
|
|
52
|
+
for (const selector of prelude.split(',')) {
|
|
53
|
+
blocks.push({
|
|
54
|
+
atRule: enclosing === undefined ? null : enclosing,
|
|
55
|
+
selector: selector.trim(),
|
|
56
|
+
declarations: parseDeclarations(body),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
start = i + 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (stack.length > 0) throw new Error('unbalanced "{" in stylesheet');
|
|
64
|
+
return blocks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The custom properties in one block body, as a Map in source order. */
|
|
68
|
+
export function parseDeclarations(body) {
|
|
69
|
+
const declarations = new Map();
|
|
70
|
+
for (const line of body.split(';')) {
|
|
71
|
+
const index = line.indexOf(':');
|
|
72
|
+
if (index < 0) continue;
|
|
73
|
+
const name = line.slice(0, index).trim();
|
|
74
|
+
if (!name.startsWith('--')) continue;
|
|
75
|
+
declarations.set(name, line.slice(index + 1).trim());
|
|
76
|
+
}
|
|
77
|
+
return declarations;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Merge every block that matches, in cascade order, into one token map.
|
|
82
|
+
* `sources` are `{ name, css }` in IMPORT order, because these files share
|
|
83
|
+
* the `:root` selector and the later import is what a browser paints.
|
|
84
|
+
*/
|
|
85
|
+
export function cascade(sources, matches) {
|
|
86
|
+
const out = new Map();
|
|
87
|
+
for (const source of sources) {
|
|
88
|
+
for (const block of parseBlocks(source.css)) {
|
|
89
|
+
if (!matches(block)) continue;
|
|
90
|
+
for (const [name, value] of block.declarations) out.set(name, value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Colour values
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export function parseHex(hex) {
|
|
101
|
+
const match = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
|
102
|
+
if (!match) return null;
|
|
103
|
+
const n = parseInt(match[1], 16);
|
|
104
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseRgba(value) {
|
|
108
|
+
const match = /^rgba?\(([^)]+)\)$/i.exec(value.trim());
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
const parts = match[1]
|
|
111
|
+
.split(/[,/\s]+/)
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.map(Number);
|
|
114
|
+
const [r, g, b, a] = parts;
|
|
115
|
+
if (r === undefined || g === undefined || b === undefined) return null;
|
|
116
|
+
if ([r, g, b].some(Number.isNaN)) return null;
|
|
117
|
+
return { rgb: { r, g, b }, a: a === undefined ? 1 : a };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Composite a possibly translucent value over an opaque backdrop. */
|
|
121
|
+
export function flatten(value, backdrop) {
|
|
122
|
+
const hex = parseHex(value);
|
|
123
|
+
if (hex) return hex;
|
|
124
|
+
const rgba = parseRgba(value);
|
|
125
|
+
if (!rgba) return null;
|
|
126
|
+
const { rgb, a } = rgba;
|
|
127
|
+
return {
|
|
128
|
+
r: rgb.r * a + backdrop.r * (1 - a),
|
|
129
|
+
g: rgb.g * a + backdrop.g * (1 - a),
|
|
130
|
+
b: rgb.b * a + backdrop.b * (1 - a),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The same colour at a new alpha, as the CSS value the build emits. */
|
|
135
|
+
export function withAlpha(value, alpha) {
|
|
136
|
+
const rgb = parseHex(value) ?? parseRgba(value)?.rgb;
|
|
137
|
+
if (!rgb) throw new Error(`cannot re-alpha "${value}"`);
|
|
138
|
+
return `rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${alpha})`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// WCAG
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function channel(v) {
|
|
146
|
+
const c = v / 255;
|
|
147
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function luminance({ r, g, b }) {
|
|
151
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function contrast(a, b) {
|
|
155
|
+
const la = luminance(a);
|
|
156
|
+
const lb = luminance(b);
|
|
157
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
158
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// OKLab. Perceptual distance is what "these two hues are separable" means.
|
|
163
|
+
// RGB distance says magenta and red are far apart and says the same of two
|
|
164
|
+
// greens a reader cannot tell apart at 11px.
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export function toOklab({ r, g, b }) {
|
|
168
|
+
const lr = channel(r);
|
|
169
|
+
const lg = channel(g);
|
|
170
|
+
const lb = channel(b);
|
|
171
|
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
172
|
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
173
|
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
174
|
+
return {
|
|
175
|
+
L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
176
|
+
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
177
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Perceptual distance, scaled to roughly CIE dE units so floors read familiar. */
|
|
182
|
+
export function deltaE(x, y) {
|
|
183
|
+
const a = toOklab(x);
|
|
184
|
+
const b = toOklab(y);
|
|
185
|
+
return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b) * 100;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function chroma(c) {
|
|
189
|
+
const { a, b } = toOklab(c);
|
|
190
|
+
return Math.hypot(a, b) * 100;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function fromLinear(v) {
|
|
194
|
+
const c = v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
195
|
+
return Math.min(255, Math.max(0, c * 255));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** OKLab to sRGB, clipped. Used to search for the nearest passing value. */
|
|
199
|
+
export function fromOklab({ L, a, b }) {
|
|
200
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
201
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
202
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
203
|
+
const l = l_ ** 3;
|
|
204
|
+
const m = m_ ** 3;
|
|
205
|
+
const s = s_ ** 3;
|
|
206
|
+
return {
|
|
207
|
+
r: Math.round(fromLinear(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)),
|
|
208
|
+
g: Math.round(fromLinear(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)),
|
|
209
|
+
b: Math.round(fromLinear(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const hex = ({ r, g, b }) =>
|
|
214
|
+
`#${[r, g, b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('')}`;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Move a colour along OKLab lightness only, keeping hue and chroma, until
|
|
218
|
+
* `ok` holds. A failing token moves MINIMALLY and inside its own hue family;
|
|
219
|
+
* that is what keeps a contrast fix from becoming a repaint.
|
|
220
|
+
*/
|
|
221
|
+
export function nudge(value, direction, ok, step = 0.002) {
|
|
222
|
+
const lab = toOklab(parseHex(value));
|
|
223
|
+
for (let i = 0; i <= 500; i += 1) {
|
|
224
|
+
const candidate = fromOklab({ L: lab.L + direction * step * i, a: lab.a, b: lab.b });
|
|
225
|
+
if (ok(candidate)) return { value: hex(candidate), dL: +(direction * step * i * 100).toFixed(1) };
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Dichromat simulation (a Brettel/Vienot-style linear approximation)
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/** Simulate protanopia or deuteranopia. Enough to answer "do these collide?". */
|
|
235
|
+
export function simulate(c, kind) {
|
|
236
|
+
const r = channel(c.r);
|
|
237
|
+
const g = channel(c.g);
|
|
238
|
+
const b = channel(c.b);
|
|
239
|
+
const L = 0.31399022 * r + 0.63951294 * g + 0.04649755 * b;
|
|
240
|
+
const M = 0.15537241 * r + 0.75789446 * g + 0.08670142 * b;
|
|
241
|
+
const S = 0.01775239 * r + 0.10944209 * g + 0.87256922 * b;
|
|
242
|
+
let L2 = L;
|
|
243
|
+
let M2 = M;
|
|
244
|
+
const S2 = S;
|
|
245
|
+
if (kind === 'protan') L2 = 1.05118294 * M - 0.05116099 * S;
|
|
246
|
+
else M2 = 0.9513092 * L + 0.04866992 * S;
|
|
247
|
+
return {
|
|
248
|
+
r: fromLinear(5.47221206 * L2 - 4.6419601 * M2 + 0.16963708 * S2),
|
|
249
|
+
g: fromLinear(-1.1252419 * L2 + 2.29317094 * M2 - 0.1678952 * S2),
|
|
250
|
+
b: fromLinear(0.02980165 * L2 - 0.19318073 * M2 + 1.16364789 * S2),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The three visions every separation rule is measured under. */
|
|
255
|
+
export const VISIONS = ['normal', 'protan', 'deutan'];
|
|
256
|
+
|
|
257
|
+
/** One pair's separation under each vision, in VISIONS order. */
|
|
258
|
+
export function separation(a, b) {
|
|
259
|
+
return VISIONS.map((vision) =>
|
|
260
|
+
vision === 'normal' ? deltaE(a, b) : deltaE(simulate(a, vision), simulate(b, vision)),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the published palette suite.
|
|
3
|
+
*
|
|
4
|
+
* The implementation is plain JavaScript on purpose: it runs under `node
|
|
5
|
+
* --test` from a packed tarball, with no build step between the source and the
|
|
6
|
+
* gate. But it is a PUBLIC export, and both consumers of it are TypeScript,
|
|
7
|
+
* so the shape is declared here rather than inferred as `any` at each of them.
|
|
8
|
+
*
|
|
9
|
+
* Only the entry points are declared. The colour maths underneath is re-
|
|
10
|
+
* exported for a caller that wants to measure something of its own, and is
|
|
11
|
+
* typed loosely for that reason.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One measured rule, over one state of the cascade. */
|
|
15
|
+
export interface PaletteCheck {
|
|
16
|
+
/** Which set of values: 'base', 'light', 'dark (media)', 'dark (attribute)'. */
|
|
17
|
+
state: string;
|
|
18
|
+
/** The rule being kept, as a sentence. */
|
|
19
|
+
rule: string;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** What was measured: a token name, or a pair of them. */
|
|
22
|
+
subject: string;
|
|
23
|
+
/** The measured number, or null for a structural rule that has none. */
|
|
24
|
+
value: number | null;
|
|
25
|
+
/** The measurement and its floor, ready to print. */
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PaletteSources {
|
|
30
|
+
/** The text of the built tokens.css. */
|
|
31
|
+
tokens: string;
|
|
32
|
+
/** The text of the built themes.css. */
|
|
33
|
+
themes: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PaletteResult {
|
|
37
|
+
/** The resolved values in each state of the cascade. */
|
|
38
|
+
states: Record<string, Map<string, string>>;
|
|
39
|
+
checks: PaletteCheck[];
|
|
40
|
+
failures: PaletteCheck[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Measure the whole rule table over one built pair of stylesheets. */
|
|
44
|
+
export function runPalette(sources: PaletteSources): PaletteResult;
|
|
45
|
+
|
|
46
|
+
/** The four sets of values a browser can end up with. */
|
|
47
|
+
export function paletteStates(sources: PaletteSources): Record<string, Map<string, string>>;
|
|
48
|
+
|
|
49
|
+
/** One line per failing check, for an assertion message. */
|
|
50
|
+
export function describeFailure(check: PaletteCheck): string;
|
|
51
|
+
|
|
52
|
+
/** The tightest measured value under each rule, for a report or a story. */
|
|
53
|
+
export function tightest(checks: readonly PaletteCheck[]): PaletteCheck[];
|
|
54
|
+
|
|
55
|
+
export interface Rgb {
|
|
56
|
+
r: number;
|
|
57
|
+
g: number;
|
|
58
|
+
b: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function contrast(a: Rgb, b: Rgb): number;
|
|
62
|
+
export function deltaE(a: Rgb, b: Rgb): number;
|
|
63
|
+
export function chroma(colour: Rgb): number;
|
|
64
|
+
export function flatten(value: string, backdrop: Rgb): Rgb;
|
|
65
|
+
export function parseHex(hex: string): Rgb | null;
|
|
66
|
+
export function simulate(colour: Rgb, kind: string): Rgb;
|
|
67
|
+
export function separation(a: Rgb, b: Rgb): number[];
|
|
68
|
+
export const VISIONS: readonly string[];
|
|
69
|
+
|
|
70
|
+
/** The token name of the modal veil. */
|
|
71
|
+
export const VEIL: string;
|
|
72
|
+
|
|
73
|
+
/** The band the veiled page has to stay inside, as [floor, ceiling]. */
|
|
74
|
+
export const VEIL_BAND: readonly number[];
|
|
75
|
+
|
|
76
|
+
/** The alpha the veil draws its own root's ground at. */
|
|
77
|
+
export const VEIL_ALPHA: number;
|