@tenphi/glaze 0.15.1 → 0.16.1

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.cjs CHANGED
@@ -590,6 +590,24 @@ function formatOkhsl(h, s, l, pastel = false) {
590
590
  return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
591
591
  }
592
592
  /**
593
+ * Format OKHST values as a CSS `okhst(H S% T%)` string.
594
+ * h: 0–360, s: 0–100, t: 0–100 (percentage scale for s and t).
595
+ *
596
+ * Pastel recompute matches `formatOkhsl`: convert via OKLab so external
597
+ * parsers that only understand non-pastel OKHST render identically.
598
+ */
599
+ function formatOkhst(h, s, t, pastel = false) {
600
+ let outS = s;
601
+ if (pastel) {
602
+ const REF_EPS = .05;
603
+ const den = Math.log(1 + REF_EPS) - Math.log(REF_EPS);
604
+ const y = Math.exp(t / 100 * den + Math.log(REF_EPS)) - REF_EPS;
605
+ const l = toe(Math.cbrt(Math.max(0, y)));
606
+ outS = oklabToOkhsl(okhslToOklab(h, s / 100, l, true), false)[1] * 100;
607
+ }
608
+ return `okhst(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(t, 2)}%)`;
609
+ }
610
+ /**
593
611
  * Format OKHSL values as a CSS `rgb(R G B)` string.
594
612
  * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
595
613
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
@@ -623,12 +641,34 @@ function formatHsl(h, s, l, pastel = false) {
623
641
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
624
642
  */
625
643
  function formatOklch(h, s, l, pastel = false) {
626
- const [L, a, b] = okhslToOklab(h, s / 100, l / 100, pastel);
627
- const C = Math.sqrt(a * a + b * b);
628
- let hh = Math.atan2(b, a) * (180 / Math.PI);
629
- hh = constrainAngle(hh);
644
+ const [L, C, hh] = okhslToOklch(h, s / 100, l / 100, pastel);
630
645
  return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
631
646
  }
647
+ /**
648
+ * Convert gamma-encoded sRGB channels (0–1) to a 6-digit lowercase hex
649
+ * string (`#rrggbb`). Channels are clamped to [0,1] and rounded to 8-bit.
650
+ * Alpha is not encoded here — DTCG carries it as a separate `alpha` field.
651
+ */
652
+ function srgbToHex(rgb) {
653
+ const toByte = (c) => Math.max(0, Math.min(255, Math.round(c * 255)));
654
+ const r = toByte(rgb[0]);
655
+ const g = toByte(rgb[1]);
656
+ const b = toByte(rgb[2]);
657
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
658
+ }
659
+ /**
660
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLCH components `[L, C, H]`.
661
+ * L: 0–1, C: 0–~0.4 (chroma), H: 0–360 (hue). Shared by `formatOklch` and
662
+ * the DTCG `oklch` colorSpace exporter so the two never drift apart.
663
+ */
664
+ function okhslToOklch(h, s, l, pastel = false) {
665
+ const [L, a, b] = okhslToOklab(h, s, l, pastel);
666
+ return [
667
+ L,
668
+ Math.sqrt(a * a + b * b),
669
+ constrainAngle(Math.atan2(b, a) * (180 / Math.PI))
670
+ ];
671
+ }
632
672
 
633
673
  //#endregion
634
674
  //#region src/config.ts
@@ -658,7 +698,8 @@ function defaultConfig() {
658
698
  highContrast: false
659
699
  },
660
700
  autoFlip: true,
661
- pastel: false
701
+ pastel: false,
702
+ inferRole: true
662
703
  };
663
704
  }
664
705
  let globalConfig = defaultConfig();
@@ -698,7 +739,8 @@ function configure(config) {
698
739
  },
699
740
  shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
700
741
  autoFlip: config.autoFlip ?? globalConfig.autoFlip,
701
- pastel: config.pastel ?? globalConfig.pastel
742
+ pastel: config.pastel ?? globalConfig.pastel,
743
+ inferRole: config.inferRole ?? globalConfig.inferRole
702
744
  };
703
745
  }
704
746
  function resetConfig() {
@@ -721,10 +763,57 @@ function mergeConfig(base, override) {
721
763
  modes: base.modes,
722
764
  shadowTuning: override.shadowTuning ?? base.shadowTuning,
723
765
  autoFlip: override.autoFlip ?? base.autoFlip,
724
- pastel: override.pastel ?? base.pastel
766
+ pastel: override.pastel ?? base.pastel,
767
+ inferRole: override.inferRole ?? base.inferRole
725
768
  };
726
769
  }
727
770
 
