@conductor-by-89soone/tokens 0.6.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/cli.js CHANGED
@@ -611,6 +611,59 @@ var componentTokens = [
611
611
  ...workbench
612
612
  ];
613
613
 
614
+ // src/oklab.ts
615
+ var HEX = /^#([0-9a-f]{6})$/i;
616
+ function parseHex(hex) {
617
+ const match = HEX.exec(hex);
618
+ if (match === null) throw new Error(`oklab: expected #rrggbb, got "${hex}"`);
619
+ const digits = match[1];
620
+ return [0, 2, 4].map((offset) => Number.parseInt(digits.slice(offset, offset + 2), 16) / 255);
621
+ }
622
+ var toLinear = (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
623
+ var toGamma = (channel) => channel <= 31308e-7 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - 0.055;
624
+ var clamp = (channel) => Math.min(1, Math.max(0, channel));
625
+ function toOklab(hex) {
626
+ const [r, g, b] = parseHex(hex).map(toLinear);
627
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
628
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
629
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
630
+ return [
631
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
632
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
633
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
634
+ ];
635
+ }
636
+ function fromOklab([lightness, a, b]) {
637
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
638
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
639
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
640
+ const linear = [
641
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
642
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
643
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
644
+ ];
645
+ return `#${linear.map((channel) => Math.round(clamp(toGamma(clamp(channel))) * 255).toString(16).padStart(2, "0")).join("")}`;
646
+ }
647
+ function mixOklab(foreground, background, weight) {
648
+ if (!(weight >= 0 && weight <= 1)) throw new Error(`oklab: weight must be within [0, 1], got ${weight}`);
649
+ const front = toOklab(foreground);
650
+ const back = toOklab(background);
651
+ return fromOklab(front.map((channel, index) => channel * weight + back[index] * (1 - weight)));
652
+ }
653
+ var TONE_WEIGHTS = {
654
+ background: 0.04,
655
+ border: 0.14,
656
+ backgroundHover: 0.12,
657
+ borderHover: 0.45
658
+ };
659
+ function toneValues(tones, surfaceRaised) {
660
+ const out = {};
661
+ for (const tone2 of Object.keys(tones)) {
662
+ for (const [state, weight] of Object.entries(TONE_WEIGHTS)) out[`tone.${tone2}.${state}`] = mixOklab(tones[tone2], surfaceRaised, weight);
663
+ }
664
+ return out;
665
+ }
666
+
614
667
  // src/palette.dark.ts
615
668
  function darkToken(input2) {
616
669
  return { tier: "semantic", ...input2 };
@@ -1104,6 +1157,27 @@ var dataviz = [
1104
1157
  description: "Sequential distribution step 5 of 5 (most prominent), single-hue luminance ramp. Fill only; non-text 3:1 on chart surfaces (CP-115~CP-117)."
1105
1158
  })
1106
1159
  ];
