@tenphi/glaze 0.15.1 → 0.16.0

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