771
+ //#endregion
772
+ //#region src/format-guard.ts
773
+ const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
774
+ /**
775
+ * Throw when a non-native Glaze color space is requested for an export that
776
+ * emits raw CSS or non-Tasty token maps.
777
+ */
778
+ function assertNativeFormat(format, method) {
779
+ if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
780
+ }
781
+ const SCHEME_FIELDS = [
782
+ {
783
+ field: "light",
784
+ modes: () => true
785
+ },
786
+ {
787
+ field: "dark",
788
+ modes: (m) => m.dark
789
+ },
790
+ {
791
+ field: "lightContrast",
792
+ modes: (m) => m.highContrast
793
+ },
794
+ {
795
+ field: "darkContrast",
796
+ modes: (m) => m.dark && m.highContrast
797
+ }
798
+ ];
799
+ /**
800
+ * Throw when `splitHue` is enabled but any exported color is not pastel.
801
+ * Hue rotation is only clip-free when chroma is bounded by the hue-independent
802
+ * safe chroma (`computeSafeChromaOKLCH`).
803
+ */
804
+ function assertAllPastel(resolved, modes) {
805
+ const nonPastel = [];
806
+ for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
807
+ if (!active(modes)) continue;
808
+ if (color[field].pastel !== true) {
809
+ if (!nonPastel.includes(name)) nonPastel.push(name);
810
+ break;
811
+ }
812
+ }
813
+ if (nonPastel.length === 0) return;
814
+ throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (global or per-color) or drop splitHue.`);
815
+ }
816
+
728
817
  //#endregion
729
818
  //#region src/hc-pair.ts
730
819
  function pairNormal(p) {
@@ -989,6 +1078,22 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
989
1078
  function metricLuminance(metric, linearRgb) {
990
1079
  return metric === "apca" ? apcaLuminanceFromLinearRgb(linearRgb) : gamutClampedLuminance(linearRgb);
991
1080
  }
1081
+ const APCA_PRESETS = {
1082
+ preferred: 90,
1083
+ body: 75,
1084
+ content: 60,
1085
+ large: 45,
1086
+ "non-text": 30,
1087
+ min: 15
1088
+ };
1089
+ /**
1090
+ * Resolve an APCA target — a raw Lc number (kept as-is) or an `ApcaPreset`
1091
+ * keyword mapped to its Lc value. The magnitude is forced non-negative.
1092
+ */
1093
+ function resolveApcaTarget(value) {
1094
+ if (typeof value === "number") return Math.abs(value);
1095
+ return APCA_PRESETS[value];
1096
+ }
992
1097
  const CONTRAST_PRESETS = {
993
1098
  AA: 4.5,
994
1099
  AAA: 7,
@@ -1005,16 +1110,18 @@ function pickPair(p, isHighContrast) {
1005
1110
  /**
1006
1111
  * Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
1007
1112
  * given mode into `{ metric, target }`. Handles the inner metric HC pair and
1008
- * preset resolution.
1113
+ * preset resolution. `polarity` is passed through to the result for the APCA
1114
+ * branch (it controls argument order in the solver); WCAG ignores it.
1009
1115
  */
1010
- function resolveContrastForMode(spec, isHighContrast) {
1116
+ function resolveContrastForMode(spec, isHighContrast, polarity) {
1011
1117
  if (typeof spec === "number" || typeof spec === "string") return {
1012
1118
  metric: "wcag",
1013
1119
  target: resolveMinContrast(spec)
1014
1120
  };
1015
1121
  if ("apca" in spec) return {
1016
1122
  metric: "apca",
1017
- target: Math.abs(pickPair(spec.apca, isHighContrast))
1123
+ target: resolveApcaTarget(pickPair(spec.apca, isHighContrast)),
1124
+ polarity: polarity ?? "fg"
1018
1125
  };
1019
1126
  return {
1020
1127
  metric: "wcag",
@@ -1079,20 +1186,25 @@ function cachedLuminance(metric, h, s, t, pastel) {
1079
1186
  /**
1080
1187
  * Score a candidate luminance against the base for a metric. Returns a value
1081
1188
  * that is `>= target` exactly when the floor is met (WCAG ratio, or APCA Lc
1082
- * magnitude).
1189
+ * magnitude). For APCA, `polarity` selects the argument order: `'fg'` (the
1190
+ * default) treats the candidate as the text against a background base
1191
+ * (`apcaContrast(yCandidate, yBase)`); `'bg'` treats the candidate as the
1192
+ * background (`apcaContrast(yBase, yCandidate)`). The magnitude is taken
1193
+ * either way. WCAG is symmetric, so polarity is ignored there.
1083
1194
  */
1084
- function metricScore(metric, yCandidate, yBase) {
1195
+ function metricScore(metric, yCandidate, yBase, polarity) {
1085
1196
  if (metric === "wcag") return contrastRatioFromLuminance(yCandidate, yBase);
1086
- return Math.abs(apcaContrast(yCandidate, yBase));
1197
+ const lc = polarity === "bg" ? apcaContrast(yBase, yCandidate) : apcaContrast(yCandidate, yBase);
1198
+ return Math.abs(lc);
1087
1199
  }
1088
1200
  /**
1089
1201
  * Binary search one branch `[lo, hi]` for the position nearest to `anchor`
1090
1202
  * that meets `target`. The domain is whatever `lum` interprets (tone 0–1 or
1091
1203
  * mix parameter 0–1); the search is identical in both cases.
1092
1204
  */
1093
- function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anchor) {
1094
- const scoreLo = metricScore(metric, lum(lo), yBase);
1095
- const scoreHi = metricScore(metric, lum(hi), yBase);
1205
+ function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anchor, polarity) {
1206
+ const scoreLo = metricScore(metric, lum(lo), yBase, polarity);
1207
+ const scoreHi = metricScore(metric, lum(hi), yBase, polarity);
1096
1208
  if (scoreLo < target && scoreHi < target) return scoreLo >= scoreHi ? {
1097
1209
  pos: lo,
1098
1210
  contrast: scoreLo,
@@ -1107,13 +1219,13 @@ function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anch
1107
1219
  for (let i = 0; i < maxIter; i++) {
1108
1220
  if (high - low < epsilon) break;
1109
1221
  const mid = (low + high) / 2;
1110
- if (metricScore(metric, lum(mid), yBase) >= target) if (mid < anchor) low = mid;
1222
+ if (metricScore(metric, lum(mid), yBase, polarity) >= target) if (mid < anchor) low = mid;
1111
1223
  else high = mid;
1112
1224
  else if (mid < anchor) high = mid;
1113
1225
  else low = mid;
1114
1226
  }
1115
- const scoreLow = metricScore(metric, lum(low), yBase);
1116
- const scoreHigh = metricScore(metric, lum(high), yBase);
1227
+ const scoreLow = metricScore(metric, lum(low), yBase, polarity);
1228
+ const scoreHigh = metricScore(metric, lum(high), yBase, polarity);
1117
1229
  const lowPasses = scoreLow >= target;
1118
1230
  const highPasses = scoreHigh >= target;
1119
1231
  if (lowPasses && highPasses) return Math.abs(low - anchor) <= Math.abs(high - anchor) ? {
@@ -1156,8 +1268,8 @@ function wcagToneSeed(yBase, target, darker) {
1156
1268
  return Math.max(0, Math.min(1, toneFromY(yClamped, REF_EPS) / 100));
1157
1269
  }
1158
1270
  function solveNearestContrast(opts) {
1159
- const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower } = opts;
1160
- const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor);
1271
+ const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower, polarity } = opts;
1272
+ const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity);
1161
1273
  const initialResult = runBranch(initialIsLower);
1162
1274
  initialResult.met = initialResult.contrast >= target;
1163
1275
  if (initialResult.met && !flip) return {
@@ -1188,7 +1300,7 @@ function solveNearestContrast(opts) {
1188
1300
  const extreme = initialIsLower ? lo : hi;
1189
1301
  return {
1190
1302
  pos: extreme,
1191
- contrast: metricScore(metric, lum(extreme), yBase),
1303
+ contrast: metricScore(metric, lum(extreme), yBase, polarity),
1192
1304
  met: false,
1193
1305
  lower: initialIsLower
1194
1306
  };
@@ -1199,11 +1311,11 @@ function solveNearestContrast(opts) {
1199
1311
  */
1200
1312
  function findToneForContrast(options) {
1201
1313
  const { hue, saturation, preferredTone, baseLinearRgb, contrast, toneRange = [0, 1], epsilon = 1e-4, maxIterations = 18, pastel = false } = options;
1202
- const { metric, target } = contrast;
1314
+ const { metric, target, polarity } = contrast;
1203
1315
  const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
1204
1316
  const yBase = metricLuminance(metric, baseLinearRgb);
1205
1317
  const lum = (t) => cachedLuminance(metric, hue, saturation, t, pastel);
1206
- const scorePref = metricScore(metric, lum(preferredTone), yBase);
1318
+ const scorePref = metricScore(metric, lum(preferredTone), yBase, polarity);
1207
1319
  if (scorePref >= searchTarget) return {
1208
1320
  tone: preferredTone,
1209
1321
  contrast: scorePref,
@@ -1223,7 +1335,7 @@ function findToneForContrast(options) {
1223
1335
  met: false,
1224
1336
  branch: "preferred"
1225
1337
  };
1226
- else initialIsDarker = metricScore(metric, lum(minT), yBase) >= metricScore(metric, lum(maxT), yBase);
1338
+ else initialIsDarker = metricScore(metric, lum(minT), yBase, polarity) >= metricScore(metric, lum(maxT), yBase, polarity);
1227
1339
  const solved = solveNearestContrast({
1228
1340
  lum,
1229
1341
  yBase,
@@ -1237,7 +1349,8 @@ function findToneForContrast(options) {
1237
1349
  epsilon,
1238
1350
  maxIterations,
1239
1351
  flip: options.flip ?? false,
1240
- initialIsLower: initialIsDarker
1352
+ initialIsLower: initialIsDarker,
1353
+ polarity
1241
1354
  });
1242
1355
  return {
1243
1356
  tone: solved.pos,
@@ -1253,10 +1366,10 @@ function findToneForContrast(options) {
1253
1366
  */
1254
1367
  function findValueForMixContrast(options) {
1255
1368
  const { preferredValue, baseLinearRgb, contrast, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
1256
- const { metric, target } = contrast;
1369
+ const { metric, target, polarity } = contrast;
1257
1370
  const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
1258
1371
  const yBase = metricLuminance(metric, baseLinearRgb);
1259
- const scorePref = metricScore(metric, luminanceAtValue(preferredValue), yBase);
1372
+ const scorePref = metricScore(metric, luminanceAtValue(preferredValue), yBase, polarity);
1260
1373
  if (scorePref >= searchTarget) return {
1261
1374
  value: preferredValue,
1262
1375
  contrast: scorePref,
@@ -1272,7 +1385,7 @@ function findValueForMixContrast(options) {
1272
1385
  contrast: scorePref,
1273
1386
  met: false
1274
1387
  };
1275
- else initialIsLower = metricScore(metric, luminanceAtValue(0), yBase) >= metricScore(metric, luminanceAtValue(1), yBase);
1388
+ else initialIsLower = metricScore(metric, luminanceAtValue(0), yBase, polarity) >= metricScore(metric, luminanceAtValue(1), yBase, polarity);
1276
1389
  const solved = solveNearestContrast({
1277
1390
  lum: luminanceAtValue,
1278
1391
  yBase,
@@ -1286,7 +1399,8 @@ function findValueForMixContrast(options) {
1286
1399
  epsilon,
1287
1400
  maxIterations,
1288
1401
  flip: options.flip ?? false,
1289
- initialIsLower
1402
+ initialIsLower,
1403
+ polarity
1290
1404
  });
1291
1405
  return {
1292
1406
  value: solved.pos,
@@ -1296,6 +1410,114 @@ function findValueForMixContrast(options) {
1296
1410
  };
1297
1411
  }
1298
1412
 
1413
+ //#endregion
1414
+ //#region src/roles.ts
1415
+ const SURFACE_KEYWORDS = new Set([
1416
+ "surface",
1417
+ "bg",
1418
+ "background",
1419
+ "fill",
1420
+ "canvas",
1421
+ "paper",
1422
+ "layer"
1423
+ ]);
1424
+ const TEXT_KEYWORDS = new Set([
1425
+ "text",
1426
+ "fg",
1427
+ "foreground",
1428
+ "content",
1429
+ "ink",
1430
+ "label",
1431
+ "stroke"
1432
+ ]);
1433
+ const BORDER_KEYWORDS = new Set([
1434
+ "border",
1435
+ "divider",
1436
+ "outline",
1437
+ "separator",
1438
+ "hairline",
1439
+ "rule"
1440
+ ]);
1441
+ const ALIAS_TO_ROLE = {
1442
+ surface: "surface",
1443
+ bg: "surface",
1444
+ background: "surface",
1445
+ fill: "surface",
1446
+ canvas: "surface",
1447
+ paper: "surface",
1448
+ layer: "surface",
1449
+ text: "text",
1450
+ fg: "text",
1451
+ foreground: "text",
1452
+ content: "text",
1453
+ ink: "text",
1454
+ label: "text",
1455
+ stroke: "text",
1456
+ border: "border",
1457
+ divider: "border",
1458
+ outline: "border",
1459
+ separator: "border",
1460
+ hairline: "border",
1461
+ rule: "border"
1462
+ };
1463
+ /**
1464
+ * Normalize a `RoleInput` (canonical value or alias) into a canonical `Role`.
1465
+ * Returns `undefined` for unrecognized strings so callers can fall through to
1466
+ * the next step of the resolution chain.
1467
+ */
1468
+ function normalizeRole(input) {
1469
+ if (input === void 0) return void 0;
1470
+ return ALIAS_TO_ROLE[input];
1471
+ }
1472
+ /**
1473
+ * Tokenize a color name into lowercase keyword tokens, splitting on
1474
+ * non-alphanumeric boundaries and at camelCase boundaries. Examples:
1475
+ * - `'button-text'` → `['button', 'text']`
1476
+ * - `'inputBg'` → `['input', 'bg']`
1477
+ * - `'card_border-outline'` → `['card', 'border', 'outline']`
1478
+ */
1479
+ function tokenizeName(name) {
1480
+ const pieces = name.split(/[^0-9a-zA-Z]+/).filter(Boolean);
1481
+ const tokens = [];
1482
+ for (const piece of pieces) {
1483
+ const sub = piece.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean);
1484
+ for (const s of sub) tokens.push(s.toLowerCase());
1485
+ }
1486
+ return tokens;
1487
+ }
1488
+ /**
1489
+ * Infer a `Role` from a color name by matching its tokens against the role
1490
+ * keyword sets. When multiple tokens match, the **last** recognized token
1491
+ * wins (so `button-text` → `text`, `input-bg` → `surface`, `card-border` →
1492
+ * `border`). Returns `undefined` when no token matches.
1493
+ */
1494
+ function inferRoleFromName(name) {
1495
+ const tokens = tokenizeName(name);
1496
+ let inferred;
1497
+ for (const token of tokens) if (SURFACE_KEYWORDS.has(token)) inferred = "surface";
1498
+ else if (TEXT_KEYWORDS.has(token)) inferred = "text";
1499
+ else if (BORDER_KEYWORDS.has(token)) inferred = "border";
1500
+ return inferred;
1501
+ }
1502
+ /**
1503
+ * Map a role to its APCA polarity. `text` and `border` are foreground spots
1504
+ * against their base (the candidate is the text argument); `surface` is the
1505
+ * background (the base is the text argument).
1506
+ */
1507
+ function roleToPolarity(role) {
1508
+ return role === "surface" ? "bg" : "fg";
1509
+ }
1510
+ /**
1511
+ * The opposite role of `role`, used when a color with no explicit role and no
1512
+ * inferable name depends on a base: the dependent color plays the opposite
1513
+ * role of its base. `surface` ↔ `text`; `border` is treated as a foreground
1514
+ * spot, so its opposite is `surface`.
1515
+ */
1516
+ function oppositeRole(role) {
1517
+ if (role === "surface") return "text";
1518
+ return "surface";
1519
+ }
1520
+
1299
1521
  //#endregion
1300
1522
  //#region src/shadow.ts
1301
1523
  /**
@@ -1502,7 +1724,7 @@ function warnContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
1502
1724
  * warning when the actual measured contrast drifts below the target.
1503
1725
  */
1504
1726
  function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase) {
1505
- const actual = contrast.metric === "apca" ? Math.abs(apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
1727
+ const actual = contrast.metric === "apca" ? Math.abs(contrast.polarity === "bg" ? apcaContrast(yBase, yColor) : apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
1506
1728
  if (actual >= (contrast.metric === "apca" ? contrast.target - CONTRAST_WARN_SLACK_APCA : contrast.target * CONTRAST_WARN_SLACK_WCAG)) return;
1507
1729
  const scheme = schemeLabel(isDark, isHighContrast);
1508
1730
  if (dedupe(`drift|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`)) return;
@@ -1559,8 +1781,53 @@ function toToneVariant(v) {
1559
1781
  alpha: v.alpha
1560
1782
  };
1561
1783
  }
1562
- function resolveContrastSpec(spec, isHighContrast) {
1563
- return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast);
1784
+ /**
1785
+ * Resolve the role of a base color referenced by `baseName`, returning the
1786
+ * role the *dependent* color should take (the opposite of the base's role).
1787
+ * A base that lives in `defs` recursively resolves and is inverted via
1788
+ * `oppositeRole`; an external base (no local def, e.g. an injected standalone
1789
+ * token) is treated as a background, so the dependent defaults to foreground
1790
+ * (`'text'`).
1791
+ */
1792
+ function resolveBaseRoleInMap(baseName, defs, inferRole, roles) {
1793
+ if (!baseName) return void 0;
1794
+ const baseDef = defs[baseName];
1795
+ if (!baseDef) return "text";
1796
+ return oppositeRole(resolveRoleInMap(baseName, baseDef, defs, inferRole, roles));
1797
+ }
1798
+ /**
1799
+ * Role-resolution core that does not need a full `ResolveContext`. Shared by
1800
+ * the resolver (via `resolveRole`) and `verifyContrastDrift`.
1801
+ */
1802
+ function resolveRoleInMap(name, def, defs, inferRole, roles) {
1803
+ const cached = roles.get(name);
1804
+ if (cached) return cached;
1805
+ let role;
1806
+ if (isShadowDef(def)) role = "surface";
1807
+ else if (isMixDef(def)) role = normalizeRole(def.role) ?? (inferRole ? inferRoleFromName(name) : void 0) ?? resolveBaseRoleInMap(def.base, defs, inferRole, roles) ?? "text";
1808
+ else {
1809
+ const regDef = def;
1810
+ role = normalizeRole(regDef.role) ?? (inferRole ? inferRoleFromName(name) : void 0) ?? resolveBaseRoleInMap(regDef.base, defs, inferRole, roles) ?? "text";
1811
+ }
1812
+ const finalRole = role ?? "text";
1813
+ roles.set(name, finalRole);
1814
+ return finalRole;
1815
+ }
1816
+ /**
1817
+ * Resolve a color's semantic `role` (text / surface / border) per the chain:
1818
+ * 1. explicit `def.role` (normalized)
1819
+ * 2. inferred from the color name when `config.inferRole` is on
1820
+ * 3. opposite of the base's role
1821
+ * 4. `'text'` (foreground) default
1822
+ *
1823
+ * Memoized on `ctx.roles` so the four scheme passes share one resolution.
1824
+ * Shadows have no contrast participation and default to `'surface'`.
1825
+ */
1826
+ function resolveRole(name, def, ctx) {
1827
+ return resolveRoleInMap(name, def, ctx.defs, ctx.config.inferRole, ctx.roles);
1828
+ }
1829
+ function resolveContrastSpec(spec, isHighContrast, polarity) {
1830
+ return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast, polarity);
1564
1831
  }
1565
1832
  /**
1566
1833
  * Apply the relative-tone delta against a base, honoring `flip`.
@@ -1582,14 +1849,14 @@ function resolveRootColor(def, isHighContrast) {
1582
1849
  satFactor: clamp(def.saturation ?? 1, 0, 1)
1583
1850
  };
1584
1851
  }
1585
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue) {
1852
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
1586
1853
  const baseName = def.base;
1587
1854
  const baseResolved = ctx.resolved.get(baseName);
1588
1855
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
1589
1856
  const mode = def.mode ?? "auto";
1590
1857
  const satFactor = clamp(def.saturation ?? 1, 0, 1);
1591
1858
  const flip = def.flip ?? ctx.config.autoFlip;
1592
- const pastel = def.pastel ?? ctx.config.pastel;
1859
+ const pastel = effectivePastel;
1593
1860
  const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1594
1861
  const baseTone = baseVariant.t * 100;
1595
1862
  let preferredTone;
@@ -1605,7 +1872,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1605
1872
  }
1606
1873
  const rawContrast = def.contrast;
1607
1874
  if (rawContrast !== void 0) {
1608
- const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast);
1875
+ const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
1609
1876
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
1610
1877
  const baseOkhsl = toOkhslVariant(baseVariant);
1611
1878
  const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
@@ -1637,11 +1904,12 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
1637
1904
  }
1638
1905
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1639
1906
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1640
- if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
1907
+ if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
1641
1908
  const regDef = def;
1642
1909
  const mode = regDef.mode ?? "auto";
1643
1910
  const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
1644
1911
  const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
1912
+ const polarity = roleToPolarity(resolveRole(name, def, ctx));
1645
1913
  const pastel = regDef.pastel ?? ctx.config.pastel;
1646
1914
  let finalTone;
1647
1915
  let satFactor;
@@ -1650,7 +1918,7 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
1650
1918
  finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
1651
1919
  satFactor = root.satFactor;
1652
1920
  } else {
1653
- const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
1921
+ const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
1654
1922
  finalTone = dep.tone;
1655
1923
  satFactor = dep.satFactor;
1656
1924
  }
@@ -1713,7 +1981,7 @@ function linearRgbToToneVariant(rgb, pastel) {
1713
1981
  alpha: 1
1714
1982
  });
1715
1983
  }
1716
- function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1984
+ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
1717
1985
  const baseResolved = ctx.resolved.get(def.base);
1718
1986
  const targetResolved = ctx.resolved.get(def.target);
1719
1987
  const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
@@ -1721,11 +1989,12 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1721
1989
  let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1722
1990
  const blend = def.blend ?? "opaque";
1723
1991
  const space = def.space ?? "okhsl";
1992
+ const polarity = roleToPolarity(resolveRole(name, def, ctx));
1724
1993
  const pastel = def.pastel ?? ctx.config.pastel;
1725
1994
  const baseLinear = okhslVariantToLinearRgb(baseVariant, baseVariant.pastel ?? ctx.config.pastel);
1726
1995
  const targetLinear = okhslVariantToLinearRgb(targetVariant, targetVariant.pastel ?? ctx.config.pastel);
1727
1996
  if (def.contrast !== void 0) {
1728
- const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast);
1997
+ const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, polarity);
1729
1998
  const metric = resolvedContrast.metric;
1730
1999
  let luminanceAt;
1731
2000
  if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
@@ -1814,6 +2083,7 @@ function seedField(order, ctx, field, source) {
1814
2083
  * its tone implies because chromatic luminance drifts from the gray tone.
1815
2084
  */
1816
2085
  function verifyContrastDrift(order, defs, result, config) {
2086
+ const roles = /* @__PURE__ */ new Map();
1817
2087
  for (const name of order) {
1818
2088
  const def = defs[name];
1819
2089
  if (isShadowDef(def) || isMixDef(def)) continue;
@@ -1822,6 +2092,7 @@ function verifyContrastDrift(order, defs, result, config) {
1822
2092
  const color = result.get(name);
1823
2093
  const base = result.get(regDef.base);
1824
2094
  if (!color || !base) continue;
2095
+ const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
1825
2096
  for (const s of [
1826
2097
  {
1827
2098
  isDark: false,
@@ -1844,7 +2115,7 @@ function verifyContrastDrift(order, defs, result, config) {
1844
2115
  field: "darkContrast"
1845
2116
  }
1846
2117
  ]) {
1847
- const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast);
2118
+ const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, polarity);
1848
2119
  const cVariant = color[s.field];
1849
2120
  const bVariant = base[s.field];
1850
2121
  const cOkhsl = toOkhslVariant(cVariant);
@@ -1865,7 +2136,8 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
1865
2136
  saturation,
1866
2137
  defs,
1867
2138
  resolved: /* @__PURE__ */ new Map(),
1868
- config
2139
+ config,
2140
+ roles: /* @__PURE__ */ new Map()
1869
2141
  };
1870
2142
  if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
1871
2143
  const lightMap = runPass(order, defs, ctx, false, false, "light");
@@ -1889,17 +2161,121 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
1889
2161
  return result;
1890
2162
  }
1891
2163
 
2164
+ //#endregion
2165
+ //#region src/channels.ts
2166
+ /**
2167
+ * Hue channel planning for `splitHue` exports.
2168
+ *
2169
+ * Builds per-color hue var references and scheme-independent `--*-hue`
2170
+ * declarations for oklch CSS / Tasty output when every color is pastel.
2171
+ */
2172
+ const ACHROMATIC_EPSILON = 1e-6;
2173
+ function cssProp(prefix, name, suffix) {
2174
+ return `--${prefix}${name}${suffix}`;
2175
+ }
2176
+ function isAchromatic(v) {
2177
+ return v.s <= ACHROMATIC_EPSILON;
2178
+ }
2179
+ function themeHuePlan(name, def, variant, ctx) {
2180
+ if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
2181
+ hueVar: "",
2182
+ inline: true,
2183
+ declarations: []
2184
+ };
2185
+ const regDef = def;
2186
+ const baseHueVar = `var(--${ctx.baseName}-hue)`;
2187
+ if (regDef.hue === void 0) return {
2188
+ hueVar: baseHueVar,
2189
+ inline: false,
2190
+ declarations: []
2191
+ };
2192
+ const parsed = parseRelativeOrAbsolute(regDef.hue);
2193
+ const prop = cssProp(ctx.prefix, name, "-hue");
2194
+ if (parsed.relative) {
2195
+ const sign = parsed.value >= 0 ? "+" : "-";
2196
+ const magnitude = Math.abs(parsed.value);
2197
+ const value = `calc(var(--${ctx.baseName}-hue) ${sign} ${magnitude})`;
2198
+ return {
2199
+ hueVar: `var(${prop})`,
2200
+ inline: false,
2201
+ declarations: [{
2202
+ prop,
2203
+ value
2204
+ }]
2205
+ };
2206
+ }
2207
+ const absHue = (parsed.value % 360 + 360) % 360;
2208
+ return {
2209
+ hueVar: `var(${prop})`,
2210
+ inline: false,
2211
+ declarations: [{
2212
+ prop,
2213
+ value: String(absHue)
2214
+ }]
2215
+ };
2216
+ }
2217
+ function standaloneHuePlan(name, variant, ctx) {
2218
+ if (isAchromatic(variant)) return {
2219
+ hueVar: "",
2220
+ inline: true,
2221
+ declarations: []
2222
+ };
2223
+ const hue = ctx.resolvedHue ?? variant.h;
2224
+ const prop = cssProp(ctx.prefix, name, "-hue");
2225
+ return {
2226
+ hueVar: `var(${prop})`,
2227
+ inline: false,
2228
+ declarations: [{
2229
+ prop,
2230
+ value: String(hue)
2231
+ }]
2232
+ };
2233
+ }
2234
+ function buildHuePlan(name, def, variant, ctx) {
2235
+ if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
2236
+ return themeHuePlan(name, def, variant, ctx);
2237
+ }
2238
+ /** Collect unique hue declarations across all colors (theme + per-color). */
2239
+ function collectHueDeclarations(resolved, ctx) {
2240
+ if (ctx.emitDeclarations === false) return [];
2241
+ const seen = /* @__PURE__ */ new Set();
2242
+ const out = [];
2243
+ const push = (decl) => {
2244
+ if (seen.has(decl.prop)) return;
2245
+ seen.add(decl.prop);
2246
+ out.push(decl);
2247
+ };
2248
+ if (ctx.mode === "theme") push({
2249
+ prop: `--${ctx.baseName}-hue`,
2250
+ value: String(ctx.seedHue)
2251
+ });
2252
+ for (const [name, color] of resolved) {
2253
+ const def = ctx.defs[name];
2254
+ const plan = buildHuePlan(name, def, color.light, ctx);
2255
+ for (const decl of plan.declarations) push(decl);
2256
+ }
2257
+ return out;
2258
+ }
2259
+ function buildHuePlans(resolved, ctx) {
2260
+ const plans = /* @__PURE__ */ new Map();
2261
+ for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
2262
+ return plans;
2263
+ }
2264
+
1892
2265
  //#endregion
1893
2266
  //#region src/formatters.ts
1894
2267
  /**
1895
2268
  * Output formatting for resolved color maps.
1896
2269
  *
1897
2270
  * Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
1898
- * `oklch`) and the four token-map shapes Glaze emits:
2271
+ * `oklch`) and the token-map shapes Glaze emits:
1899
2272
  * - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
1900
2273
  * - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
1901
2274
  * - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
1902
2275
  * - `buildCssMap` — CSS custom property declaration strings per variant.
2276
+ * - `buildDtcgMap` — W3C DTCG (2025.10) token documents, one per scheme.
2277
+ * - `buildDtcgResolver` — W3C DTCG Resolver-Module document (one modifier, a context per scheme).
2278
+ * - `buildTailwindMap` — Tailwind v4 `@theme` block + dark/HC overrides.
1903
2279
  */
1904
2280
  const formatters = {
1905
2281
  okhsl: formatOkhsl,
@@ -1911,13 +2287,37 @@ function fmt(value, decimals) {
1911
2287
  return parseFloat(value.toFixed(decimals)).toString();
1912
2288
  }
1913
2289
  function formatVariant(v, format = "okhsl", pastel = false) {
2290
+ const effectivePastel = v.pastel ?? pastel;
2291
+ let base;
2292
+ if (format === "okhst") base = formatOkhst(v.h, v.s * 100, v.t * 100, effectivePastel);
2293
+ else {
2294
+ const { l } = variantToOkhsl(v);
2295
+ base = formatters[format](v.h, v.s * 100, l * 100, effectivePastel);
2296
+ }
2297
+ if (v.alpha >= 1) return base;
2298
+ const closing = base.lastIndexOf(")");
2299
+ return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
2300
+ }
2301
+ /**
2302
+ * Format a resolved variant as `oklch(L C <hueVar>)`, splicing a CSS hue var
2303
+ * for `splitHue` exports. Falls back to inline when the plan is inline.
2304
+ */
2305
+ function formatVariantHue(v, plan, pastel = false) {
1914
2306
  const effectivePastel = v.pastel ?? pastel;
1915
2307
  const { l } = variantToOkhsl(v);
1916
- const base = formatters[format](v.h, v.s * 100, l * 100, effectivePastel);
2308
+ const [L, C] = okhslToOklch(v.h, v.s, l, effectivePastel);
2309
+ let base;
2310
+ if (plan.inline) if (v.s <= 1e-6) base = `oklch(${fmt(L, 4)} 0 0)`;
2311
+ else base = formatOklch(v.h, v.s * 100, l * 100, effectivePastel);
2312
+ else base = `oklch(${fmt(L, 4)} ${fmt(C, 4)} ${plan.hueVar})`;
1917
2313
  if (v.alpha >= 1) return base;
1918
2314
  const closing = base.lastIndexOf(")");
1919
2315
  return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
1920
2316
  }
2317
+ function formatColorValue(v, format, pastel, huePlan) {
2318
+ if (format === "oklch" && huePlan !== void 0) return formatVariantHue(v, huePlan, pastel);
2319
+ return formatVariant(v, format, pastel);
2320
+ }
1921
2321
  function resolveModes(override) {
1922
2322
  const cfg = getConfig();
1923
2323
  return {
@@ -1925,18 +2325,36 @@ function resolveModes(override) {
1925
2325
  highContrast: override?.highContrast ?? cfg.modes.highContrast
1926
2326
  };
1927
2327
  }
1928
- function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false) {
2328
+ function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false, channelCtx) {
1929
2329
  const tokens = {};
2330
+ const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2331
+ if (huePlans !== void 0 && channelCtx !== void 0) {
2332
+ const emitDecls = channelCtx.emitDeclarations !== false;
2333
+ if (emitDecls && channelCtx.mode === "theme") tokens[`$${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
2334
+ for (const [name, color] of resolved) {
2335
+ const plan = huePlans.get(name);
2336
+ if (emitDecls) for (const decl of plan.declarations) {
2337
+ const key = `$${decl.prop.slice(2)}`;
2338
+ if (!(key in tokens)) tokens[key] = { "": decl.value };
2339
+ }
2340
+ const colorKey = `#${prefix}${name}`;
2341
+ tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
2342
+ }
2343
+ return tokens;
2344
+ }
1930
2345
  for (const [name, color] of resolved) {
1931
2346
  const key = `#${prefix}${name}`;
1932
- const entry = { "": formatVariant(color.light, format, pastel) };
1933
- if (modes.dark) entry[states.dark] = formatVariant(color.dark, format, pastel);
1934
- if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format, pastel);
1935
- if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format, pastel);
1936
- tokens[key] = entry;
2347
+ tokens[key] = buildTokenEntry(color, states, modes, format, pastel);
1937
2348
  }
