@tenphi/glaze 0.0.0-snapshot.d38eee5 → 0.0.0-snapshot.e052c9b

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
@@ -728,6 +768,52 @@ function mergeConfig(base, override) {
728
768
  };
729
769
  }
730
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 `splitChannels` 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: splitChannels 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 splitChannels.`);
815
+ }
816
+
731
817
  //#endregion
732
818
  //#region src/hc-pair.ts
733
819
  function pairNormal(p) {
@@ -2075,17 +2161,121 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
2075
2161
  return result;
2076
2162
  }
2077
2163
 
2164
+ //#endregion
2165
+ //#region src/channels.ts
2166
+ /**
2167
+ * Hue channel planning for `splitChannels` 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
+
2078
2265
  //#endregion
2079
2266
  //#region src/formatters.ts
2080
2267
  /**
2081
2268
  * Output formatting for resolved color maps.
2082
2269
  *
2083
2270
  * Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
2084
- * `oklch`) and the four token-map shapes Glaze emits:
2271
+ * `oklch`) and the token-map shapes Glaze emits:
2085
2272
  * - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
2086
2273
  * - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
2087
2274
  * - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
2088
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.
2089
2279
  */
2090
2280
  const formatters = {
2091
2281
  okhsl: formatOkhsl,
@@ -2097,13 +2287,37 @@ function fmt(value, decimals) {
2097
2287
  return parseFloat(value.toFixed(decimals)).toString();
2098
2288
  }
2099
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 `splitChannels` exports. Falls back to inline when the plan is inline.
2304
+ */
2305
+ function formatVariantHue(v, plan, pastel = false) {
2100
2306
  const effectivePastel = v.pastel ?? pastel;
2101
2307
  const { l } = variantToOkhsl(v);
2102
- 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})`;
2103
2313
  if (v.alpha >= 1) return base;
2104
2314
  const closing = base.lastIndexOf(")");
2105
2315
  return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
2106
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
+ }
2107
2321
  function resolveModes(override) {
2108
2322
  const cfg = getConfig();
2109
2323
  return {
@@ -2111,18 +2325,36 @@ function resolveModes(override) {
2111
2325
  highContrast: override?.highContrast ?? cfg.modes.highContrast
2112
2326
  };
2113
2327
  }
2114
- function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false) {
2328
+ function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false, channelCtx) {
2115
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
+ }
2116
2345
  for (const [name, color] of resolved) {
2117
2346
  const key = `#${prefix}${name}`;
2118
- const entry = { "": formatVariant(color.light, format, pastel) };
2119
- if (modes.dark) entry[states.dark] = formatVariant(color.dark, format, pastel);
2120
- if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format, pastel);
2121
- if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format, pastel);
2122
- tokens[key] = entry;
2347
+ tokens[key] = buildTokenEntry(color, states, modes, format, pastel);
2123
2348
  }
2124
2349
  return tokens;
2125
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
+ }
2126
2358
  function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl", pastel = false) {
2127
2359
  const result = { light: {} };
2128
2360
  if (modes.dark) result.dark = {};
@@ -2148,19 +2380,22 @@ function buildJsonMap(resolved, modes, format = "okhsl", pastel = false) {
2148
2380
  }
2149
2381
  return result;
2150
2382
  }
2151
- function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
2383
+ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCtx) {
2152
2384
  const lines = {
2153
2385
  light: [],
2154
2386
  dark: [],
2155
2387
  lightContrast: [],
2156
2388
  darkContrast: []
2157
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};`);
2158
2392
  for (const [name, color] of resolved) {
2159
2393
  const prop = `--${prefix}${name}${suffix}`;
2160
- lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
2161
- lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
2162
- lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
2163
- 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)};`);
2164
2399
  }
2165
2400
  return {
2166
2401
  light: lines.light.join("\n"),
@@ -2169,6 +2404,193 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
2169
2404
  darkContrast: lines.darkContrast.join("\n")
2170
2405
  };
2171
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
+ }
2172
2594
 
2173
2595
  //#endregion
2174
2596
  //#region src/color-token.ts
@@ -2554,10 +2976,51 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
2554
2976
  token: tokenLike,
2555
2977
  tasty: tokenLike,