1160
+ var TONE_STATE_DESCRIPTIONS = {
1161
+ background: "rest fill",
1162
+ border: "rest edge",
1163
+ backgroundHover: "hover fill",
1164
+ borderHover: "hover edge"
1165
+ };
1166
+ function valueOf(tokens, key) {
1167
+ const value2 = tokens.find((token) => token.key === key)?.value;
1168
+ if (typeof value2 !== "string") throw new Error(`palette.dark: tone base "${key}" has no literal colour value`);
1169
+ return value2;
1170
+ }
1171
+ var toneBases = { accent: valueOf(accent, "accent.DEFAULT"), danger: valueOf(status, "status.danger") };
1172
+ var tone = Object.entries(toneValues(toneBases, valueOf(surface, "surface.raised"))).map(([key, value2]) => {
1173
+ const [, toneName, state] = key.split(".");
1174
+ return darkToken({
1175
+ key,
1176
+ usage: "decorative",
1177
+ value: value2,
1178
+ description: `Secondary ${toneName}-tone button ${TONE_STATE_DESCRIPTIONS[state]}: ${Math.round(TONE_WEIGHTS[state] * 100)}% of \`${toneName === "accent" ? "accent" : "status.danger"}\` over \`surface.raised\`, mixed in OKLab at build time (CR-050).`
1179
+ });
1180
+ });
1107
1181
  var darkPalette = [
1108
1182
  ...surface,
1109
1183
  ...text,
@@ -1115,11 +1189,12 @@ var darkPalette = [
1115
1189
  ...fontFamily,
1116
1190
  ...elevation,
1117
1191
  ...interaction,
1192
+ ...tone,
1118
1193
  ...dataviz
1119
1194
  ];
1120
1195
 
1121
1196
  // src/palette.light.ts
1122
- var values = {
1197
+ var baseValues = {
1123
1198
  "surface.base": "#e8ecf2",
1124
1199
  "surface.canvas": "#eef1f6",
1125
1200
  "surface.subtle": "#f3f6f9",
@@ -1189,6 +1264,13 @@ var values = {
1189
1264
  "dataviz.sequential.4": "#4539cd",
1190
1265
  "dataviz.sequential.5": "#3329a4"
1191
1266
  };
1267
+ var values = {
1268
+ ...baseValues,
1269
+ ...toneValues(
1270
+ { accent: baseValues["accent.DEFAULT"], danger: baseValues["status.danger"] },
1271
+ baseValues["surface.raised"]
1272
+ )
1273
+ };
1192
1274
  var lightAdditions = [];
1193
1275
  function derivePalette(base, overrides, additions = []) {
1194
1276
  const baseKeys = new Set(base.map((token) => token.key));
@@ -362,6 +362,12 @@ var datavizPairs = [
362
362
  pair("CP-116", "dataviz.sequential.5", "surface.canvas", "nonText"),
363
363
  pair("CP-117", "dataviz.sequential.5", "surface.raised", "nonText")
364
364
  ];
365
+ var tonePairs = [
366
+ pair("CP-118", "text.primary", "tone.accent.background", "body"),
367
+ pair("CP-119", "text.primary", "tone.accent.backgroundHover", "body"),
368
+ pair("CP-120", "text.primary", "tone.danger.background", "body"),
369
+ pair("CP-121", "text.primary", "tone.danger.backgroundHover", "body")
370
+ ];
365
371
  var contrastPairs = [
366
372
  ...textOnSurface,
367
373
  ...accentPairs,
@@ -371,7 +377,8 @@ var contrastPairs = [
371
377
  ...severityPairs,
372
378
  ...badgePairs,
373
379
  ...disabledPairs,
374
- ...datavizPairs
380
+ ...datavizPairs,
381
+ ...tonePairs
375
382
  ];
376
383
  var forbiddenPairs = [
377
384
  {
@@ -832,6 +839,59 @@ var componentTokens = [
832
839
  ...workbench
833
840
  ];
834
841
 
842
+ // src/oklab.ts
843
+ var HEX = /^#([0-9a-f]{6})$/i;
844
+ function parseHex(hex) {
845
+ const match = HEX.exec(hex);
846
+ if (match === null) throw new Error(`oklab: expected #rrggbb, got "${hex}"`);
847
+ const digits = match[1];
848
+ return [0, 2, 4].map((offset) => Number.parseInt(digits.slice(offset, offset + 2), 16) / 255);
849
+ }
850
+ var toLinear = (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
851
+ var toGamma = (channel) => channel <= 31308e-7 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - 0.055;
852
+ var clamp = (channel) => Math.min(1, Math.max(0, channel));
853
+ function toOklab(hex) {
854
+ const [r, g, b] = parseHex(hex).map(toLinear);
855
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
856
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
857
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
858
+ return [
859
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
860
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
861
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
862
+ ];
863
+ }
864
+ function fromOklab([lightness, a, b]) {
865
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
866
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
867
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
868
+ const linear = [
869
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
870
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
871
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
872
+ ];
873
+ return `#${linear.map((channel) => Math.round(clamp(toGamma(clamp(channel))) * 255).toString(16).padStart(2, "0")).join("")}`;
874
+ }
875
+ function mixOklab(foreground, background, weight) {
876
+ if (!(weight >= 0 && weight <= 1)) throw new Error(`oklab: weight must be within [0, 1], got ${weight}`);
877
+ const front = toOklab(foreground);
878
+ const back = toOklab(background);
879
+ return fromOklab(front.map((channel, index) => channel * weight + back[index] * (1 - weight)));
880
+ }
881
+ var TONE_WEIGHTS = {
882
+ background: 0.04,
883
+ border: 0.14,
884
+ backgroundHover: 0.12,
885
+ borderHover: 0.45
886
+ };
887
+ function toneValues(tones, surfaceRaised) {
888
+ const out = {};
889
+ for (const tone2 of Object.keys(tones)) {
890
+ for (const [state, weight] of Object.entries(TONE_WEIGHTS)) out[`tone.${tone2}.${state}`] = mixOklab(tones[tone2], surfaceRaised, weight);
891
+ }
892
+ return out;
893
+ }
894
+
835
895
  // src/palette.dark.ts
836
896
  function darkToken(input2) {
837
897
  return { tier: "semantic", ...input2 };
@@ -1325,6 +1385,27 @@ var dataviz = [
1325
1385
  description: "Sequential distribution step 5 of 5 (most prominent), single-hue luminance ramp. Fill only; non-text 3:1 on chart surfaces (CP-115~CP-117)."
1326
1386
  })
1327
1387
  ];
1388
+ var TONE_STATE_DESCRIPTIONS = {
1389
+ background: "rest fill",
1390
+ border: "rest edge",
1391
+ backgroundHover: "hover fill",
1392
+ borderHover: "hover edge"
1393
+ };
1394
+ function valueOf(tokens, key) {
1395
+ const value2 = tokens.find((token) => token.key === key)?.value;
1396
+ if (typeof value2 !== "string") throw new Error(`palette.dark: tone base "${key}" has no literal colour value`);
1397
+ return value2;
1398
+ }
1399
+ var toneBases = { accent: valueOf(accent, "accent.DEFAULT"), danger: valueOf(status, "status.danger") };
1400
+ var tone = Object.entries(toneValues(toneBases, valueOf(surface, "surface.raised"))).map(([key, value2]) => {
1401
+ const [, toneName, state] = key.split(".");
1402
+ return darkToken({
1403
+ key,
1404
+ usage: "decorative",
1405
+ value: value2,
1406
+ description: `Secondary ${toneName}-tone button ${TONE_STATE_DESCRIPTIONS[state]}: ${Math.round(TONE_WEIGHTS[state] * 100)}% of \`${toneName === "accent" ? "accent" : "status.danger"}\` over \`surface.raised\`, mixed in OKLab at build time (CR-050).`
1407
+ });
1408
+ });
1328
1409
  var darkPalette = [
1329
1410
  ...surface,
1330
1411
  ...text,
@@ -1336,11 +1417,12 @@ var darkPalette = [
1336
1417
  ...fontFamily,
1337
1418
  ...elevation,
1338
1419
  ...interaction,
1420
+ ...tone,
1339
1421
  ...dataviz
1340
1422
  ];
1341
1423
 
1342
1424
  // src/palette.light.ts
1343
- var values = {
1425
+ var baseValues = {
1344
1426
  "surface.base": "#e8ecf2",
1345
1427
  "surface.canvas": "#eef1f6",
1346
1428
  "surface.subtle": "#f3f6f9",
@@ -1410,6 +1492,13 @@ var values = {
1410
1492
  "dataviz.sequential.4": "#4539cd",
1411
1493
  "dataviz.sequential.5": "#3329a4"
1412
1494
  };
1495
+ var values = {
1496
+ ...baseValues,
1497
+ ...toneValues(
1498
+ { accent: baseValues["accent.DEFAULT"], danger: baseValues["status.danger"] },
1499
+ baseValues["surface.raised"]
1500
+ )
1501
+ };
1413
1502
  var lightAdditions = [];
1414
1503
  function derivePalette(base, overrides, additions = []) {
1415
1504
  const baseKeys = new Set(base.map((token) => token.key));
@@ -1602,7 +1691,7 @@ function tokensForTheme(theme) {
1602
1691
  // src/contrast/color.ts
1603
1692
  var HEX_SOURCE = "#([0-9a-f]{3,8})\\b";
1604
1693
  var FUNCTIONAL_SOURCE = "\\b(rgba?|hsla?)\\(\\s*([^()]*)\\)";
1605
- var clamp = (value2, low, high) => Math.min(Math.max(value2, low), high);
1694
+ var clamp2 = (value2, low, high) => Math.min(Math.max(value2, low), high);
1606
1695
  function fromHex(digits) {
1607
1696
  const byte = (pair2) => parseInt(pair2.length === 1 ? pair2 + pair2 : pair2, 16);
1608
1697
  if (digits.length === 3 || digits.length === 4) {
@@ -1626,14 +1715,14 @@ function argumentsOf(body) {
1626
1715
  function toAlpha(raw) {
1627
1716
  if (raw === void 0) return 1;
1628
1717
  const value2 = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 : Number(raw);
1629
- return Number.isFinite(value2) ? clamp(value2, 0, 1) : 1;
1718
+ return Number.isFinite(value2) ? clamp2(value2, 0, 1) : 1;
1630
1719
  }
1631
1720
  function fromRgb(body) {
1632
1721
  const parts = argumentsOf(body);
1633
1722
  if (parts.length < 3) return void 0;
1634
1723
  const channel = (raw) => {
1635
1724
  const value2 = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 * 255 : Number(raw);
1636
- return Number.isFinite(value2) ? clamp(Math.round(value2), 0, 255) : Number.NaN;
1725
+ return Number.isFinite(value2) ? clamp2(Math.round(value2), 0, 255) : Number.NaN;
1637
1726
  };
1638
1727
  const r = channel(parts[0]);
1639
1728
  const g = channel(parts[1]);
@@ -1648,10 +1737,10 @@ function fromHsl(body) {
1648
1737
  const saturation = Number(parts[1].replace(/%$/, "")) / 100;
1649
1738
  const lightness = Number(parts[2].replace(/%$/, "")) / 100;
1650
1739
  if (![hue, saturation, lightness].every((value2) => Number.isFinite(value2))) return void 0;
1651
- const chroma = (1 - Math.abs(2 * clamp(lightness, 0, 1) - 1)) * clamp(saturation, 0, 1);
1740
+ const chroma = (1 - Math.abs(2 * clamp2(lightness, 0, 1) - 1)) * clamp2(saturation, 0, 1);
1652
1741
  const sextant = (hue % 360 + 360) % 360 / 60;
1653
1742
  const secondary = chroma * (1 - Math.abs(sextant % 2 - 1));
1654
- const lift = clamp(lightness, 0, 1) - chroma / 2;
1743
+ const lift = clamp2(lightness, 0, 1) - chroma / 2;
1655
1744
  const wheel = [
1656
1745
  [chroma, secondary, 0],
1657
1746
  [secondary, chroma, 0],
@@ -1661,7 +1750,7 @@ function fromHsl(body) {
1661
1750
  [chroma, 0, secondary]
1662
1751
  ];
1663
1752
  const [r, g, b] = wheel[Math.floor(sextant) % 6];
1664
- const channel = (raw) => clamp(Math.round((raw + lift) * 255), 0, 255);
1753
+ const channel = (raw) => clamp2(Math.round((raw + lift) * 255), 0, 255);
1665
1754
  return { r: channel(r), g: channel(g), b: channel(b), alpha: toAlpha(parts[3]) };
1666
1755
  }
1667
1756
  function parseFunctional(name, body) {
@@ -10,11 +10,11 @@
10
10
  "nonText": 3
11
11
  },
12
12
  "summary": {
13
- "declaredPairs": 116,
14
- "checks": 232,
15
- "passed": 232,
13
+ "declaredPairs": 120,
14
+ "checks": 240,
15
+ "passed": 240,
16
16
  "failed": 0,
17
- "excludedTokens": 241,
17
+ "excludedTokens": 249,
18
18
  "forbiddenPairs": 2
19
19
  },
20
20
  "results": [
@@ -1526,6 +1526,58 @@
1526
1526
  "ratio": 10.836,
1527
1527
  "pass": true
1528
1528
  },
1529
+ {
1530
+ "id": "CP-118",
1531
+ "theme": "dark",
1532
+ "foreground": "text.primary",
1533
+ "background": "tone.accent.background",
1534
+ "foregroundValue": "#f4f7fb",
1535
+ "backgroundValue": "#172131",
1536
+ "compositedForeground": "#f4f7fb",
1537
+ "usage": "body",
1538
+ "threshold": 4.5,
1539
+ "ratio": 15.0532,
1540
+ "pass": true
1541
+ },
1542
+ {
1543
+ "id": "CP-119",
1544
+ "theme": "dark",
1545
+ "foreground": "text.primary",
1546
+ "background": "tone.accent.backgroundHover",
1547
+ "foregroundValue": "#f4f7fb",
1548
+ "backgroundValue": "#1d2840",
1549
+ "compositedForeground": "#f4f7fb",
1550
+ "usage": "body",
1551
+ "threshold": 4.5,
1552
+ "ratio": 13.6691,
1553
+ "pass": true
1554
+ },
1555
+ {
1556
+ "id": "CP-120",
1557
+ "theme": "dark",
1558
+ "foreground": "text.primary",
1559
+ "background": "tone.danger.background",
1560
+ "foregroundValue": "#f4f7fb",
1561
+ "backgroundValue": "#1d202c",
1562
+ "compositedForeground": "#f4f7fb",
1563
+ "usage": "body",
1564
+ "threshold": 4.5,
1565
+ "ratio": 15.0893,
1566
+ "pass": true
1567
+ },
1568
+ {
1569
+ "id": "CP-121",
1570
+ "theme": "dark",
1571
+ "foreground": "text.primary",
1572
+ "background": "tone.danger.backgroundHover",
1573
+ "foregroundValue": "#f4f7fb",
1574
+ "backgroundValue": "#2d252f",
1575
+ "compositedForeground": "#f4f7fb",
1576
+ "usage": "body",
1577
+ "threshold": 4.5,
1578
+ "ratio": 13.79,
1579
+ "pass": true
1580
+ },
1529
1581
  {
1530
1582
  "id": "CP-001",
1531
1583
  "theme": "light",
@@ -3033,6 +3085,58 @@
3033
3085
  "threshold": 3,
3034
3086
  "ratio": 10.2529,
3035
3087
  "pass": true
3088
+ },
3089
+ {
3090
+ "id": "CP-118",
3091
+ "theme": "light",
3092
+ "foreground": "text.primary",
3093
+ "background": "tone.accent.background",
3094
+ "foregroundValue": "#0c121c",
3095
+ "backgroundValue": "#f3f6fc",
3096
+ "compositedForeground": "#0c121c",
3097
+ "usage": "body",
3098
+ "threshold": 4.5,
3099
+ "ratio": 17.3371,
3100
+ "pass": true
3101
+ },
3102
+ {
3103
+ "id": "CP-119",
3104
+ "theme": "light",
3105
+ "foreground": "text.primary",
3106
+ "background": "tone.accent.backgroundHover",
3107
+ "foregroundValue": "#0c121c",
3108
+ "backgroundValue": "#e4e9fa",
3109
+ "compositedForeground": "#0c121c",
3110
+ "usage": "body",
3111
+ "threshold": 4.5,
3112
+ "ratio": 15.4924,
3113
+ "pass": true
3114
+ },
3115
+ {
3116
+ "id": "CP-120",
3117
+ "theme": "light",
3118
+ "foreground": "text.primary",
3119
+ "background": "tone.danger.background",
3120
+ "foregroundValue": "#0c121c",
3121
+ "backgroundValue": "#fbf4f4",
3122
+ "compositedForeground": "#0c121c",
3123
+ "usage": "body",
3124
+ "threshold": 4.5,
3125
+ "ratio": 17.292,
3126
+ "pass": true
3127
+ },
3128
+ {
3129
+ "id": "CP-121",
3130
+ "theme": "light",
3131
+ "foreground": "text.primary",
3132
+ "background": "tone.danger.backgroundHover",
3133
+ "foregroundValue": "#0c121c",
3134
+ "backgroundValue": "#f9e4e2",
3135
+ "compositedForeground": "#0c121c",
3136
+ "usage": "body",
3137
+ "threshold": 4.5,
3138
+ "ratio": 15.3929,
3139
+ "pass": true
3036
3140
  }
3037
3141
  ],
3038
3142
  "exclusions": [
@@ -3176,6 +3280,46 @@
3176
3280
  "usage": "decorative",
3177
3281
  "reason": "Policy-blocked fill. Contrast-exempt as a fill; checked beneath the label (5.27:1)."
3178
3282
  },
3283
+ {
3284
+ "key": "tone.accent.background",
3285
+ "usage": "decorative",
3286
+ "reason": "Secondary accent-tone button rest fill: 4% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3287
+ },
3288
+ {
3289
+ "key": "tone.accent.border",
3290
+ "usage": "decorative",
3291
+ "reason": "Secondary accent-tone button rest edge: 14% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3292
+ },
3293
+ {
3294
+ "key": "tone.accent.backgroundHover",
3295
+ "usage": "decorative",
3296
+ "reason": "Secondary accent-tone button hover fill: 12% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3297
+ },
3298
+ {
3299
+ "key": "tone.accent.borderHover",
3300
+ "usage": "decorative",
3301
+ "reason": "Secondary accent-tone button hover edge: 45% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3302
+ },
3303
+ {
3304
+ "key": "tone.danger.background",
3305
+ "usage": "decorative",
3306
+ "reason": "Secondary danger-tone button rest fill: 4% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3307
+ },
3308
+ {
3309
+ "key": "tone.danger.border",
3310
+ "usage": "decorative",
3311
+ "reason": "Secondary danger-tone button rest edge: 14% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3312
+ },
3313
+ {
3314
+ "key": "tone.danger.backgroundHover",
3315
+ "usage": "decorative",
3316
+ "reason": "Secondary danger-tone button hover fill: 12% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3317
+ },
3318
+ {
3319
+ "key": "tone.danger.borderHover",
3320
+ "usage": "decorative",
3321
+ "reason": "Secondary danger-tone button hover edge: 45% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050)."
3322
+ },
3179
3323
  {
3180
3324
  "key": "font.size.2xs",
3181
3325
  "usage": "decorative",
package/dist/index.d.ts CHANGED
@@ -90,4 +90,42 @@ interface TokenGroup {
90
90
  readonly order: number;
91
91
  }
92
92
 
93
- export { CDT_PREFIX, PACKAGE_NAME, SEVERITY_ICONS, STATUS_ICONS, type SeverityIconName, type StatusIconName, type TokenDefinition, type TokenGroup, type TokenTier, type TokenUsage };
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
@@ -103,6 +103,20 @@ var tokens = {
103
103
  disabled: "var(--cdt-state-disabled)",
104
104
  disabledPolicy: "var(--cdt-state-disabled-policy)"
105
105
  },
106
+ tone: {
107
+ accent: {
108
+ background: "var(--cdt-tone-accent-background)",
109
+ border: "var(--cdt-tone-accent-border)",
110
+ backgroundHover: "var(--cdt-tone-accent-background-hover)",
111
+ borderHover: "var(--cdt-tone-accent-border-hover)"
112
+ },
113
+ danger: {
114
+ background: "var(--cdt-tone-danger-background)",
115
+ border: "var(--cdt-tone-danger-border)",
116
+ backgroundHover: "var(--cdt-tone-danger-background-hover)",
117
+ borderHover: "var(--cdt-tone-danger-border-hover)"
118
+ }
119
+ },
106
120
  dataviz: {
107
121
  series: {
108
122
  "1": "var(--cdt-dataviz-series-1)",
@@ -462,4 +476,174 @@ var SEVERITY_ICONS = {
462
476
  blocked: "shield-x"
463
477
  };
464
478
 
465
- export { CDT_PREFIX, PACKAGE_NAME, SEVERITY_ICONS, STATUS_ICONS, breakpoints, tokens };
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/dist/tokens.css CHANGED
@@ -69,6 +69,15 @@
69
69
  --cdt-state-disabled: #18202c;
70
70
  --cdt-state-disabled-policy: #422006;
71
71
 
72
+ --cdt-tone-accent-background: #172131;
73
+ --cdt-tone-accent-border: #1f2944;
74
+ --cdt-tone-accent-background-hover: #1d2840;
75
+ --cdt-tone-accent-border-hover: #394682;
76
+ --cdt-tone-danger-background: #1d202c;
77
+ --cdt-tone-danger-border: #312630;
78
+ --cdt-tone-danger-background-hover: #2d252f;
79
+ --cdt-tone-danger-border-hover: #72363a;
80
+
72
81
  --cdt-dataviz-series-1: #e88171;
73
82
  --cdt-dataviz-series-2: #1db198;
74
83
  --cdt-dataviz-series-3: #e979ad;
@@ -427,6 +436,15 @@
427
436
  --cdt-state-disabled: #e2e8f0;
428
437
  --cdt-state-disabled-policy: #fef3c7;
429
438
 
439
+ --cdt-tone-accent-background: #f3f6fc;
440
+ --cdt-tone-accent-border: #e0e6fa;
441
+ --cdt-tone-accent-background-hover: #e4e9fa;
442
+ --cdt-tone-accent-border-hover: #a7b5ef;
443
+ --cdt-tone-danger-background: #fbf4f4;
444
+ --cdt-tone-danger-border: #f9e0dd;
445
+ --cdt-tone-danger-background-hover: #f9e4e2;
446
+ --cdt-tone-danger-border-hover: #eea39a;
447
+
430
448
  --cdt-dataviz-series-1: #d45946;
431
449
  --cdt-dataviz-series-2: #228f7d;
432
450
  --cdt-dataviz-series-3: #d54e8d;
@@ -786,6 +804,15 @@
786
804
  --cdt-state-disabled: #e2e8f0;
787
805
  --cdt-state-disabled-policy: #fef3c7;
788
806
 
807
+ --cdt-tone-accent-background: #f3f6fc;
808
+ --cdt-tone-accent-border: #e0e6fa;
809
+ --cdt-tone-accent-background-hover: #e4e9fa;
810
+ --cdt-tone-accent-border-hover: #a7b5ef;
811
+ --cdt-tone-danger-background: #fbf4f4;
812
+ --cdt-tone-danger-border: #f9e0dd;
813
+ --cdt-tone-danger-background-hover: #f9e4e2;
814
+ --cdt-tone-danger-border-hover: #eea39a;
815
+
789
816
  --cdt-dataviz-series-1: #d45946;
790
817
  --cdt-dataviz-series-2: #228f7d;
791
818
  --cdt-dataviz-series-3: #d54e8d;
package/dist/tokens.d.ts CHANGED
@@ -105,6 +105,20 @@ declare const tokens: {
105
105
  readonly disabled: "var(--cdt-state-disabled)";
106
106
  readonly disabledPolicy: "var(--cdt-state-disabled-policy)";
107
107
  };
108
+ readonly tone: {
109
+ readonly accent: {
110
+ readonly background: "var(--cdt-tone-accent-background)";
111
+ readonly border: "var(--cdt-tone-accent-border)";
112
+ readonly backgroundHover: "var(--cdt-tone-accent-background-hover)";
113
+ readonly borderHover: "var(--cdt-tone-accent-border-hover)";
114
+ };
115
+ readonly danger: {
116
+ readonly background: "var(--cdt-tone-danger-background)";
117
+ readonly border: "var(--cdt-tone-danger-border)";
118
+ readonly backgroundHover: "var(--cdt-tone-danger-background-hover)";
119
+ readonly borderHover: "var(--cdt-tone-danger-border-hover)";
120
+ };
121
+ };
108
122
  readonly dataviz: {
109
123
  readonly series: {
110
124
  readonly "1": "var(--cdt-dataviz-series-1)";
package/dist/tokens.js CHANGED
@@ -99,6 +99,20 @@ var tokens = {
99
99
  disabled: "var(--cdt-state-disabled)",
100
100
  disabledPolicy: "var(--cdt-state-disabled-policy)"
101
101
  },
102
+ tone: {
103
+ accent: {
104
+ background: "var(--cdt-tone-accent-background)",
105
+ border: "var(--cdt-tone-accent-border)",
106
+ backgroundHover: "var(--cdt-tone-accent-background-hover)",
107
+ borderHover: "var(--cdt-tone-accent-border-hover)"
108
+ },
109
+ danger: {
110
+ background: "var(--cdt-tone-danger-background)",
111
+ border: "var(--cdt-tone-danger-border)",
112
+ backgroundHover: "var(--cdt-tone-danger-background-hover)",
113
+ borderHover: "var(--cdt-tone-danger-border-hover)"
114
+ }
115
+ },
102
116
  dataviz: {
103
117
  series: {
104
118
  "1": "var(--cdt-dataviz-series-1)",
package/dist/tokens.json CHANGED
@@ -582,6 +582,94 @@
582
582
  "light": "#fef3c7"
583
583
  }
584
584
  },
585
+ {
586
+ "key": "tone.accent.background",
587
+ "cssName": "--cdt-tone-accent-background",
588
+ "tier": "semantic",
589
+ "usage": "decorative",
590
+ "description": "Secondary accent-tone button rest fill: 4% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050).",
591
+ "values": {
592
+ "dark": "#172131",
593
+ "light": "#f3f6fc"
594
+ }
595
+ },
596
+ {
597
+ "key": "tone.accent.border",
598
+ "cssName": "--cdt-tone-accent-border",
599
+ "tier": "semantic",
600
+ "usage": "decorative",
601
+ "description": "Secondary accent-tone button rest edge: 14% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050).",
602
+ "values": {
603
+ "dark": "#1f2944",
604
+ "light": "#e0e6fa"
605
+ }
606
+ },
607
+ {
608
+ "key": "tone.accent.backgroundHover",
609
+ "cssName": "--cdt-tone-accent-background-hover",
610
+ "tier": "semantic",
611
+ "usage": "decorative",
612
+ "description": "Secondary accent-tone button hover fill: 12% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050).",
613
+ "values": {
614
+ "dark": "#1d2840",
615
+ "light": "#e4e9fa"
616
+ }
617
+ },
618
+ {
619
+ "key": "tone.accent.borderHover",
620
+ "cssName": "--cdt-tone-accent-border-hover",
621
+ "tier": "semantic",
622
+ "usage": "decorative",
623
+ "description": "Secondary accent-tone button hover edge: 45% of `accent` over `surface.raised`, mixed in OKLab at build time (CR-050).",
624
+ "values": {
625
+ "dark": "#394682",
626
+ "light": "#a7b5ef"
627
+ }
628
+ },
629
+ {
630
+ "key": "tone.danger.background",
631
+ "cssName": "--cdt-tone-danger-background",
632
+ "tier": "semantic",
633
+ "usage": "decorative",
634
+ "description": "Secondary danger-tone button rest fill: 4% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050).",
635
+ "values": {
636
+ "dark": "#1d202c",
637
+ "light": "#fbf4f4"
638
+ }
639
+ },
640
+ {
641
+ "key": "tone.danger.border",
642
+ "cssName": "--cdt-tone-danger-border",
643
+ "tier": "semantic",
644
+ "usage": "decorative",
645
+ "description": "Secondary danger-tone button rest edge: 14% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050).",
646
+ "values": {
647
+ "dark": "#312630",
648
+ "light": "#f9e0dd"
649
+ }
650
+ },
651
+ {
652
+ "key": "tone.danger.backgroundHover",
653
+ "cssName": "--cdt-tone-danger-background-hover",
654
+ "tier": "semantic",
655
+ "usage": "decorative",
656
+ "description": "Secondary danger-tone button hover fill: 12% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050).",
657
+ "values": {
658
+ "dark": "#2d252f",
659
+ "light": "#f9e4e2"
660
+ }
661
+ },
662
+ {
663
+ "key": "tone.danger.borderHover",
664
+ "cssName": "--cdt-tone-danger-border-hover",
665
+ "tier": "semantic",
666
+ "usage": "decorative",
667
+ "description": "Secondary danger-tone button hover edge: 45% of `status.danger` over `surface.raised`, mixed in OKLab at build time (CR-050).",
668
+ "values": {
669
+ "dark": "#72363a",
670
+ "light": "#eea39a"
671
+ }
672
+ },
585
673
  {
586
674
  "key": "dataviz.series.1",
587
675
  "cssName": "--cdt-dataviz-series-1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conductor-by-89soone/tokens",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Theme-aware design tokens and validation CLIs for Conductor.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://89sooner.github.io/design-system/",