1938
2349
  return tokens;
1939
2350
  }
2351
+ function buildTokenEntry(color, states, modes, format, pastel, huePlan) {
2352
+ const entry = { "": formatColorValue(color.light, format, pastel, huePlan) };
2353
+ if (modes.dark) entry[states.dark] = formatColorValue(color.dark, format, pastel, huePlan);
2354
+ if (modes.highContrast) entry[states.highContrast] = formatColorValue(color.lightContrast, format, pastel, huePlan);
2355
+ if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatColorValue(color.darkContrast, format, pastel, huePlan);
2356
+ return entry;
2357
+ }
1940
2358
  function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl", pastel = false) {
1941
2359
  const result = { light: {} };
1942
2360
  if (modes.dark) result.dark = {};
@@ -1962,19 +2380,22 @@ function buildJsonMap(resolved, modes, format = "okhsl", pastel = false) {
1962
2380
  }
1963
2381
  return result;
1964
2382
  }
1965
- function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
2383
+ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCtx) {
1966
2384
  const lines = {
1967
2385
  light: [],
1968
2386
  dark: [],
1969
2387
  lightContrast: [],
1970
2388
  darkContrast: []
1971
2389
  };
2390
+ const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
2391
+ if (huePlans !== void 0 && channelCtx !== void 0) for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
1972
2392
  for (const [name, color] of resolved) {
1973
2393
  const prop = `--${prefix}${name}${suffix}`;
1974
- lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
1975
- lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
1976
- lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
1977
- lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format, pastel)};`);
2394
+ const plan = huePlans?.get(name);
2395
+ lines.light.push(`${prop}: ${formatColorValue(color.light, format, pastel, plan)};`);
2396
+ lines.dark.push(`${prop}: ${formatColorValue(color.dark, format, pastel, plan)};`);
2397
+ lines.lightContrast.push(`${prop}: ${formatColorValue(color.lightContrast, format, pastel, plan)};`);
2398
+ lines.darkContrast.push(`${prop}: ${formatColorValue(color.darkContrast, format, pastel, plan)};`);
1978
2399
  }
1979
2400
  return {
1980
2401
  light: lines.light.join("\n"),
@@ -1983,6 +2404,193 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
1983
2404
  darkContrast: lines.darkContrast.join("\n")
1984
2405
  };
1985
2406
  }
2407
+ function roundTo(value, decimals) {
2408
+ return parseFloat(value.toFixed(decimals));
2409
+ }
2410
+ /**
2411
+ * Build a DTCG `$value` color object for a resolved variant.
2412
+ *
2413
+ * `srgb` (default) emits gamma sRGB components in 0–1 plus a 6-digit `hex`
2414
+ * hint — the most universally understood form (Figma, Tokens Studio, Style
2415
+ * Dictionary). `oklch` emits `[L, C, H]` components with no hex — Glaze-native
2416
+ * and wide-gamut. `alpha` is included only when below 1.
2417
+ */
2418
+ function dtcgColorValue(v, colorSpace = "srgb", pastel = false) {
2419
+ const effectivePastel = v.pastel ?? pastel;
2420
+ const { l } = variantToOkhsl(v);
2421
+ const alpha = v.alpha < 1 ? roundTo(v.alpha, 6) : void 0;
2422
+ if (colorSpace === "oklch") {
2423
+ const [L, C, H] = okhslToOklch(v.h, v.s, l, effectivePastel);
2424
+ const value = {
2425
+ colorSpace: "oklch",
2426
+ components: [
2427
+ roundTo(L, 6),
2428
+ roundTo(C, 6),
2429
+ roundTo(H, 4)
2430
+ ]
2431
+ };
2432
+ if (alpha !== void 0) value.alpha = alpha;
2433
+ return value;
2434
+ }
2435
+ const [r, g, b] = okhslToSrgb(v.h, v.s, l, effectivePastel);
2436
+ const value = {
2437
+ colorSpace: "srgb",
2438
+ components: [
2439
+ roundTo(r, 6),
2440
+ roundTo(g, 6),
2441
+ roundTo(b, 6)
2442
+ ],
2443
+ hex: srgbToHex([
2444
+ r,
2445
+ g,
2446
+ b
2447
+ ])
2448
+ };
2449
+ if (alpha !== void 0) value.alpha = alpha;
2450
+ return value;
2451
+ }
2452
+ function dtcgToken(v, colorSpace, pastel) {
2453
+ return {
2454
+ $type: "color",
2455
+ $value: dtcgColorValue(v, colorSpace, pastel)
2456
+ };
2457
+ }
2458
+ /**
2459
+ * Build a `GlazeDtcgResult`: one spec-conformant DTCG token document per
2460
+ * scheme variant, gated by `modes`. Light is always present.
2461
+ */
2462
+ function buildDtcgMap(resolved, prefix, modes, colorSpace = "srgb", pastel = false) {
2463
+ const light = {};
2464
+ const dark = modes.dark ? {} : void 0;
2465
+ const lightContrast = modes.highContrast ? {} : void 0;
2466
+ const darkContrast = modes.dark && modes.highContrast ? {} : void 0;
2467
+ for (const [name, color] of resolved) {
2468
+ const key = `${prefix}${name}`;
2469
+ light[key] = dtcgToken(color.light, colorSpace, pastel);
2470
+ if (dark) dark[key] = dtcgToken(color.dark, colorSpace, pastel);
2471
+ if (lightContrast) lightContrast[key] = dtcgToken(color.lightContrast, colorSpace, pastel);
2472
+ if (darkContrast) darkContrast[key] = dtcgToken(color.darkContrast, colorSpace, pastel);
2473
+ }
2474
+ return {
2475
+ light,
2476
+ dark,
2477
+ lightContrast,
2478
+ darkContrast
2479
+ };
2480
+ }
2481
+ /**
2482
+ * Default context names emitted on the `scheme` modifier — the Glaze variant
2483
+ * keys, so the resolver document mirrors `GlazeDtcgResult` exactly.
2484
+ */
2485
+ const DEFAULT_DTCG_CONTEXT_NAMES = {
2486
+ light: "light",
2487
+ dark: "dark",
2488
+ lightContrast: "lightContrast",
2489
+ darkContrast: "darkContrast"
2490
+ };
2491
+ /**
2492
+ * Wrap a per-scheme `GlazeDtcgResult` into a single W3C DTCG Resolver-Module
2493
+ * document. The light document becomes `sets[setName].sources[0]` (the default
2494
+ * context); each other present variant becomes a `contexts[ctx]` override
2495
+ * array on a single `modifiers[modifierName]`. Absent variants (per the
2496
+ * `modes` already applied to `result`) are omitted — light is always present
2497
+ * and is the modifier `default`. Only the resolver-specific options are read;
2498
+ * `modes` / `colorSpace` were already consumed by the `buildDtcgMap` call that
2499
+ * produced `result`.
2500
+ */
2501
+ function buildDtcgResolver(result, options) {
2502
+ const setName = options?.setName ?? "base";
2503
+ const modifierName = options?.modifierName ?? "scheme";
2504
+ const ctx = {
2505
+ ...DEFAULT_DTCG_CONTEXT_NAMES,
2506
+ ...options?.contextNames
2507
+ };
2508
+ const contexts = { [ctx.light]: [] };
2509
+ if (result.dark) contexts[ctx.dark] = [result.dark];
2510
+ if (result.lightContrast) contexts[ctx.lightContrast] = [result.lightContrast];
2511
+ if (result.darkContrast) contexts[ctx.darkContrast] = [result.darkContrast];
2512
+ return {
2513
+ version: options?.version ?? "2025.10",
2514
+ sets: { [setName]: { sources: [result.light] } },
2515
+ modifiers: { [modifierName]: {
2516
+ default: ctx.light,
2517
+ contexts
2518
+ } },
2519
+ resolutionOrder: [{ $ref: `#/sets/${setName}` }, { $ref: `#/modifiers/${modifierName}` }]
2520
+ };
2521
+ }
2522
+ function tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel) {
2523
+ const lines = {
2524
+ light: [],
2525
+ dark: [],
2526
+ lightContrast: [],
2527
+ darkContrast: []
2528
+ };
2529
+ for (const [name, color] of resolved) {
2530
+ const prop = `--${cssPrefix}${themePrefix}${name}`;
2531
+ lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
2532
+ lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
2533
+ lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
2534
+ lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format, pastel)};`);
2535
+ }
2536
+ return lines;
2537
+ }
2538
+ function indentBlock(text, pad) {
2539
+ return text.split("\n").map((line) => line.length === 0 ? line : pad + line).join("\n");
2540
+ }
2541
+ function emitRule(selector, body) {
2542
+ return `${selector} {\n${indentBlock(body, " ")}\n}`;
2543
+ }
2544
+ /**
2545
+ * Emit a CSS block for a set of declarations scoped by one or more selectors
2546
+ * / at-rules. Class-like selectors concatenate (`.dark.high-contrast`);
2547
+ * at-rules (`@media …`) nest `:root` (or the chained selector) inside.
2548
+ */
2549
+ function emitScoped(scopes, declarations) {
2550
+ if (declarations.length === 0) return void 0;
2551
+ const atRules = [];
2552
+ let selectorChain = "";
2553
+ for (const scope of scopes) if (scope.startsWith("@")) atRules.push(scope);
2554
+ else selectorChain += scope;
2555
+ let css = emitRule(selectorChain || ":root", declarations.join("\n"));
2556
+ for (const rule of atRules) css = emitRule(rule, css);
2557
+ return css;
2558
+ }
2559
+ /**
2560
+ * Render accumulated per-scheme declaration lines as a Tailwind v4 CSS string:
2561
+ * an `@theme` block (light baseline) plus dark / high-contrast overrides under
2562
+ * the configured selectors. Empty blocks are skipped.
2563
+ */
2564
+ function emitTailwindCss(lines, modes, darkSelector, highContrastSelector) {
2565
+ const blocks = [];
2566
+ if (lines.light.length > 0) blocks.push(emitRule("@theme", lines.light.join("\n")));
2567
+ if (modes.dark) {
2568
+ const dark = emitScoped([darkSelector], lines.dark);
2569
+ if (dark) blocks.push(dark);
2570
+ }
2571
+ if (modes.highContrast) {
2572
+ const hc = emitScoped([highContrastSelector], lines.lightContrast);
2573
+ if (hc) blocks.push(hc);
2574
+ }
2575
+ if (modes.dark && modes.highContrast) {
2576
+ const dhc = emitScoped([darkSelector, highContrastSelector], lines.darkContrast);
2577
+ if (dhc) blocks.push(dhc);
2578
+ }
2579
+ return blocks.join("\n\n");
2580
+ }
2581
+ /**
2582
+ * Build per-scheme declaration lines for a single theme (used by
2583
+ * `theme.tailwind()` and as the palette `buildOne` step).
2584
+ */
2585
+ function buildTailwindLines(resolved, themePrefix, cssPrefix, format, pastel) {
2586
+ return tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel);
2587
+ }
2588
+ /**
2589
+ * Build a complete Tailwind v4 CSS string for a single theme.
2590
+ */
2591
+ function buildTailwindMap(resolved, themePrefix, cssPrefix, modes, format, darkSelector, highContrastSelector, pastel = false) {
2592
+ return emitTailwindCss(tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel), modes, darkSelector, highContrastSelector);
2593
+ }
1986
2594
 
1987
2595
  //#endregion
1988
2596
  //#region src/color-token.ts
@@ -2327,6 +2935,7 @@ function buildStandaloneValueDefs(main, options) {
2327
2935
  flip: options?.flip,
2328
2936
  opacity: options?.opacity,
2329
2937
  pastel: options?.pastel,
2938
+ role: options?.role,
2330
2939
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
2331
2940
  };
2332
2941
  const defs = { [primary]: valueDef };
@@ -2367,10 +2976,51 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2367
2976
  token: tokenLike,
2368
2977
  tasty: tokenLike,
2369
2978
  json(options) {
2370
- return buildJsonMap(resolveOnce(), resolveModes(options?.modes), options?.format, effectiveConfig.pastel)[primary];
2979
+ const format = options?.format ?? "oklch";
2980
+ assertNativeFormat(format, "json");
2981
+ return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, effectiveConfig.pastel)[primary];
2371
2982
  },
2372
2983
  css(options) {
2373
- return buildCssMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.suffix ?? "-color", options.format ?? "rgb", effectiveConfig.pastel);
2984
+ const format = options.format ?? "rgb";
2985
+ assertNativeFormat(format, "css");
2986
+ const resolved = resolveOnce().get(primary);
2987
+ const renamed = new Map([[options.name, resolved]]);
2988
+ let channelCtx;
2989
+ if (options.splitHue && format === "oklch") {
2990
+ assertAllPastel(renamed, resolveModes());
2991
+ channelCtx = {
2992
+ seedHue,
2993
+ baseName: options.name,
2994
+ prefix: "",
2995
+ defs: { [options.name]: defs[primary] },
2996
+ mode: "standalone",
2997
+ resolvedHue: resolved.light.h
2998
+ };
2999
+ }
3000
+ return buildCssMap(renamed, "", options.suffix ?? "-color", format, effectiveConfig.pastel, channelCtx);
3001
+ },
3002
+ dtcg(options) {
3003
+ const modes = resolveModes(options?.modes);
3004
+ const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3005
+ const result = { light: doc.light[primary] };
3006
+ if (doc.dark) result.dark = doc.dark[primary];
3007
+ if (doc.lightContrast) result.lightContrast = doc.lightContrast[primary];
3008
+ if (doc.darkContrast) result.darkContrast = doc.darkContrast[primary];
3009
+ return result;
3010
+ },
3011
+ dtcgResolver(options) {
3012
+ const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", effectiveConfig.pastel);
3013
+ const name = options.name;
3014
+ const result = { light: { [name]: doc.light[primary] } };
3015
+ if (doc.dark) result.dark = { [name]: doc.dark[primary] };
3016
+ if (doc.lightContrast) result.lightContrast = { [name]: doc.lightContrast[primary] };
3017
+ if (doc.darkContrast) result.darkContrast = { [name]: doc.darkContrast[primary] };
3018
+ return buildDtcgResolver(result, options);
3019
+ },
3020
+ tailwind(options) {
3021
+ const format = options.format ?? "oklch";
3022
+ assertNativeFormat(format, "tailwind");
3023
+ return buildTailwindMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.namespace ?? "color-", resolveModes(options?.modes), format, options.darkSelector ?? ".dark", options.highContrastSelector ?? ".high-contrast", effectiveConfig.pastel);
2374
3024
  },
2375
3025
  export: exportData
2376
3026
  };
@@ -2428,6 +3078,7 @@ function createColorToken(input, configOverride) {
2428
3078
  contrast: input.contrast,
2429
3079
  opacity: input.opacity,
2430
3080
  pastel: input.pastel,
3081
+ role: input.role,
2431
3082
  base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
2432
3083
  } };
2433
3084
  if (needsSeedAnchor) {
@@ -2478,6 +3129,7 @@ function buildOverridesExport(options) {
2478
3129
  if (options.opacity !== void 0) out.opacity = options.opacity;
2479
3130
  if (options.name !== void 0) out.name = options.name;
2480
3131
  if (options.pastel !== void 0) out.pastel = options.pastel;
3132
+ if (options.role !== void 0) out.role = options.role;
2481
3133
  if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
2482
3134
  return out;
2483
3135
  }
@@ -2494,6 +3146,7 @@ function buildStructuredInputExport(input) {
2494
3146
  if (input.contrast !== void 0) out.contrast = input.contrast;
2495
3147
  if (input.name !== void 0) out.name = input.name;
2496
3148
  if (input.pastel !== void 0) out.pastel = input.pastel;
3149
+ if (input.role !== void 0) out.role = input.role;
2497
3150
  if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
2498
3151
  return out;
2499
3152
  }
@@ -2515,6 +3168,7 @@ function rehydrateOverrides(data) {
2515
3168
  if (data.opacity !== void 0) out.opacity = data.opacity;
2516
3169
  if (data.name !== void 0) out.name = data.name;
2517
3170
  if (data.pastel !== void 0) out.pastel = data.pastel;
3171
+ if (data.role !== void 0) out.role = data.role;
2518
3172
  if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
2519
3173
  return out;
2520
3174
  }
@@ -2531,6 +3185,7 @@ function rehydrateStructuredInput(data) {
2531
3185
  if (data.contrast !== void 0) out.contrast = data.contrast;
2532
3186
  if (data.name !== void 0) out.name = data.name;
2533
3187
  if (data.pastel !== void 0) out.pastel = data.pastel;
3188
+ if (data.role !== void 0) out.role = data.role;
2534
3189
  if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
2535
3190
  return out;
2536
3191
  }
@@ -2555,16 +3210,6 @@ function colorFromExport(data) {
2555
3210
 
2556
3211
  //#endregion
2557
3212
  //#region src/palette.ts
2558
- /**
2559
- * Palette factory.
2560
- *
2561
- * Composes multiple themes into a single token namespace with optional
2562
- * theme-name prefixes and a "primary theme" that also surfaces an
2563
- * unprefixed copy of its tokens. All four export methods (`tokens` /
2564
- * `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
2565
- * handles validation, per-theme iteration, prefix resolution, collision
2566
- * filtering, and primary duplication.
2567
- */
2568
3213
  function resolvePrefix(options, themeName, defaultPrefix = false) {
2569
3214
  const prefix = options?.prefix ?? defaultPrefix;
2570
3215
  if (prefix === true) return `${themeName}-`;
@@ -2604,6 +3249,26 @@ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
2604
3249
  }