2556
2978
  json(options) {
2557
- 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];
2558
2982
  },
2559
2983
  css(options) {
2560
- 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.splitChannels && 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);
2561
3024
  },
2562
3025
  export: exportData
2563
3026
  };
@@ -2747,16 +3210,6 @@ function colorFromExport(data) {
2747
3210
 
2748
3211
  //#endregion
2749
3212
  //#region src/palette.ts
2750
- /**
2751
- * Palette factory.
2752
- *
2753
- * Composes multiple themes into a single token namespace with optional
2754
- * theme-name prefixes and a "primary theme" that also surfaces an
2755
- * unprefixed copy of its tokens. All four export methods (`tokens` /
2756
- * `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
2757
- * handles validation, per-theme iteration, prefix resolution, collision
2758
- * filtering, and primary duplication.
2759
- */
2760
3213
  function resolvePrefix(options, themeName, defaultPrefix = false) {
2761
3214
  const prefix = options?.prefix ?? defaultPrefix;
2762
3215
  if (prefix === true) return `${themeName}-`;
@@ -2796,6 +3249,26 @@ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
2796
3249
  }
2797
3250
  return filtered;
2798
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, splitChannels, format, modes, filtered) {
3261
+ if (!splitChannels || 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
+ }
2799
3272
  /**
2800
3273
  * Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
2801
3274
  * this because it doesn't do collision filtering or primary duplication.
@@ -2809,17 +3282,29 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
2809
3282
  const resolved = theme.resolve();
2810
3283
  const pastel = theme.getConfig().pastel;
2811
3284
  const prefix = resolvePrefix(options, themeName, true);
2812
- merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel));
2813
- 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));
2814
3287
  }
2815
3288
  return acc;
2816
3289
  }
2817
3290
  function createPalette(themes, paletteOptions) {
2818
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
+ };
2819
3302
  return {
2820
3303
  tokens(options) {
3304
+ const format = options?.format ?? "oklch";
3305
+ assertNativeFormat(format, "tokens");
2821
3306
  const modes = resolveModes(options?.modes);
2822
- 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) => {
2823
3308
  for (const variant of Object.keys(part)) {
2824
3309
  if (!acc[variant]) acc[variant] = {};
2825
3310
  Object.assign(acc[variant], part[variant]);
@@ -2833,18 +3318,27 @@ function createPalette(themes, paletteOptions) {
2833
3318
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2834
3319
  };
2835
3320
  const modes = resolveModes(options?.modes);
2836
- 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?.splitChannels, format, modes, filtered));
3324
+ }, (acc, part) => Object.assign(acc, part), () => ({}));
2837
3325
  },
2838
3326
  json(options) {
3327
+ const format = options?.format ?? "oklch";
3328
+ assertNativeFormat(format, "json");
2839
3329
  const modes = resolveModes(options?.modes);
2840
3330
  const result = {};
2841
- 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);
2842
3332
  return result;
2843
3333
  },
2844
3334
  css(options) {
2845
3335
  const suffix = options?.suffix ?? "-color";
2846
3336
  const format = options?.format ?? "rgb";
2847
- 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?.splitChannels, format, modes, filtered));
3341
+ }, (acc, part) => {
2848
3342
  for (const key of [
2849
3343
  "light",
2850
3344
  "dark",
@@ -2863,24 +3357,39 @@ function createPalette(themes, paletteOptions) {
2863
3357
  lightContrast: lines.lightContrast.join("\n"),
2864
3358
  darkContrast: lines.darkContrast.join("\n")
2865
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);
2866
3387
  }
2867
3388
  };
2868
3389
  }
2869
3390
 
2870
3391
  //#endregion
2871
3392
  //#region src/theme.ts
2872
- /**
2873
- * Theme factory.
2874
- *
2875
- * Wraps a hue/saturation seed, a mutable `ColorMap`, and an optional
2876
- * per-theme `GlazeConfigOverride`. Exposes `tokens()` / `tasty()` /
2877
- * `json()` / `css()` / `resolve()` / `export()` / `extend()`.
2878
- *
2879
- * The per-theme config override is **merged over the live global config at
2880
- * resolve time** so the theme still reacts to later `configure()` calls
2881
- * for fields it didn't override. The merged config is memoized by
2882
- * `configVersion` to avoid rebuilding it on every export call.
2883
- */
2884
3393
  function createTheme(hue, saturation, initialColors, configOverride) {
2885
3394
  let colorDefs = initialColors ? { ...initialColors } : {};
2886
3395
  let cache = null;
@@ -2904,6 +3413,18 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2904
3413
  function invalidate() {
2905
3414
  cache = null;
2906
3415
  }
3416
+ function channelCtxFor(options, formatDefault, prefix) {
3417
+ const format = options?.format ?? formatDefault;
3418
+ if (!options?.splitChannels || 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
+ }
2907
3428
  return {
2908
3429
  get hue() {
2909
3430
  return hue;
@@ -2967,8 +3488,10 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2967
3488
  return new Map(resolveCached());
2968
3489
  },
2969
3490
  tokens(options) {
3491
+ const format = options?.format ?? "oklch";
3492
+ assertNativeFormat(format, "tokens");
2970
3493
  const modes = resolveModes(options?.modes);
2971
- return buildFlatTokenMap(resolveCached(), "", modes, options?.format, getEffectiveConfig().pastel);
3494
+ return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
2972
3495
  },
2973
3496
  tasty(options) {
2974
3497
  const cfg = getEffectiveConfig();
@@ -2977,14 +3500,34 @@ function createTheme(hue, saturation, initialColors, configOverride) {
2977
3500
  highContrast: options?.states?.highContrast ?? cfg.states.highContrast
2978
3501
  };
2979
3502
  const modes = resolveModes(options?.modes);
2980
- 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);
2981
3506
  },
2982
3507
  json(options) {
3508
+ const format = options?.format ?? "oklch";
3509
+ assertNativeFormat(format, "json");
2983
3510
  const modes = resolveModes(options?.modes);
2984
- return buildJsonMap(resolveCached(), modes, options?.format, getEffectiveConfig().pastel);
3511
+ return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
2985
3512
  },
2986
3513
  css(options) {
2987
- 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);
2988
3531
  }
2989
3532
  };
2990
3533
  }
