@conductor-by-89soone/tokens 0.6.1 → 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.d.ts +39 -1
- package/dist/index.js +171 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -90,4 +90,42 @@ interface TokenGroup {
|
|
|
90
90
|
readonly order: number;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* WCAG 2.1 contrast ratio (1–21) of `foreground` drawn on `background`.
|
|
95
|
+
*
|
|
96
|
+
* Accepts `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` and `hsl()`/`hsla()`, alone or as the single
|
|
97
|
+
* colour inside a compound value such as a box-shadow. A translucent
|
|
98
|
+
* foreground is composited over the background first; the background must be opaque, because what
|
|
99
|
+
* it sits on is unknown here. Throws on anything else rather than returning a ratio that could pass.
|
|
100
|
+
*/
|
|
101
|
+
declare function contrastRatio(foreground: string, background: string): number;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* OKLab mixing for derived state colours (CR-050).
|
|
105
|
+
*
|
|
106
|
+
* The stylesheet used to tint tone buttons with `color-mix(in srgb, …)` at render time. Mixing in
|
|
107
|
+
* gamma-encoded sRGB darkens and desaturates the midpoints, and a render-time mix is invisible to
|
|
108
|
+
* `checkContrast`. These tints are now computed here, at build time, in OKLab — the Cartesian form
|
|
109
|
+
* of OKLCH, so lightness is perceptually even — and emitted as plain hex. No browser has to support
|
|
110
|
+
* `oklch()` or relative colour syntax, and every derived background can sit in a contrast pair.
|
|
111
|
+
*
|
|
112
|
+
* OKLab rather than polar OKLCH: the surfaces are near-neutral, so their hue is noise, and an OKLCH
|
|
113
|
+
* mix would swing the tint's hue along that noise (a red mixed toward a blue-grey surface drifts
|
|
114
|
+
* purple). Mixing a and b linearly keeps the tone's hue and walks its chroma down evenly.
|
|
115
|
+
*
|
|
116
|
+
* Refs: CR-050 FR-THM-004 FR-TOK-001
|
|
117
|
+
*/
|
|
118
|
+
/** An OKLab (L, a, b) triple, or an sRGB triple in [0, 1] internally. */
|
|
119
|
+
type OklabColor = readonly [number, number, number];
|
|
120
|
+
/** sRGB → OKLab (Björn Ottosson's reference matrices). */
|
|
121
|
+
declare function toOklab(hex: string): OklabColor;
|
|
122
|
+
/** OKLab → sRGB hex, clipping each out-of-gamut channel. */
|
|
123
|
+
declare function fromOklab([lightness, a, b]: OklabColor): string;
|
|
124
|
+
/** `weight` of `foreground` over `background`, mixed in OKLab. 0 → background, 1 → foreground. */
|
|
125
|
+
declare function mixOklab(foreground: string, background: string, weight: number): string;
|
|
126
|
+
/** The two tones whose state tints are derived (`tone.accent.*`, `tone.danger.*`). */
|
|
127
|
+
type ToneName = "accent" | "danger";
|
|
128
|
+
/** `tone.<tone>.<state>` → hex for one theme, from that theme's tone colours and raised surface. */
|
|
129
|
+
declare function toneValues(tones: Readonly<Record<ToneName, string>>, surfaceRaised: string): Readonly<Record<string, string>>;
|
|
130
|
+
|
|
131
|
+
export { CDT_PREFIX, type OklabColor, PACKAGE_NAME, SEVERITY_ICONS, STATUS_ICONS, type SeverityIconName, type StatusIconName, type TokenDefinition, type TokenGroup, type TokenTier, type TokenUsage, type ToneName, contrastRatio, fromOklab, mixOklab, toOklab, toneValues };
|
package/dist/index.js
CHANGED
|
@@ -476,4 +476,174 @@ var SEVERITY_ICONS = {
|
|
|
476
476
|
blocked: "shield-x"
|
|
477
477
|
};
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
// src/contrast/color.ts
|
|
480
|
+
var HEX_SOURCE = "#([0-9a-f]{3,8})\\b";
|
|
481
|
+
var FUNCTIONAL_SOURCE = "\\b(rgba?|hsla?)\\(\\s*([^()]*)\\)";
|
|
482
|
+
var clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
483
|
+
function fromHex(digits) {
|
|
484
|
+
const byte = (pair) => parseInt(pair.length === 1 ? pair + pair : pair, 16);
|
|
485
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
486
|
+
const [r, g, b, a] = [...digits];
|
|
487
|
+
return { r: byte(r), g: byte(g), b: byte(b), alpha: a === void 0 ? 1 : byte(a) / 255 };
|
|
488
|
+
}
|
|
489
|
+
if (digits.length === 6 || digits.length === 8) {
|
|
490
|
+
const at = (index) => digits.slice(index, index + 2);
|
|
491
|
+
return {
|
|
492
|
+
r: byte(at(0)),
|
|
493
|
+
g: byte(at(2)),
|
|
494
|
+
b: byte(at(4)),
|
|
495
|
+
alpha: digits.length === 8 ? byte(at(6)) / 255 : 1
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
function argumentsOf(body) {
|
|
501
|
+
return body.replace(/\//g, " ").split(/[\s,]+/).filter((part) => part !== "");
|
|
502
|
+
}
|
|
503
|
+
function toAlpha(raw) {
|
|
504
|
+
if (raw === void 0) return 1;
|
|
505
|
+
const value = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 : Number(raw);
|
|
506
|
+
return Number.isFinite(value) ? clamp(value, 0, 1) : 1;
|
|
507
|
+
}
|
|
508
|
+
function fromRgb(body) {
|
|
509
|
+
const parts = argumentsOf(body);
|
|
510
|
+
if (parts.length < 3) return void 0;
|
|
511
|
+
const channel = (raw) => {
|
|
512
|
+
const value = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 * 255 : Number(raw);
|
|
513
|
+
return Number.isFinite(value) ? clamp(Math.round(value), 0, 255) : Number.NaN;
|
|
514
|
+
};
|
|
515
|
+
const r = channel(parts[0]);
|
|
516
|
+
const g = channel(parts[1]);
|
|
517
|
+
const b = channel(parts[2]);
|
|
518
|
+
if ([r, g, b].some(Number.isNaN)) return void 0;
|
|
519
|
+
return { r, g, b, alpha: toAlpha(parts[3]) };
|
|
520
|
+
}
|
|
521
|
+
function fromHsl(body) {
|
|
522
|
+
const parts = argumentsOf(body);
|
|
523
|
+
if (parts.length < 3) return void 0;
|
|
524
|
+
const hue = Number(parts[0].replace(/deg$/i, ""));
|
|
525
|
+
const saturation = Number(parts[1].replace(/%$/, "")) / 100;
|
|
526
|
+
const lightness = Number(parts[2].replace(/%$/, "")) / 100;
|
|
527
|
+
if (![hue, saturation, lightness].every((value) => Number.isFinite(value))) return void 0;
|
|
528
|
+
const chroma = (1 - Math.abs(2 * clamp(lightness, 0, 1) - 1)) * clamp(saturation, 0, 1);
|
|
529
|
+
const sextant = (hue % 360 + 360) % 360 / 60;
|
|
530
|
+
const secondary = chroma * (1 - Math.abs(sextant % 2 - 1));
|
|
531
|
+
const lift = clamp(lightness, 0, 1) - chroma / 2;
|
|
532
|
+
const wheel = [
|
|
533
|
+
[chroma, secondary, 0],
|
|
534
|
+
[secondary, chroma, 0],
|
|
535
|
+
[0, chroma, secondary],
|
|
536
|
+
[0, secondary, chroma],
|
|
537
|
+
[secondary, 0, chroma],
|
|
538
|
+
[chroma, 0, secondary]
|
|
539
|
+
];
|
|
540
|
+
const [r, g, b] = wheel[Math.floor(sextant) % 6];
|
|
541
|
+
const channel = (raw) => clamp(Math.round((raw + lift) * 255), 0, 255);
|
|
542
|
+
return { r: channel(r), g: channel(g), b: channel(b), alpha: toAlpha(parts[3]) };
|
|
543
|
+
}
|
|
544
|
+
function parseFunctional(name, body) {
|
|
545
|
+
return name.toLowerCase().startsWith("hsl") ? fromHsl(body) : fromRgb(body);
|
|
546
|
+
}
|
|
547
|
+
function findColors(value) {
|
|
548
|
+
const found = [];
|
|
549
|
+
for (const match of value.matchAll(new RegExp(FUNCTIONAL_SOURCE, "gi"))) {
|
|
550
|
+
const parsed = parseFunctional(match[1], match[2]);
|
|
551
|
+
if (parsed) found.push(parsed);
|
|
552
|
+
}
|
|
553
|
+
for (const match of value.matchAll(new RegExp(HEX_SOURCE, "gi"))) {
|
|
554
|
+
const parsed = fromHex(match[1]);
|
|
555
|
+
if (parsed) found.push(parsed);
|
|
556
|
+
}
|
|
557
|
+
return found;
|
|
558
|
+
}
|
|
559
|
+
function linearise(channel8) {
|
|
560
|
+
const channel = channel8 / 255;
|
|
561
|
+
return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
|
562
|
+
}
|
|
563
|
+
function relativeLuminance({ r, g, b }) {
|
|
564
|
+
return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b);
|
|
565
|
+
}
|
|
566
|
+
function contrastRatio(one, other) {
|
|
567
|
+
const first = relativeLuminance(one);
|
|
568
|
+
const second = relativeLuminance(other);
|
|
569
|
+
const lighter = Math.max(first, second);
|
|
570
|
+
const darker = Math.min(first, second);
|
|
571
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
572
|
+
}
|
|
573
|
+
function compositeOver(foreground, background) {
|
|
574
|
+
if (foreground.alpha >= 1) return foreground;
|
|
575
|
+
const mix = (front, back) => Math.round(front * foreground.alpha + back * (1 - foreground.alpha));
|
|
576
|
+
return {
|
|
577
|
+
r: mix(foreground.r, background.r),
|
|
578
|
+
g: mix(foreground.g, background.g),
|
|
579
|
+
b: mix(foreground.b, background.b),
|
|
580
|
+
alpha: 1
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/measure.ts
|
|
585
|
+
function parse(role, value) {
|
|
586
|
+
const colours = findColors(value);
|
|
587
|
+
if (colours.length !== 1) throw new Error(`contrastRatio: ${role} "${value}" must hold exactly one #hex, rgb() or hsl() colour, found ${colours.length}`);
|
|
588
|
+
return colours[0];
|
|
589
|
+
}
|
|
590
|
+
function contrastRatio2(foreground, background) {
|
|
591
|
+
const back = parse("background", background);
|
|
592
|
+
if (back.alpha < 1) throw new Error(`contrastRatio: background "${background}" is translucent; composite it onto its surface first`);
|
|
593
|
+
return contrastRatio(compositeOver(parse("foreground", foreground), back), back);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/oklab.ts
|
|
597
|
+
var HEX = /^#([0-9a-f]{6})$/i;
|
|
598
|
+
function parseHex(hex) {
|
|
599
|
+
const match = HEX.exec(hex);
|
|
600
|
+
if (match === null) throw new Error(`oklab: expected #rrggbb, got "${hex}"`);
|
|
601
|
+
const digits = match[1];
|
|
602
|
+
return [0, 2, 4].map((offset) => Number.parseInt(digits.slice(offset, offset + 2), 16) / 255);
|
|
603
|
+
}
|
|
604
|
+
var toLinear = (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
|
605
|
+
var toGamma = (channel) => channel <= 31308e-7 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - 0.055;
|
|
606
|
+
var clamp2 = (channel) => Math.min(1, Math.max(0, channel));
|
|
607
|
+
function toOklab(hex) {
|
|
608
|
+
const [r, g, b] = parseHex(hex).map(toLinear);
|
|
609
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
610
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
611
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
612
|
+
return [
|
|
613
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
614
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
615
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
function fromOklab([lightness, a, b]) {
|
|
619
|
+
const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
620
|
+
const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
621
|
+
const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
622
|
+
const linear = [
|
|
623
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
624
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
625
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
626
|
+
];
|
|
627
|
+
return `#${linear.map((channel) => Math.round(clamp2(toGamma(clamp2(channel))) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
628
|
+
}
|
|
629
|
+
function mixOklab(foreground, background, weight) {
|
|
630
|
+
if (!(weight >= 0 && weight <= 1)) throw new Error(`oklab: weight must be within [0, 1], got ${weight}`);
|
|
631
|
+
const front = toOklab(foreground);
|
|
632
|
+
const back = toOklab(background);
|
|
633
|
+
return fromOklab(front.map((channel, index) => channel * weight + back[index] * (1 - weight)));
|
|
634
|
+
}
|
|
635
|
+
var TONE_WEIGHTS = {
|
|
636
|
+
background: 0.04,
|
|
637
|
+
border: 0.14,
|
|
638
|
+
backgroundHover: 0.12,
|
|
639
|
+
borderHover: 0.45
|
|
640
|
+
};
|
|
641
|
+
function toneValues(tones, surfaceRaised) {
|
|
642
|
+
const out = {};
|
|
643
|
+
for (const tone of Object.keys(tones)) {
|
|
644
|
+
for (const [state, weight] of Object.entries(TONE_WEIGHTS)) out[`tone.${tone}.${state}`] = mixOklab(tones[tone], surfaceRaised, weight);
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export { CDT_PREFIX, PACKAGE_NAME, SEVERITY_ICONS, STATUS_ICONS, breakpoints, contrastRatio2 as contrastRatio, fromOklab, mixOklab, toOklab, tokens, toneValues };
|
package/package.json
CHANGED