2605
3250
  return filtered;
2606
3251
  }
3252
+ function colorMapFromTheme(theme) {
3253
+ const defs = {};
3254
+ for (const name of theme.list()) {
3255
+ const def = theme.color(name);
3256
+ if (def !== void 0) defs[name] = def;
3257
+ }
3258
+ return defs;
3259
+ }
3260
+ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue, format, modes, filtered) {
3261
+ if (!splitHue || format !== "oklch") return void 0;
3262
+ assertAllPastel(filtered, modes);
3263
+ return {
3264
+ seedHue: theme.hue,
3265
+ baseName: themeName,
3266
+ prefix: themedPrefix,
3267
+ defs: colorMapFromTheme(theme),
3268
+ mode: "theme",
3269
+ emitDeclarations: passPrefix === themedPrefix
3270
+ };
3271
+ }
2607
3272
  /**
2608
3273
  * Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
2609
3274
  * this because it doesn't do collision filtering or primary duplication.
@@ -2617,17 +3282,29 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
2617
3282
  const resolved = theme.resolve();
2618
3283
  const pastel = theme.getConfig().pastel;
2619
3284
  const prefix = resolvePrefix(options, themeName, true);
2620
- merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel));
2621
- if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel));
3285
+ merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel, themeName, theme));
3286
+ if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel, themeName, theme));
2622
3287
  }
2623
3288
  return acc;
2624
3289
  }
2625
3290
  function createPalette(themes, paletteOptions) {
2626
3291
  validatePrimaryTheme(paletteOptions?.primary, themes);
3292
+ const buildDtcgResult = (options) => {
3293
+ const modes = resolveModes(options?.modes);
3294
+ const colorSpace = options?.colorSpace ?? "srgb";
3295
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, _themeName, _theme) => buildDtcgMap(filtered, prefix, modes, colorSpace, pastel), (acc, part) => {
3296
+ Object.assign(acc.light, part.light);
3297
+ if (part.dark) acc.dark = Object.assign(acc.dark ?? {}, part.dark);
3298
+ if (part.lightContrast) acc.lightContrast = Object.assign(acc.lightContrast ?? {}, part.lightContrast);
3299
+ if (part.darkContrast) acc.darkContrast = Object.assign(acc.darkContrast ?? {}, part.darkContrast);
3300
+ }, () => ({ light: {} }));
3301
+ };
2627
3302
  return {
2628
3303
  tokens(options) {
3304
+ const format = options?.format ?? "oklch";
3305
+ assertNativeFormat(format, "tokens");
2629
3306
  const modes = resolveModes(options?.modes);
2630
- return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes, options?.format, pastel), (acc, part) => {
3307
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes, format, pastel), (acc, part) => {
2631
3308
  for (const variant of Object.keys(part)) {
2632
3309
  if (!acc[variant]) acc[variant] = {};
2633
3310
  Object.assign(acc[variant], part[variant]);
@@ -2641,18 +3318,27 @@ function createPalette(themes, paletteOptions) {
2641
3318
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2642
3319
  };
2643
3320
  const modes = resolveModes(options?.modes);
2644
- return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildTokenMap(filtered, prefix, states, modes, options?.format, pastel), (acc, part) => Object.assign(acc, part), () => ({}));
3321
+ const format = options?.format ?? "okhsl";
3322
+ return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, themeName, theme) => {
3323
+ return buildTokenMap(filtered, prefix, states, modes, format, pastel, channelCtxForTheme(theme, themeName, prefix, resolvePrefix(options, themeName, true), options?.splitHue, format, modes, filtered));
3324
+ }, (acc, part) => Object.assign(acc, part), () => ({}));
2645
3325
  },
2646
3326
  json(options) {
3327
+ const format = options?.format ?? "oklch";
3328
+ assertNativeFormat(format, "json");
2647
3329
  const modes = resolveModes(options?.modes);
2648
3330
  const result = {};
2649
- for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format, theme.getConfig().pastel);
3331
+ for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, format, theme.getConfig().pastel);
2650
3332
  return result;
2651
3333
  },
2652
3334
  css(options) {
2653
3335
  const suffix = options?.suffix ?? "-color";
2654
3336
  const format = options?.format ?? "rgb";
2655
- const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildCssMap(filtered, prefix, suffix, format, pastel), (acc, part) => {
3337
+ assertNativeFormat(format, "css");
3338
+ const modes = resolveModes();
3339
+ const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, themeName, theme) => {
3340
+ return buildCssMap(filtered, prefix, suffix, format, pastel, channelCtxForTheme(theme, themeName, prefix, resolvePrefix(options, themeName, true), options?.splitHue, format, modes, filtered));
3341
+ }, (acc, part) => {
2656
3342
  for (const key of [
2657
3343
  "light",
2658
3344
  "dark",
@@ -2671,24 +3357,39 @@ function createPalette(themes, paletteOptions) {
2671
3357
  lightContrast: lines.lightContrast.join("\n"),
2672
3358
  darkContrast: lines.darkContrast.join("\n")
2673
3359
  };
3360
+ },
3361
+ dtcg(options) {
3362
+ return buildDtcgResult(options);
3363
+ },
3364
+ dtcgResolver(options) {
3365
+ return buildDtcgResolver(buildDtcgResult(options), options);
3366
+ },
3367
+ tailwind(options) {
3368
+ const modes = resolveModes(options?.modes);
3369
+ const cssPrefix = options?.namespace ?? "color-";
3370
+ const format = options?.format ?? "oklch";
3371
+ assertNativeFormat(format, "tailwind");
3372
+ const darkSelector = options?.darkSelector ?? ".dark";
3373
+ const highContrastSelector = options?.highContrastSelector ?? ".high-contrast";
3374
+ return emitTailwindCss(buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, _themeName, _theme) => buildTailwindLines(filtered, prefix, cssPrefix, format, pastel), (acc, part) => {
3375
+ for (const variant of [
3376
+ "light",
3377
+ "dark",
3378
+ "lightContrast",
3379
+ "darkContrast"
3380
+ ]) acc[variant].push(...part[variant]);
3381
+ }, () => ({
3382
+ light: [],
3383
+ dark: [],
3384
+ lightContrast: [],
3385
+ darkContrast: []
3386
+ })), modes, darkSelector, highContrastSelector);
2674
3387
  }
2675
3388
  };
2676
3389
  }
2677
3390
 
2678
3391
  //#endregion
2679
3392
  //#region src/theme.ts
2680
- /**
2681
- * Theme factory.
2682
- *
2683
- * Wraps a hue/saturation seed, a mutable `ColorMap`, and an optional
2684
- * per-theme `GlazeConfigOverride`. Exposes `tokens()` / `tasty()` /
2685
- * `json()` / `css()` / `resolve()` / `export()` / `extend()`.
2686
- *
2687
- * The per-theme config override is **merged over the live global config at
2688
- * resolve time** so the theme still reacts to later `configure()` calls
2689
- * for fields it didn't override. The merged config is memoized by
2690
- * `configVersion` to avoid rebuilding it on every export call.
2691
- */
2692
3393
  function createTheme(hue, saturation, initialColors, configOverride) {
2693
3394
  let colorDefs = initialColors ? { ...initialColors } : {};
2694
3395
  let cache = null;
@@ -2712,6 +3413,18 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2712
3413
  function invalidate() {
2713
3414
  cache = null;
2714
3415
  }
3416
+ function channelCtxFor(options, formatDefault, prefix) {
3417
+ const format = options?.format ?? formatDefault;
3418
+ if (!options?.splitHue || format !== "oklch") return void 0;
3419
+ assertAllPastel(resolveCached(), resolveModes(options?.modes));
3420
+ return {
3421
+ seedHue: hue,
3422
+ baseName: options.name ?? "theme",
3423
+ prefix,
3424
+ defs: colorDefs,
3425
+ mode: "theme"
3426
+ };
3427
+ }
2715
3428
  return {
2716
3429
  get hue() {
2717
3430
  return hue;
@@ -2775,8 +3488,10 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2775
3488
  return new Map(resolveCached());
2776
3489
  },
2777
3490
  tokens(options) {
3491
+ const format = options?.format ?? "oklch";
3492
+ assertNativeFormat(format, "tokens");
2778
3493
  const modes = resolveModes(options?.modes);
2779
- return buildFlatTokenMap(resolveCached(), "", modes, options?.format, getEffectiveConfig().pastel);
3494
+ return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
2780
3495
  },
2781
3496
  tasty(options) {
2782
3497
  const cfg = getEffectiveConfig();
@@ -2785,14 +3500,34 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2785
3500
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2786
3501
  };
2787
3502
  const modes = resolveModes(options?.modes);
2788
- return buildTokenMap(resolveCached(), "", states, modes, options?.format, cfg.pastel);
3503
+ const format = options?.format ?? "okhsl";
3504
+ const channelCtx = channelCtxFor(options, "okhsl", "");
3505
+ return buildTokenMap(resolveCached(), "", states, modes, format, cfg.pastel, channelCtx);
2789
3506
  },
2790
3507
  json(options) {
3508
+ const format = options?.format ?? "oklch";
3509
+ assertNativeFormat(format, "json");
2791
3510
  const modes = resolveModes(options?.modes);
2792
- return buildJsonMap(resolveCached(), modes, options?.format, getEffectiveConfig().pastel);
3511
+ return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
2793
3512
  },
2794
3513
  css(options) {
2795
- return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb", getEffectiveConfig().pastel);
3514
+ const format = options?.format ?? "rgb";
3515
+ assertNativeFormat(format, "css");
3516
+ const channelCtx = channelCtxFor(options, "rgb", "");
3517
+ return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
3518
+ },
3519
+ dtcg(options) {
3520
+ const modes = resolveModes(options?.modes);
3521
+ return buildDtcgMap(resolveCached(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
3522
+ },
3523
+ dtcgResolver(options) {
3524
+ return buildDtcgResolver(buildDtcgMap(resolveCached(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel), options);
3525
+ },
3526
+ tailwind(options) {
3527
+ const format = options?.format ?? "oklch";
3528
+ assertNativeFormat(format, "tailwind");
3529
+ const modes = resolveModes(options?.modes);
3530
+ return buildTailwindMap(resolveCached(), "", options?.namespace ?? "color-", modes, format, options?.darkSelector ?? ".dark", options?.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
2796
3531
  }
2797
3532
  };
2798
3533
  }
@@ -2986,29 +3721,39 @@ glaze.resetConfig = function resetConfig$1() {
2986
3721
  //#endregion
2987
3722
  exports.REF_EPS = REF_EPS;
2988
3723
  exports.apcaContrast = apcaContrast;
3724
+ exports.assertAllPastel = assertAllPastel;
3725
+ exports.assertNativeFormat = assertNativeFormat;
2989
3726
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
2990
3727
  exports.cuspLightness = cuspLightness;
2991
3728
  exports.findToneForContrast = findToneForContrast;
2992
3729
  exports.findValueForMixContrast = findValueForMixContrast;
2993
3730
  exports.formatHsl = formatHsl;
2994
3731
  exports.formatOkhsl = formatOkhsl;
3732
+ exports.formatOkhst = formatOkhst;
2995
3733
  exports.formatOklch = formatOklch;
2996
3734
  exports.formatRgb = formatRgb;
2997
3735
  exports.fromTone = fromTone;
2998
3736
  exports.gamutClampedLuminance = gamutClampedLuminance;
2999
3737
  exports.glaze = glaze;
3000
3738
  exports.hslToSrgb = hslToSrgb;
3739
+ exports.inferRoleFromName = inferRoleFromName;
3740
+ exports.normalizeRole = normalizeRole;
3001
3741
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
3002
3742
  exports.okhslToOkhst = okhslToOkhst;
3003
3743
  exports.okhslToOklab = okhslToOklab;
3744
+ exports.okhslToOklch = okhslToOklch;
3004
3745
  exports.okhslToSrgb = okhslToSrgb;
3005
3746
  exports.okhstToOkhsl = okhstToOkhsl;
3006
3747
  exports.oklabToOkhsl = oklabToOkhsl;
3748
+ exports.oppositeRole = oppositeRole;
3007
3749
  exports.parseHex = parseHex;
3008
3750
  exports.parseHexAlpha = parseHexAlpha;
3009
3751
  exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
3752
+ exports.resolveApcaTarget = resolveApcaTarget;
3010
3753
  exports.resolveContrastForMode = resolveContrastForMode;
3011
3754
  exports.resolveMinContrast = resolveMinContrast;
3755
+ exports.roleToPolarity = roleToPolarity;
3756
+ exports.srgbToHex = srgbToHex;
3012
3757
  exports.srgbToOkhsl = srgbToOkhsl;
3013
3758
  exports.toTone = toTone;
3014
3759
  exports.toneFromY = toneFromY;