@@ -3178,12 +3721,15 @@ glaze.resetConfig = function resetConfig$1() {
3178
3721
  //#endregion
3179
3722
  exports.REF_EPS = REF_EPS;
3180
3723
  exports.apcaContrast = apcaContrast;
3724
+ exports.assertAllPastel = assertAllPastel;
3725
+ exports.assertNativeFormat = assertNativeFormat;
3181
3726
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
3182
3727
  exports.cuspLightness = cuspLightness;
3183
3728
  exports.findToneForContrast = findToneForContrast;
3184
3729
  exports.findValueForMixContrast = findValueForMixContrast;
3185
3730
  exports.formatHsl = formatHsl;
3186
3731
  exports.formatOkhsl = formatOkhsl;
3732
+ exports.formatOkhst = formatOkhst;
3187
3733
  exports.formatOklch = formatOklch;
3188
3734
  exports.formatRgb = formatRgb;
3189
3735
  exports.fromTone = fromTone;
@@ -3195,6 +3741,7 @@ exports.normalizeRole = normalizeRole;
3195
3741
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
3196
3742
  exports.okhslToOkhst = okhslToOkhst;
3197
3743
  exports.okhslToOklab = okhslToOklab;
3744
+ exports.okhslToOklch = okhslToOklch;
3198
3745
  exports.okhslToSrgb = okhslToSrgb;
3199
3746
  exports.okhstToOkhsl = okhstToOkhsl;
3200
3747
  exports.oklabToOkhsl = oklabToOkhsl;
@@ -3206,6 +3753,7 @@ exports.resolveApcaTarget = resolveApcaTarget;
3206
3753
  exports.resolveContrastForMode = resolveContrastForMode;
3207
3754
  exports.resolveMinContrast = resolveMinContrast;
3208
3755
  exports.roleToPolarity = roleToPolarity;
3756
+ exports.srgbToHex = srgbToHex;
3209
3757
  exports.srgbToOkhsl = srgbToOkhsl;
3210
3758
  exports.toTone = toTone;
3211
3759
  exports.toneFromY = toneFromY;