@dowel-ui/themes 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -1,3 +1,505 @@
1
+ //#region src/colour.ts
2
+ /** Parses `oklch(L C H)` or `oklch(L C H / A)`. L may be a percentage. */
3
+ function parseOklch(value) {
4
+ const match = /^oklch\(\s*([\d.%]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+)\s*)?\)$/.exec(value.trim());
5
+ if (!match) return void 0;
6
+ const rawL = match[1] ?? "0";
7
+ return {
8
+ l: rawL.endsWith("%") ? Number.parseFloat(rawL) / 100 : Number.parseFloat(rawL),
9
+ c: Number.parseFloat(match[2] ?? "0"),
10
+ h: Number.parseFloat(match[3] ?? "0"),
11
+ alpha: match[4] === void 0 ? 1 : Number.parseFloat(match[4])
12
+ };
13
+ }
14
+ /** Linear-light sRGB, before gamma encoding. Luminance is defined on these. */
15
+ function oklchToLinearRgb(l, c, h) {
16
+ const hRad = h * Math.PI / 180;
17
+ const a = c * Math.cos(hRad);
18
+ const bb = c * Math.sin(hRad);
19
+ const lCube = (l + .3963377774 * a + .2158037573 * bb) ** 3;
20
+ const mCube = (l - .1055613458 * a - .0638541728 * bb) ** 3;
21
+ const sCube = (l - .0894841775 * a - 1.291485548 * bb) ** 3;
22
+ return {
23
+ r: 4.0767416621 * lCube - 3.3077115913 * mCube + .2309699292 * sCube,
24
+ g: -1.2684380046 * lCube + 2.6097574011 * mCube - .3413193965 * sCube,
25
+ b: -.0041960863 * lCube - .7034186147 * mCube + 1.707614701 * sCube
26
+ };
27
+ }
28
+ function clamp(value) {
29
+ return Math.min(1, Math.max(0, value));
30
+ }
31
+ /** WCAG 2.x relative luminance. */
32
+ function luminance(rgb) {
33
+ return .2126 * clamp(rgb.r) + .7152 * clamp(rgb.g) + .0722 * clamp(rgb.b);
34
+ }
35
+ /**
36
+ * Composites a translucent colour over an opaque one.
37
+ *
38
+ * Several tokens are alpha values over a surface — an overlay, a tinted alert
39
+ * background. Measuring them without compositing would report the contrast of a
40
+ * colour nobody ever sees.
41
+ */
42
+ function composite(foreground, alpha, background) {
43
+ return {
44
+ r: foreground.r * alpha + background.r * (1 - alpha),
45
+ g: foreground.g * alpha + background.g * (1 - alpha),
46
+ b: foreground.b * alpha + background.b * (1 - alpha)
47
+ };
48
+ }
49
+ function contrastRatio(a, b) {
50
+ const la = luminance(a);
51
+ const lb = luminance(b);
52
+ const lighter = Math.max(la, lb);
53
+ const darker = Math.min(la, lb);
54
+ return (lighter + .05) / (darker + .05);
55
+ }
56
+ /** `oklch(...)` string to linear RGB, composited over `over` if translucent. */
57
+ function resolveColour(value, over) {
58
+ const parsed = parseOklch(value);
59
+ if (!parsed) return void 0;
60
+ const rgb = oklchToLinearRgb(parsed.l, parsed.c, parsed.h);
61
+ if (parsed.alpha >= 1 || !over) return rgb;
62
+ return composite(rgb, parsed.alpha, over);
63
+ }
64
+ /** Gamma-encoded sRGB, 0–255, from linear-light sRGB. */
65
+ function encodeSrgb(rgb) {
66
+ const encode = (channel) => {
67
+ const value = clamp(channel);
68
+ const encoded = value <= .0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - .055;
69
+ return Math.round(encoded * 255);
70
+ };
71
+ return {
72
+ r: encode(rgb.r),
73
+ g: encode(rgb.g),
74
+ b: encode(rgb.b)
75
+ };
76
+ }
77
+ /** Linear-light sRGB from gamma-encoded channels, each 0–255. */
78
+ function decodeSrgb(r, g, b) {
79
+ const decode = (channel) => {
80
+ const value = channel / 255;
81
+ return value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4;
82
+ };
83
+ return {
84
+ r: decode(r),
85
+ g: decode(g),
86
+ b: decode(b)
87
+ };
88
+ }
89
+ /**
90
+ * Linear-light sRGB to OKLCH.
91
+ *
92
+ * The inverse of `oklchToLinearRgb`, needed because people pick colours as hex
93
+ * and the tokens are authored in OKLCH. Round-tripping through this is lossy
94
+ * only where the input is outside the OKLCH gamut the tokens use, which a hex
95
+ * value from a colour input never is.
96
+ */
97
+ function linearRgbToOklch(rgb) {
98
+ const lCube = .4122214708 * rgb.r + .5363325363 * rgb.g + .0514459929 * rgb.b;
99
+ const mCube = .2119034982 * rgb.r + .6806995451 * rgb.g + .1073969566 * rgb.b;
100
+ const sCube = .0883024619 * rgb.r + .2817188376 * rgb.g + .6299787005 * rgb.b;
101
+ const l_ = Math.cbrt(lCube);
102
+ const m_ = Math.cbrt(mCube);
103
+ const s_ = Math.cbrt(sCube);
104
+ const l = .2104542553 * l_ + .793617785 * m_ - .0040720468 * s_;
105
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + .4505937099 * s_;
106
+ const b = .0259040371 * l_ + .7827717662 * m_ - .808675766 * s_;
107
+ const c = Math.sqrt(a * a + b * b);
108
+ return {
109
+ l,
110
+ c,
111
+ h: c < 1e-6 ? 0 : (Math.atan2(b, a) * 180 / Math.PI + 360) % 360
112
+ };
113
+ }
114
+ /** `#rrggbb` (or `#rgb`) to OKLCH. Undefined for anything else. */
115
+ function hexToOklch(hex) {
116
+ const value = hex.trim().replace(/^#/, "");
117
+ const full = value.length === 3 ? value.split("").map((digit) => digit + digit).join("") : value;
118
+ if (!/^[0-9a-f]{6}$/i.test(full)) return void 0;
119
+ return linearRgbToOklch(decodeSrgb(Number.parseInt(full.slice(0, 2), 16), Number.parseInt(full.slice(2, 4), 16), Number.parseInt(full.slice(4, 6), 16)));
120
+ }
121
+ function oklchToHex({ l, c, h }) {
122
+ const { r, g, b } = encodeSrgb(oklchToLinearRgb(l, c, h));
123
+ const pair = (channel) => channel.toString(16).padStart(2, "0");
124
+ return `#${pair(r)}${pair(g)}${pair(b)}`;
125
+ }
126
+ /** An OKLCH triple as the tokens write it. */
127
+ function formatOklch({ l, c, h }) {
128
+ const round = (value, places) => Number(value.toFixed(places)).toString();
129
+ return `oklch(${round(l, 3)} ${round(c, 3)} ${round(h, 1)})`;
130
+ }
131
+ //#endregion
132
+ //#region src/preset.ts
133
+ /**
134
+ * Deriving a theme preset from a single colour.
135
+ *
136
+ * A preset in this system reassigns four tokens per mode and inherits
137
+ * everything else, so building one is not a palette exercise — it is picking a
138
+ * primary and then answering three questions the shipped presets already
139
+ * answer: what it looks like pressed, what it looks like in dark mode, and what
140
+ * text can be read on it.
141
+ *
142
+ * The deltas below are read off the presets that ship. They are not arbitrary:
143
+ * every one of those passes the contrast audit in both modes, so starting from
144
+ * the same relationships means a derived preset starts somewhere that works.
145
+ */
146
+ /** Lightness step from the base colour to its hover state, in light mode. */
147
+ const LIGHT_HOVER_DELTA = -.045;
148
+ /** And to its active state, which is a press and reads as further down. */
149
+ const LIGHT_ACTIVE_DELTA = -.083;
150
+ /**
151
+ * Dark mode raises lightness and drops chroma.
152
+ *
153
+ * A colour that reads as saturated on white reads as glaring on near-black, and
154
+ * one dark enough to sit on white disappears into the background.
155
+ */
156
+ const DARK_LIGHTNESS_DELTA = .115;
157
+ const DARK_CHROMA_DELTA = -.015;
158
+ const DARK_HOVER_DELTA = .045;
159
+ const DARK_ACTIVE_DELTA = -.045;
160
+ /** The near-white the shipped presets use for text on a saturated colour. */
161
+ const LIGHT_FOREGROUND = {
162
+ l: .985,
163
+ c: .002,
164
+ h: 265
165
+ };
166
+ /** WCAG 2.2 AA for normal text; a button label is normal text. */
167
+ const TEXT_MINIMUM = 4.5;
168
+ function clampLightness(value) {
169
+ return Math.min(.99, Math.max(.01, value));
170
+ }
171
+ function ratio(a, b) {
172
+ return contrastRatio(oklchToLinearRgb(a.l, a.c, a.h), oklchToLinearRgb(b.l, b.c, b.h));
173
+ }
174
+ /**
175
+ * Text for a saturated background: near-white, or a dark tint of its own hue.
176
+ *
177
+ * Whichever reads better, rather than always white. A light primary — amber,
178
+ * lime — cannot carry white text at 4.5:1 no matter how it is nudged, and the
179
+ * shipped `amber` preset is dark-on-light for exactly this reason.
180
+ */
181
+ function foregroundFor(background) {
182
+ const dark = {
183
+ l: .155,
184
+ c: .03,
185
+ h: background.h
186
+ };
187
+ return ratio(LIGHT_FOREGROUND, background) >= ratio(dark, background) ? LIGHT_FOREGROUND : dark;
188
+ }
189
+ function derivePreset(input, options = {}) {
190
+ const primary = {
191
+ ...input,
192
+ l: clampLightness(input.l),
193
+ c: Math.max(0, input.c)
194
+ };
195
+ const light = {
196
+ primary,
197
+ primaryHover: {
198
+ ...primary,
199
+ l: clampLightness(primary.l + LIGHT_HOVER_DELTA)
200
+ },
201
+ primaryActive: {
202
+ ...primary,
203
+ l: clampLightness(primary.l + LIGHT_ACTIVE_DELTA)
204
+ },
205
+ primaryForeground: foregroundFor(primary)
206
+ };
207
+ const darkPrimary = {
208
+ l: clampLightness(options.darkLightness ?? primary.l + DARK_LIGHTNESS_DELTA),
209
+ c: Math.max(0, primary.c + DARK_CHROMA_DELTA),
210
+ h: primary.h
211
+ };
212
+ return {
213
+ light,
214
+ dark: {
215
+ primary: darkPrimary,
216
+ primaryHover: {
217
+ ...darkPrimary,
218
+ l: clampLightness(darkPrimary.l + DARK_HOVER_DELTA)
219
+ },
220
+ primaryActive: {
221
+ ...darkPrimary,
222
+ l: clampLightness(darkPrimary.l + DARK_ACTIVE_DELTA)
223
+ },
224
+ primaryForeground: foregroundFor(darkPrimary)
225
+ }
226
+ };
227
+ }
228
+ /**
229
+ * The pairs a derived preset is responsible for.
230
+ *
231
+ * Only these four per mode: every other pair in the system is inherited from
232
+ * the base tokens, which the audit already covers. Reporting the inherited ones
233
+ * would be reporting on something the person cannot change from here.
234
+ */
235
+ function checkPreset(preset) {
236
+ const checks = [];
237
+ for (const [mode, values] of [["Light", preset.light], ["Dark", preset.dark]]) for (const [state, background] of [
238
+ ["primary", values.primary],
239
+ ["primary-hover", values.primaryHover],
240
+ ["primary-active", values.primaryActive]
241
+ ]) checks.push({
242
+ label: `${mode}: primary-foreground on ${state}`,
243
+ ratio: ratio(values.primaryForeground, background),
244
+ minimum: TEXT_MINIMUM,
245
+ passes: ratio(values.primaryForeground, background) >= TEXT_MINIMUM
246
+ });
247
+ return checks;
248
+ }
249
+ function block(selector, mode) {
250
+ return [
251
+ `${selector} {`,
252
+ ` --primary: ${formatOklch(mode.primary)};`,
253
+ ` --primary-hover: ${formatOklch(mode.primaryHover)};`,
254
+ ` --primary-active: ${formatOklch(mode.primaryActive)};`,
255
+ ` --primary-foreground: ${formatOklch(mode.primaryForeground)};`,
256
+ `}`
257
+ ].join("\n");
258
+ }
259
+ /**
260
+ * The preset as a stylesheet, in the same shape as the ones that ship.
261
+ *
262
+ * Deliberately the same file format rather than a bespoke export: what comes
263
+ * out of here can be dropped into `packages/themes/src/presets/` unchanged, and
264
+ * is then covered by the same audit as everything else.
265
+ */
266
+ function formatPreset(name, preset) {
267
+ return [
268
+ `/* Theme preset: ${name}.`,
269
+ ` * Apply with data-theme="${name}" on the <html> element. Only the brand-carrying`,
270
+ ` * tokens are reassigned; every neutral, radius and motion token is inherited. */`,
271
+ "",
272
+ block(`[data-theme="${name}"]`, preset.light),
273
+ "",
274
+ block(`.dark[data-theme="${name}"]`, preset.dark),
275
+ ""
276
+ ].join("\n");
277
+ }
278
+ /** A name usable as a `data-theme` value. */
279
+ function slugify(name) {
280
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
281
+ return slug.length > 0 ? slug : "custom";
282
+ }
283
+ //#endregion
284
+ //#region src/figma.ts
285
+ /** Strips block comments, which otherwise hide `--x: y;` pairs inside them from the parser. */
286
+ function stripComments(css) {
287
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
288
+ }
289
+ /**
290
+ * The declarations inside the first block whose selector is exactly `selector`.
291
+ *
292
+ * Exact, not substring: `.dark` must not match `.dark[data-theme="ocean"]`, and
293
+ * `:root` must not match `:root[dir="rtl"]`. Whitespace inside a value is
294
+ * collapsed, because a font stack written over four lines is one value.
295
+ */
296
+ function parseTokenCss(css, selector) {
297
+ const clean = stripComments(css);
298
+ const declarations = {};
299
+ let searchFrom = 0;
300
+ for (;;) {
301
+ const brace = clean.indexOf("{", searchFrom);
302
+ if (brace === -1) break;
303
+ const start = Math.max(clean.lastIndexOf("}", brace), clean.lastIndexOf(";", brace), clean.lastIndexOf("{", brace - 1));
304
+ const candidate = clean.slice(start + 1, brace).trim();
305
+ const end = clean.indexOf("}", brace);
306
+ if (end === -1) break;
307
+ if (candidate === selector) {
308
+ const body = clean.slice(brace + 1, end);
309
+ for (const match of body.matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)) {
310
+ const name = match[1];
311
+ const value = match[2];
312
+ if (name && value) declarations[name] = value.replace(/\s+/g, " ").trim();
313
+ }
314
+ return declarations;
315
+ }
316
+ searchFrom = end + 1;
317
+ }
318
+ return declarations;
319
+ }
320
+ /**
321
+ * Resolves `var(--x)` references the way the cascade would.
322
+ *
323
+ * `scopes` are searched in order, so a mode's own declarations shadow the root's
324
+ * and the root's shadow the raw scale — which is exactly what `.dark { --x }`
325
+ * over `:root { --x }` over `@theme { --x }` means. A fallback inside the
326
+ * `var()` is used when nothing defines the name, and an unresolvable reference
327
+ * is left as written rather than silently dropped.
328
+ */
329
+ function resolveReferences(value, scopes) {
330
+ let current = value;
331
+ for (let depth = 0; depth < 16; depth += 1) {
332
+ const next = current.replace(/var\(\s*--([\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\))?[^()]*))?\)/g, (whole, name, fallback) => {
333
+ for (const scope of scopes) {
334
+ const found = scope[name];
335
+ if (found !== void 0) return found;
336
+ }
337
+ return fallback?.trim() ?? whole;
338
+ });
339
+ if (next === current) return current;
340
+ current = next;
341
+ }
342
+ return current;
343
+ }
344
+ /** `oklch(...)` to `#rrggbb`, or `#rrggbbaa` when it carries alpha. */
345
+ function cssColourToHex(value) {
346
+ const parsed = parseOklch(value.trim());
347
+ if (!parsed) return void 0;
348
+ const { r, g, b } = encodeSrgb(oklchToLinearRgb(parsed.l, parsed.c, parsed.h));
349
+ const pair = (channel) => channel.toString(16).padStart(2, "0");
350
+ const rgb = `#${pair(r)}${pair(g)}${pair(b)}`;
351
+ return parsed.alpha >= 1 ? rgb : `${rgb}${pair(Math.round(parsed.alpha * 255))}`;
352
+ }
353
+ const ROOT_FONT_PX = 16;
354
+ /**
355
+ * A length in px, for the values the scale is written in.
356
+ *
357
+ * Handles the two shapes the tokens use: a plain `rem`/`px`, and the radius
358
+ * ladder's `calc(<rem> * var(--radius-scale, 1))`, which is resolved with the
359
+ * given scale so an exported theme carries the corner radius it was designed
360
+ * with rather than a formula Figma cannot evaluate.
361
+ */
362
+ function cssLengthToPx(value, radiusScale = 1) {
363
+ const trimmed = value.trim();
364
+ const calc = /^calc\(\s*([\d.]+)rem\s*\*\s*var\(--radius-scale(?:,\s*[\d.]+)?\)\s*\)$/.exec(trimmed);
365
+ if (calc?.[1]) return round(Number.parseFloat(calc[1]) * ROOT_FONT_PX * radiusScale);
366
+ const rem = /^([\d.]+)rem$/.exec(trimmed);
367
+ if (rem?.[1]) return round(Number.parseFloat(rem[1]) * ROOT_FONT_PX);
368
+ const px = /^([\d.]+)px$/.exec(trimmed);
369
+ if (px?.[1]) return round(Number.parseFloat(px[1]));
370
+ }
371
+ function round(value) {
372
+ return Math.round(value * 100) / 100;
373
+ }
374
+ function px(value) {
375
+ return `${String(value)}px`;
376
+ }
377
+ /** The four tokens a derived preset owns, as declarations, per mode. */
378
+ function presetDeclarations(preset) {
379
+ const mode = (entry) => ({
380
+ primary: formatOklch(entry.primary),
381
+ "primary-hover": formatOklch(entry.primaryHover),
382
+ "primary-active": formatOklch(entry.primaryActive),
383
+ "primary-foreground": formatOklch(entry.primaryForeground)
384
+ });
385
+ return {
386
+ light: mode(preset.light),
387
+ dark: mode(preset.dark)
388
+ };
389
+ }
390
+ function colourGroup(declarations, scopes, filter) {
391
+ const group = {};
392
+ for (const [name, raw] of Object.entries(declarations)) {
393
+ if (!filter(name)) continue;
394
+ const hex = cssColourToHex(resolveReferences(raw, scopes));
395
+ if (hex) group[name] = {
396
+ $type: "color",
397
+ $value: hex
398
+ };
399
+ }
400
+ return group;
401
+ }
402
+ /** `--color-neutral-500` → `neutral.500`, nested. */
403
+ function scaleColours(scale) {
404
+ const group = {};
405
+ for (const [name, raw] of Object.entries(scale)) {
406
+ const match = /^color-([a-z]+)-(\d+)$/.exec(name);
407
+ if (!match) continue;
408
+ const [, family, step] = match;
409
+ if (!family || !step) continue;
410
+ const hex = cssColourToHex(raw);
411
+ if (!hex) continue;
412
+ const familyGroup = group[family] ??= {};
413
+ familyGroup[step] = {
414
+ $type: "color",
415
+ $value: hex
416
+ };
417
+ }
418
+ return group;
419
+ }
420
+ function radii(scale, radiusScale) {
421
+ const group = {};
422
+ for (const [name, raw] of Object.entries(scale)) {
423
+ const match = /^radius-([\w]+)$/.exec(name);
424
+ if (!match?.[1]) continue;
425
+ const length = cssLengthToPx(raw, radiusScale);
426
+ if (length !== void 0) group[match[1]] = {
427
+ $type: "dimension",
428
+ $value: px(length)
429
+ };
430
+ }
431
+ return group;
432
+ }
433
+ function typography(scale) {
434
+ const family = {};
435
+ for (const [name, raw] of Object.entries(scale)) {
436
+ const match = /^font-([a-z]+)$/.exec(name);
437
+ if (!match?.[1]) continue;
438
+ family[match[1]] = {
439
+ $type: "fontFamily",
440
+ $value: raw.split(",").map((entry) => entry.trim().replace(/^["']|["']$/g, ""))
441
+ };
442
+ }
443
+ const size = {};
444
+ for (const [name, raw] of Object.entries(scale)) {
445
+ const step = /^text-([\w]+)$/.exec(name)?.[1];
446
+ if (!step) continue;
447
+ const fontSize = cssLengthToPx(raw);
448
+ if (fontSize === void 0) continue;
449
+ const entry = { size: {
450
+ $type: "dimension",
451
+ $value: px(fontSize)
452
+ } };
453
+ const lineHeight = scale[`text-${step}--line-height`];
454
+ const lineHeightPx = lineHeight === void 0 ? void 0 : cssLengthToPx(lineHeight);
455
+ if (lineHeightPx !== void 0) entry.lineHeight = {
456
+ $type: "dimension",
457
+ $value: px(lineHeightPx)
458
+ };
459
+ size[step] = entry;
460
+ }
461
+ return {
462
+ family,
463
+ text: size
464
+ };
465
+ }
466
+ /**
467
+ * The whole theme, as a design-tokens document.
468
+ *
469
+ * Three sets: `core` (the raw scales, the same in both modes), `light` and
470
+ * `dark` (the semantic colours, resolved). A designer enables `core` plus one
471
+ * mode, which mirrors exactly how the CSS composes.
472
+ */
473
+ function toDesignTokens(input) {
474
+ const radiusScale = input.radiusScale ?? 1;
475
+ const light = {
476
+ ...input.light,
477
+ ...input.preset?.light
478
+ };
479
+ const dark = {
480
+ ...input.dark,
481
+ ...input.preset?.dark
482
+ };
483
+ const semantic = (name) => name !== "radius-scale" && !name.startsWith("color-");
484
+ return {
485
+ $description: `Dowel design tokens, preset "${input.name}". Generated from the CSS the components use; colours are sRGB hex converted from OKLCH.`,
486
+ core: {
487
+ color: scaleColours(input.scale),
488
+ radius: radii(input.scale, radiusScale),
489
+ font: typography(input.scale)
490
+ },
491
+ light: { color: colourGroup(light, [light, input.scale], semantic) },
492
+ dark: { color: colourGroup({
493
+ ...light,
494
+ ...dark
495
+ }, [
496
+ dark,
497
+ light,
498
+ input.scale
499
+ ], semantic) }
500
+ };
501
+ }
502
+ //#endregion
1
503
  //#region src/index.ts
2
504
  /**
3
505
  * Typed surface of the theme layer. The CSS is the implementation; these
@@ -34,6 +536,6 @@ function isColorMode(value) {
34
536
  return COLOR_MODES.includes(value);
35
537
  }
36
538
  //#endregion
37
- export { COLOR_MODES, DARK_CLASS, RADIUS_SCALE_PROPERTY, THEME_ATTRIBUTE, THEME_PRESETS, isColorMode, isThemePreset };
539
+ export { COLOR_MODES, DARK_CLASS, RADIUS_SCALE_PROPERTY, TEXT_MINIMUM, THEME_ATTRIBUTE, THEME_PRESETS, checkPreset, composite, contrastRatio, cssColourToHex, cssLengthToPx, decodeSrgb, derivePreset, encodeSrgb, foregroundFor, formatOklch, formatPreset, hexToOklch, isColorMode, isThemePreset, linearRgbToOklch, luminance, oklchToHex, oklchToLinearRgb, parseOklch, parseTokenCss, presetDeclarations, resolveColour, resolveReferences, slugify, toDesignTokens };
38
540
 
39
541
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Typed surface of the theme layer. The CSS is the implementation; these\n * constants exist so theme switchers, the docs playground and the future CLI\n * all agree on the same vocabulary.\n */\n\nexport const THEME_PRESETS = [\n \"default\",\n \"ocean\",\n \"emerald\",\n \"violet\",\n \"rose\",\n \"amber\",\n \"monochrome\",\n] as const;\n\nexport type ThemePreset = (typeof THEME_PRESETS)[number];\n\nexport const COLOR_MODES = [\"light\", \"dark\", \"system\"] as const;\n\nexport type ColorMode = (typeof COLOR_MODES)[number];\n\n/** Attribute set on the <html> element to activate a preset. */\nexport const THEME_ATTRIBUTE = \"data-theme\";\n\n/** Class toggled on the <html> element for dark mode. */\nexport const DARK_CLASS = \"dark\";\n\n/**\n * Custom property that re-proportions every radius token at once.\n * `1` is the designed default; `0` gives fully square corners.\n */\nexport const RADIUS_SCALE_PROPERTY = \"--radius-scale\";\n\nexport function isThemePreset(value: string): value is ThemePreset {\n return (THEME_PRESETS as readonly string[]).includes(value);\n}\n\nexport function isColorMode(value: string): value is ColorMode {\n return (COLOR_MODES as readonly string[]).includes(value);\n}\n"],"mappings":";;;;;;AAMA,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAIA,MAAa,cAAc;CAAC;CAAS;CAAQ;AAAQ;;AAKrD,MAAa,kBAAkB;;AAG/B,MAAa,aAAa;;;;;AAM1B,MAAa,wBAAwB;AAErC,SAAgB,cAAc,OAAqC;CACjE,OAAQ,cAAoC,SAAS,KAAK;AAC5D;AAEA,SAAgB,YAAY,OAAmC;CAC7D,OAAQ,YAAkC,SAAS,KAAK;AAC1D"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/colour.ts","../src/preset.ts","../src/figma.ts","../src/index.ts"],"sourcesContent":["/**\n * OKLCH → sRGB, and WCAG contrast.\n *\n * The design tokens are authored in OKLCH because its lightness is\n * perceptually even. WCAG contrast, however, is defined on sRGB relative\n * luminance — so checking the palette means actually converting it rather than\n * eyeballing the lightness numbers, which are not the same thing.\n */\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** Parses `oklch(L C H)` or `oklch(L C H / A)`. L may be a percentage. */\nexport function parseOklch(\n value: string,\n): { l: number; c: number; h: number; alpha: number } | undefined {\n const match = /^oklch\\(\\s*([\\d.%]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)$/.exec(\n value.trim(),\n );\n if (!match) return undefined;\n\n const rawL = match[1] ?? \"0\";\n const l = rawL.endsWith(\"%\") ? Number.parseFloat(rawL) / 100 : Number.parseFloat(rawL);\n\n return {\n l,\n c: Number.parseFloat(match[2] ?? \"0\"),\n h: Number.parseFloat(match[3] ?? \"0\"),\n alpha: match[4] === undefined ? 1 : Number.parseFloat(match[4]),\n };\n}\n\n/** Linear-light sRGB, before gamma encoding. Luminance is defined on these. */\nexport function oklchToLinearRgb(l: number, c: number, h: number): Rgb {\n const hRad = (h * Math.PI) / 180;\n const a = c * Math.cos(hRad);\n const bb = c * Math.sin(hRad);\n\n const lCube = (l + 0.3963377774 * a + 0.2158037573 * bb) ** 3;\n const mCube = (l - 0.1055613458 * a - 0.0638541728 * bb) ** 3;\n const sCube = (l - 0.0894841775 * a - 1.291485548 * bb) ** 3;\n\n return {\n r: 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube,\n g: -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube,\n b: -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube,\n };\n}\n\nfunction clamp(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\n/** WCAG 2.x relative luminance. */\nexport function luminance(rgb: Rgb): number {\n return 0.2126 * clamp(rgb.r) + 0.7152 * clamp(rgb.g) + 0.0722 * clamp(rgb.b);\n}\n\n/**\n * Composites a translucent colour over an opaque one.\n *\n * Several tokens are alpha values over a surface — an overlay, a tinted alert\n * background. Measuring them without compositing would report the contrast of a\n * colour nobody ever sees.\n */\nexport function composite(foreground: Rgb, alpha: number, background: Rgb): Rgb {\n return {\n r: foreground.r * alpha + background.r * (1 - alpha),\n g: foreground.g * alpha + background.g * (1 - alpha),\n b: foreground.b * alpha + background.b * (1 - alpha),\n };\n}\n\nexport function contrastRatio(a: Rgb, b: Rgb): number {\n const la = luminance(a);\n const lb = luminance(b);\n const lighter = Math.max(la, lb);\n const darker = Math.min(la, lb);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/** `oklch(...)` string to linear RGB, composited over `over` if translucent. */\nexport function resolveColour(value: string, over?: Rgb): Rgb | undefined {\n const parsed = parseOklch(value);\n if (!parsed) return undefined;\n\n const rgb = oklchToLinearRgb(parsed.l, parsed.c, parsed.h);\n if (parsed.alpha >= 1 || !over) return rgb;\n return composite(rgb, parsed.alpha, over);\n}\n\n/** Gamma-encoded sRGB, 0–255, from linear-light sRGB. */\nexport function encodeSrgb(rgb: Rgb): { r: number; g: number; b: number } {\n const encode = (channel: number) => {\n const value = clamp(channel);\n const encoded = value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;\n return Math.round(encoded * 255);\n };\n\n return { r: encode(rgb.r), g: encode(rgb.g), b: encode(rgb.b) };\n}\n\n/** Linear-light sRGB from gamma-encoded channels, each 0–255. */\nexport function decodeSrgb(r: number, g: number, b: number): Rgb {\n const decode = (channel: number) => {\n const value = channel / 255;\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n };\n\n return { r: decode(r), g: decode(g), b: decode(b) };\n}\n\nexport interface Oklch {\n l: number;\n c: number;\n h: number;\n}\n\n/**\n * Linear-light sRGB to OKLCH.\n *\n * The inverse of `oklchToLinearRgb`, needed because people pick colours as hex\n * and the tokens are authored in OKLCH. Round-tripping through this is lossy\n * only where the input is outside the OKLCH gamut the tokens use, which a hex\n * value from a colour input never is.\n */\nexport function linearRgbToOklch(rgb: Rgb): Oklch {\n const lCube = 0.4122214708 * rgb.r + 0.5363325363 * rgb.g + 0.0514459929 * rgb.b;\n const mCube = 0.2119034982 * rgb.r + 0.6806995451 * rgb.g + 0.1073969566 * rgb.b;\n const sCube = 0.0883024619 * rgb.r + 0.2817188376 * rgb.g + 0.6299787005 * rgb.b;\n\n const l_ = Math.cbrt(lCube);\n const m_ = Math.cbrt(mCube);\n const s_ = Math.cbrt(sCube);\n\n const l = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;\n const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;\n const b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;\n\n const c = Math.sqrt(a * a + b * b);\n // atan2 returns (-180, 180]; hues are conventionally [0, 360).\n const h = c < 1e-6 ? 0 : ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360;\n\n return { l, c, h };\n}\n\n/** `#rrggbb` (or `#rgb`) to OKLCH. Undefined for anything else. */\nexport function hexToOklch(hex: string): Oklch | undefined {\n const value = hex.trim().replace(/^#/, \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((digit) => digit + digit)\n .join(\"\")\n : value;\n\n if (!/^[0-9a-f]{6}$/i.test(full)) return undefined;\n\n return linearRgbToOklch(\n decodeSrgb(\n Number.parseInt(full.slice(0, 2), 16),\n Number.parseInt(full.slice(2, 4), 16),\n Number.parseInt(full.slice(4, 6), 16),\n ),\n );\n}\n\nexport function oklchToHex({ l, c, h }: Oklch): string {\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(l, c, h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n return `#${pair(r)}${pair(g)}${pair(b)}`;\n}\n\n/** An OKLCH triple as the tokens write it. */\nexport function formatOklch({ l, c, h }: Oklch): string {\n const round = (value: number, places: number) => Number(value.toFixed(places)).toString();\n return `oklch(${round(l, 3)} ${round(c, 3)} ${round(h, 1)})`;\n}\n","import { contrastRatio, formatOklch, oklchToLinearRgb, type Oklch } from \"./colour\";\n\n/**\n * Deriving a theme preset from a single colour.\n *\n * A preset in this system reassigns four tokens per mode and inherits\n * everything else, so building one is not a palette exercise — it is picking a\n * primary and then answering three questions the shipped presets already\n * answer: what it looks like pressed, what it looks like in dark mode, and what\n * text can be read on it.\n *\n * The deltas below are read off the presets that ship. They are not arbitrary:\n * every one of those passes the contrast audit in both modes, so starting from\n * the same relationships means a derived preset starts somewhere that works.\n */\n\n/** Lightness step from the base colour to its hover state, in light mode. */\nconst LIGHT_HOVER_DELTA = -0.045;\n/** And to its active state, which is a press and reads as further down. */\nconst LIGHT_ACTIVE_DELTA = -0.083;\n\n/**\n * Dark mode raises lightness and drops chroma.\n *\n * A colour that reads as saturated on white reads as glaring on near-black, and\n * one dark enough to sit on white disappears into the background.\n */\nconst DARK_LIGHTNESS_DELTA = 0.115;\nconst DARK_CHROMA_DELTA = -0.015;\nconst DARK_HOVER_DELTA = 0.045;\nconst DARK_ACTIVE_DELTA = -0.045;\n\n/** The near-white the shipped presets use for text on a saturated colour. */\nconst LIGHT_FOREGROUND: Oklch = { l: 0.985, c: 0.002, h: 265 };\n\n/** WCAG 2.2 AA for normal text; a button label is normal text. */\nexport const TEXT_MINIMUM = 4.5;\n\nfunction clampLightness(value: number): number {\n return Math.min(0.99, Math.max(0.01, value));\n}\n\nfunction ratio(a: Oklch, b: Oklch): number {\n return contrastRatio(oklchToLinearRgb(a.l, a.c, a.h), oklchToLinearRgb(b.l, b.c, b.h));\n}\n\n/**\n * Text for a saturated background: near-white, or a dark tint of its own hue.\n *\n * Whichever reads better, rather than always white. A light primary — amber,\n * lime — cannot carry white text at 4.5:1 no matter how it is nudged, and the\n * shipped `amber` preset is dark-on-light for exactly this reason.\n */\nexport function foregroundFor(background: Oklch): Oklch {\n const dark: Oklch = { l: 0.155, c: 0.03, h: background.h };\n return ratio(LIGHT_FOREGROUND, background) >= ratio(dark, background)\n ? LIGHT_FOREGROUND\n : dark;\n}\n\nexport interface PresetMode {\n primary: Oklch;\n primaryHover: Oklch;\n primaryActive: Oklch;\n primaryForeground: Oklch;\n}\n\nexport interface DerivedPreset {\n light: PresetMode;\n dark: PresetMode;\n}\n\nexport interface DeriveOptions {\n /**\n * Lightness of the dark-mode primary.\n *\n * Overridable because it is the one derived value with no single right\n * answer: how bright a brand reads on near-black is a judgement about the\n * brand, not about contrast.\n */\n darkLightness?: number;\n}\n\nexport function derivePreset(input: Oklch, options: DeriveOptions = {}): DerivedPreset {\n // Clamped on the way in as well as on the way out. Every value this returns\n // is one it is responsible for, including the one it was handed: a preset\n // built on pure black is not a preset anyone can use, and passing it through\n // untouched would mean the only unusable value in the output is the one that\n // was never checked.\n const primary: Oklch = { ...input, l: clampLightness(input.l), c: Math.max(0, input.c) };\n\n const light: PresetMode = {\n primary,\n primaryHover: { ...primary, l: clampLightness(primary.l + LIGHT_HOVER_DELTA) },\n primaryActive: { ...primary, l: clampLightness(primary.l + LIGHT_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(primary),\n };\n\n const darkPrimary: Oklch = {\n l: clampLightness(options.darkLightness ?? primary.l + DARK_LIGHTNESS_DELTA),\n c: Math.max(0, primary.c + DARK_CHROMA_DELTA),\n h: primary.h,\n };\n\n const dark: PresetMode = {\n primary: darkPrimary,\n primaryHover: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_HOVER_DELTA) },\n primaryActive: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(darkPrimary),\n };\n\n return { light, dark };\n}\n\nexport interface ContrastCheck {\n label: string;\n ratio: number;\n minimum: number;\n passes: boolean;\n}\n\n/**\n * The pairs a derived preset is responsible for.\n *\n * Only these four per mode: every other pair in the system is inherited from\n * the base tokens, which the audit already covers. Reporting the inherited ones\n * would be reporting on something the person cannot change from here.\n */\nexport function checkPreset(preset: DerivedPreset): ContrastCheck[] {\n const checks: ContrastCheck[] = [];\n\n for (const [mode, values] of [\n [\"Light\", preset.light],\n [\"Dark\", preset.dark],\n ] as const) {\n for (const [state, background] of [\n [\"primary\", values.primary],\n [\"primary-hover\", values.primaryHover],\n [\"primary-active\", values.primaryActive],\n ] as const) {\n checks.push({\n label: `${mode}: primary-foreground on ${state}`,\n ratio: ratio(values.primaryForeground, background),\n minimum: TEXT_MINIMUM,\n passes: ratio(values.primaryForeground, background) >= TEXT_MINIMUM,\n });\n }\n }\n\n return checks;\n}\n\nfunction block(selector: string, mode: PresetMode): string {\n return [\n `${selector} {`,\n ` --primary: ${formatOklch(mode.primary)};`,\n ` --primary-hover: ${formatOklch(mode.primaryHover)};`,\n ` --primary-active: ${formatOklch(mode.primaryActive)};`,\n ` --primary-foreground: ${formatOklch(mode.primaryForeground)};`,\n `}`,\n ].join(\"\\n\");\n}\n\n/**\n * The preset as a stylesheet, in the same shape as the ones that ship.\n *\n * Deliberately the same file format rather than a bespoke export: what comes\n * out of here can be dropped into `packages/themes/src/presets/` unchanged, and\n * is then covered by the same audit as everything else.\n */\nexport function formatPreset(name: string, preset: DerivedPreset): string {\n return [\n `/* Theme preset: ${name}.`,\n ` * Apply with data-theme=\"${name}\" on the <html> element. Only the brand-carrying`,\n ` * tokens are reassigned; every neutral, radius and motion token is inherited. */`,\n \"\",\n block(`[data-theme=\"${name}\"]`, preset.light),\n \"\",\n block(`.dark[data-theme=\"${name}\"]`, preset.dark),\n \"\",\n ].join(\"\\n\");\n}\n\n/** A name usable as a `data-theme` value. */\nexport function slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug.length > 0 ? slug : \"custom\";\n}\n","import { encodeSrgb, oklchToLinearRgb, parseOklch, formatOklch } from \"./colour\";\nimport type { DerivedPreset, PresetMode } from \"./preset\";\n\n/**\n * The tokens, in the shape a design tool reads.\n *\n * The CSS is the source of truth and stays that way — nothing here is a second\n * palette to keep in step. This reads the same `tokens.css`, `base.css` and\n * preset files the components consume, resolves every `var()` the way a\n * browser would, and writes the result as W3C Design Tokens (DTCG): the format\n * Tokens Studio for Figma imports directly, and the one every other design\n * tool is converging on.\n *\n * Colours come out as sRGB hex. Figma has no OKLCH; converting here, with the\n * same maths the contrast audit uses, means the swatch a designer sees is the\n * colour a user gets — rather than whatever a tool makes of an `oklch()` string\n * it cannot parse.\n */\n\n/** A flat map of custom property name (without the leading `--`) to raw value. */\nexport type Declarations = Record<string, string>;\n\n/** A W3C Design Tokens document: nested groups whose leaves carry `$type` and `$value`. */\nexport interface DesignToken {\n $type: \"color\" | \"dimension\" | \"fontFamily\" | \"number\";\n $value: string | number | string[];\n $description?: string;\n}\n\nexport interface DesignTokenGroup {\n [key: string]: DesignToken | DesignTokenGroup | string | undefined;\n $description?: string;\n}\n\n/** Strips block comments, which otherwise hide `--x: y;` pairs inside them from the parser. */\nfunction stripComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\n/**\n * The declarations inside the first block whose selector is exactly `selector`.\n *\n * Exact, not substring: `.dark` must not match `.dark[data-theme=\"ocean\"]`, and\n * `:root` must not match `:root[dir=\"rtl\"]`. Whitespace inside a value is\n * collapsed, because a font stack written over four lines is one value.\n */\nexport function parseTokenCss(css: string, selector: string): Declarations {\n const clean = stripComments(css);\n const declarations: Declarations = {};\n\n let searchFrom = 0;\n for (;;) {\n const brace = clean.indexOf(\"{\", searchFrom);\n if (brace === -1) break;\n\n // The selector is whatever sits between the previous `}` (or `;`, or the\n // start) and this `{`.\n const start = Math.max(\n clean.lastIndexOf(\"}\", brace),\n clean.lastIndexOf(\";\", brace),\n clean.lastIndexOf(\"{\", brace - 1),\n );\n const candidate = clean.slice(start + 1, brace).trim();\n const end = clean.indexOf(\"}\", brace);\n if (end === -1) break;\n\n if (candidate === selector) {\n const body = clean.slice(brace + 1, end);\n for (const match of body.matchAll(/--([\\w-]+)\\s*:\\s*([^;]+);/g)) {\n const name = match[1];\n const value = match[2];\n if (name && value) declarations[name] = value.replace(/\\s+/g, \" \").trim();\n }\n return declarations;\n }\n\n searchFrom = end + 1;\n }\n\n return declarations;\n}\n\n/**\n * Resolves `var(--x)` references the way the cascade would.\n *\n * `scopes` are searched in order, so a mode's own declarations shadow the root's\n * and the root's shadow the raw scale — which is exactly what `.dark { --x }`\n * over `:root { --x }` over `@theme { --x }` means. A fallback inside the\n * `var()` is used when nothing defines the name, and an unresolvable reference\n * is left as written rather than silently dropped.\n */\nexport function resolveReferences(value: string, scopes: Declarations[]): string {\n let current = value;\n // Bounded, so a cycle terminates with the reference left in place rather\n // than hanging the build.\n for (let depth = 0; depth < 16; depth += 1) {\n const next = current.replace(\n /var\\(\\s*--([\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g,\n (whole, name: string, fallback: string | undefined) => {\n for (const scope of scopes) {\n const found = scope[name];\n if (found !== undefined) return found;\n }\n return fallback?.trim() ?? whole;\n },\n );\n if (next === current) return current;\n current = next;\n }\n return current;\n}\n\n/** `oklch(...)` to `#rrggbb`, or `#rrggbbaa` when it carries alpha. */\nexport function cssColourToHex(value: string): string | undefined {\n const parsed = parseOklch(value.trim());\n if (!parsed) return undefined;\n\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(parsed.l, parsed.c, parsed.h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n const rgb = `#${pair(r)}${pair(g)}${pair(b)}`;\n return parsed.alpha >= 1 ? rgb : `${rgb}${pair(Math.round(parsed.alpha * 255))}`;\n}\n\nconst ROOT_FONT_PX = 16;\n\n/**\n * A length in px, for the values the scale is written in.\n *\n * Handles the two shapes the tokens use: a plain `rem`/`px`, and the radius\n * ladder's `calc(<rem> * var(--radius-scale, 1))`, which is resolved with the\n * given scale so an exported theme carries the corner radius it was designed\n * with rather than a formula Figma cannot evaluate.\n */\nexport function cssLengthToPx(value: string, radiusScale = 1): number | undefined {\n const trimmed = value.trim();\n\n const calc = /^calc\\(\\s*([\\d.]+)rem\\s*\\*\\s*var\\(--radius-scale(?:,\\s*[\\d.]+)?\\)\\s*\\)$/.exec(\n trimmed,\n );\n if (calc?.[1]) return round(Number.parseFloat(calc[1]) * ROOT_FONT_PX * radiusScale);\n\n const rem = /^([\\d.]+)rem$/.exec(trimmed);\n if (rem?.[1]) return round(Number.parseFloat(rem[1]) * ROOT_FONT_PX);\n\n const px = /^([\\d.]+)px$/.exec(trimmed);\n if (px?.[1]) return round(Number.parseFloat(px[1]));\n\n return undefined;\n}\n\nfunction round(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\nfunction px(value: number): string {\n return `${String(value)}px`;\n}\n\n/** The four tokens a derived preset owns, as declarations, per mode. */\nexport function presetDeclarations(preset: DerivedPreset): {\n light: Declarations;\n dark: Declarations;\n} {\n const mode = (entry: PresetMode): Declarations => ({\n primary: formatOklch(entry.primary),\n \"primary-hover\": formatOklch(entry.primaryHover),\n \"primary-active\": formatOklch(entry.primaryActive),\n \"primary-foreground\": formatOklch(entry.primaryForeground),\n });\n return { light: mode(preset.light), dark: mode(preset.dark) };\n}\n\nexport interface DesignTokensInput {\n /** Named in the document, e.g. \"ocean\" or a studio preset's slug. */\n name: string;\n /** The `@theme` block of tokens.css: the raw scales. */\n scale: Declarations;\n /** The `:root` block of base.css. */\n light: Declarations;\n /** The `.dark` block of base.css. */\n dark: Declarations;\n /** A preset's overrides, layered over `light` and `dark`. */\n preset?: { light: Declarations; dark: Declarations };\n /** The `--radius-scale` the theme was designed at. */\n radiusScale?: number;\n}\n\nfunction colourGroup(\n declarations: Declarations,\n scopes: Declarations[],\n filter: (name: string) => boolean,\n): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(declarations)) {\n if (!filter(name)) continue;\n const hex = cssColourToHex(resolveReferences(raw, scopes));\n if (hex) group[name] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\n/** `--color-neutral-500` → `neutral.500`, nested. */\nfunction scaleColours(scale: Declarations): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^color-([a-z]+)-(\\d+)$/.exec(name);\n if (!match) continue;\n const [, family, step] = match;\n if (!family || !step) continue;\n const hex = cssColourToHex(raw);\n if (!hex) continue;\n const familyGroup = (group[family] ??= {}) as DesignTokenGroup;\n familyGroup[step] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\nfunction radii(scale: Declarations, radiusScale: number): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^radius-([\\w]+)$/.exec(name);\n if (!match?.[1]) continue;\n const length = cssLengthToPx(raw, radiusScale);\n if (length !== undefined) group[match[1]] = { $type: \"dimension\", $value: px(length) };\n }\n return group;\n}\n\nfunction typography(scale: Declarations): DesignTokenGroup {\n const family: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^font-([a-z]+)$/.exec(name);\n if (!match?.[1]) continue;\n family[match[1]] = {\n $type: \"fontFamily\",\n $value: raw.split(\",\").map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, \"\")),\n };\n }\n\n const size: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const step = /^text-([\\w]+)$/.exec(name)?.[1];\n if (!step) continue;\n const fontSize = cssLengthToPx(raw);\n if (fontSize === undefined) continue;\n const entry: DesignTokenGroup = { size: { $type: \"dimension\", $value: px(fontSize) } };\n const lineHeight = scale[`text-${step}--line-height`];\n const lineHeightPx = lineHeight === undefined ? undefined : cssLengthToPx(lineHeight);\n if (lineHeightPx !== undefined) {\n entry.lineHeight = { $type: \"dimension\", $value: px(lineHeightPx) };\n }\n size[step] = entry;\n }\n\n return { family, text: size };\n}\n\n/**\n * The whole theme, as a design-tokens document.\n *\n * Three sets: `core` (the raw scales, the same in both modes), `light` and\n * `dark` (the semantic colours, resolved). A designer enables `core` plus one\n * mode, which mirrors exactly how the CSS composes.\n */\nexport function toDesignTokens(input: DesignTokensInput): DesignTokenGroup {\n const radiusScale = input.radiusScale ?? 1;\n const light: Declarations = { ...input.light, ...input.preset?.light };\n const dark: Declarations = { ...input.dark, ...input.preset?.dark };\n\n const semantic = (name: string) => name !== \"radius-scale\" && !name.startsWith(\"color-\");\n\n return {\n $description: `Dowel design tokens, preset \"${input.name}\". Generated from the CSS the components use; colours are sRGB hex converted from OKLCH.`,\n core: {\n color: scaleColours(input.scale),\n radius: radii(input.scale, radiusScale),\n font: typography(input.scale),\n },\n light: {\n color: colourGroup(light, [light, input.scale], semantic),\n },\n dark: {\n // Dark declares only what differs; the rest inherits from the root.\n color: colourGroup({ ...light, ...dark }, [dark, light, input.scale], semantic),\n },\n };\n}\n","export {\n checkPreset,\n derivePreset,\n foregroundFor,\n formatPreset,\n slugify,\n TEXT_MINIMUM,\n type ContrastCheck,\n type DerivedPreset,\n type DeriveOptions,\n type PresetMode,\n} from \"./preset\";\n\nexport {\n cssColourToHex,\n cssLengthToPx,\n parseTokenCss,\n presetDeclarations,\n resolveReferences,\n toDesignTokens,\n type Declarations,\n type DesignToken,\n type DesignTokenGroup,\n type DesignTokensInput,\n} from \"./figma\";\n\nexport {\n composite,\n contrastRatio,\n decodeSrgb,\n encodeSrgb,\n formatOklch,\n hexToOklch,\n linearRgbToOklch,\n luminance,\n oklchToHex,\n oklchToLinearRgb,\n parseOklch,\n resolveColour,\n type Oklch,\n type Rgb,\n} from \"./colour\";\n\n/**\n * Typed surface of the theme layer. The CSS is the implementation; these\n * constants exist so theme switchers, the docs playground and the future CLI\n * all agree on the same vocabulary.\n */\n\nexport const THEME_PRESETS = [\n \"default\",\n \"ocean\",\n \"emerald\",\n \"violet\",\n \"rose\",\n \"amber\",\n \"monochrome\",\n] as const;\n\nexport type ThemePreset = (typeof THEME_PRESETS)[number];\n\nexport const COLOR_MODES = [\"light\", \"dark\", \"system\"] as const;\n\nexport type ColorMode = (typeof COLOR_MODES)[number];\n\n/** Attribute set on the <html> element to activate a preset. */\nexport const THEME_ATTRIBUTE = \"data-theme\";\n\n/** Class toggled on the <html> element for dark mode. */\nexport const DARK_CLASS = \"dark\";\n\n/**\n * Custom property that re-proportions every radius token at once.\n * `1` is the designed default; `0` gives fully square corners.\n */\nexport const RADIUS_SCALE_PROPERTY = \"--radius-scale\";\n\nexport function isThemePreset(value: string): value is ThemePreset {\n return (THEME_PRESETS as readonly string[]).includes(value);\n}\n\nexport function isColorMode(value: string): value is ColorMode {\n return (COLOR_MODES as readonly string[]).includes(value);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dowel-ui/themes",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Two-tier OKLCH design tokens in plain CSS for Dowel, with seven presets that all pass WCAG AA contrast in light and dark.",
6
6
  "keywords": [
@@ -33,7 +33,8 @@
33
33
  ],
34
34
  "files": [
35
35
  "dist",
36
- "src"
36
+ "src",
37
+ "!src/**/*.test.ts"
37
38
  ],
38
39
  "exports": {
39
40
  ".": {
@@ -53,11 +54,13 @@
53
54
  "tailwindcss": "4.3.3",
54
55
  "tsdown": "0.22.14",
55
56
  "typescript": "6.0.3",
56
- "@dowel-ui/config": "0.5.0"
57
+ "vitest": "4.1.10",
58
+ "@dowel-ui/config": "0.7.0"
57
59
  },
58
60
  "scripts": {
59
61
  "build": "tsdown",
60
62
  "typecheck": "tsc --noEmit",
61
- "clean": "rm -rf dist .turbo"
63
+ "clean": "rm -rf dist .turbo",
64
+ "test": "vitest run"
62
65
  }
63
66
  }