@bamboocss/generator 1.52.0 → 1.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +158 -28
  2. package/dist/index.mjs +158 -28
  3. package/package.json +7 -7
package/dist/index.cjs CHANGED
@@ -2450,41 +2450,162 @@ const OS_DARK_QUERY = "@media (prefers-color-scheme: dark)";
2450
2450
  /** Compare a condition against a known query without tripping over spacing. */
2451
2451
  const normalize = (condition) => typeof condition === "string" ? condition.replace(/\s+/g, " ").trim() : void 0;
2452
2452
  /**
2453
- * Does this value carry a comma at the top level?
2453
+ * Split a value on its top-level `separator`, ignoring one nested inside a function or a
2454
+ * string.
2454
2455
  *
2455
- * `light-dark()` takes exactly two arguments, and CSS offers no way to group a
2456
- * comma-separated value into one of them there is no parenthesized form of a shadow list.
2457
- * So a token whose light or dark arm is itself a list cannot be folded: the arms splat into
2458
- * three or more arguments, the function is invalid, and the browser drops the whole
2459
- * declaration. That failure is silent and total — a `--shadows-sm` carrying two shadows
2460
- * emitted `light-dark(a, b, c)` and every element referencing it rendered with no shadow at
2461
- * all, while the class naming it looked perfectly correct.
2462
- *
2463
- * Multi-part `box-shadow` is the shape that bites, since a realistic elevation token is
2464
- * almost always two shadows; `transition` and `background` lists are the same story.
2465
- *
2466
- * Depth-aware, so `rgb(16, 19, 26)` still folds — its commas are the function's own.
2467
- * Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a separator.
2456
+ * Depth-aware, so `rgb(16, 19, 26)` is one part rather than three its commas are the
2457
+ * function's own. Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a
2458
+ * separator. Splitting on `' '` collapses runs of whitespace, which is what makes
2459
+ * `0 1px 2px red` and `0 1px 2px red` compare component for component.
2468
2460
  */
2469
- const hasTopLevelComma = (value) => {
2461
+ function splitTopLevel(value, separator) {
2462
+ const parts = [];
2463
+ let current = "";
2470
2464
  let depth = 0;
2471
2465
  let quote;
2466
+ const isSeparator = (char) => separator === "," ? char === "," : char === " " || char === " " || char === "\n" || char === "\r";
2472
2467
  for (let index = 0; index < value.length; index++) {
2473
2468
  const char = value[index];
2474
2469
  if (quote) {
2475
- if (char === "\\") index++;
2476
- else if (char === quote) quote = void 0;
2470
+ current += char;
2471
+ if (char === "\\") {
2472
+ current += value[index + 1] ?? "";
2473
+ index++;
2474
+ } else if (char === quote) quote = void 0;
2475
+ continue;
2476
+ }
2477
+ if (char === "\"" || char === "'") {
2478
+ quote = char;
2479
+ current += char;
2477
2480
  continue;
2478
2481
  }
2479
- if (char === "\"" || char === "'") quote = char;
2480
- else if (char === "(") depth++;
2482
+ if (char === "(") depth++;
2481
2483
  else if (char === ")") depth--;
2482
- else if (char === "," && depth === 0) return true;
2484
+ else if (depth === 0 && isSeparator(char)) {
2485
+ if (separator === "," || current.trim()) parts.push(current.trim());
2486
+ current = "";
2487
+ continue;
2488
+ }
2489
+ current += char;
2483
2490
  }
2484
- return false;
2485
- };
2491
+ if (separator === "," || current.trim()) parts.push(current.trim());
2492
+ return parts;
2493
+ }
2494
+ /** Functions whose result is a `<color>`. */
2495
+ const COLOR_FUNCTIONS = new Set([
2496
+ "color",
2497
+ "color-mix",
2498
+ "device-cmyk",
2499
+ "hsl",
2500
+ "hsla",
2501
+ "hwb",
2502
+ "lab",
2503
+ "lch",
2504
+ "light-dark",
2505
+ "oklab",
2506
+ "oklch",
2507
+ "rgb",
2508
+ "rgba"
2509
+ ]);
2510
+ /** Keywords that are a `<color>`: the named set, the system set, and the two specials. */
2511
+ const COLOR_KEYWORDS = new Set(`transparent currentcolor
2512
+ aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue
2513
+ blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk
2514
+ crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki
2515
+ darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen
2516
+ darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue
2517
+ dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite
2518
+ gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki
2519
+ lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan
2520
+ lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen
2521
+ lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen
2522
+ magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen
2523
+ mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream
2524
+ mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid
2525
+ palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum
2526
+ powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown
2527
+ seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen
2528
+ steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow
2529
+ yellowgreen
2530
+ accentcolor accentcolortext activetext buttonborder buttonface buttontext canvas canvastext
2531
+ field fieldtext graytext highlight highlighttext linktext mark marktext selecteditem
2532
+ selecteditemtext visitedtext`.trim().split(/\s+/));
2533
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
2534
+ /** One value, rather than a list or a space-separated shorthand. */
2535
+ const isSingleComponent = (value) => splitTopLevel(value, ",").length === 1 && splitTopLevel(value, " ").length === 1;
2536
+ /**
2537
+ * Is this single component provably a `<color>`?
2538
+ *
2539
+ * Provably, not plausibly. A false negative costs a fold; a false positive emits CSS the
2540
+ * browser drops on the floor without a word, which is the failure this whole file is written
2541
+ * around. So anything unrecognised is not a color.
2542
+ *
2543
+ * A `var()` is only a color when it names a token bamboo emitted from the `colors` category —
2544
+ * the reference itself says nothing about its type, and guessing wrong is exactly the
2545
+ * silent-drop case.
2546
+ */
2547
+ function isColorValue(value, colorVars) {
2548
+ if (value.startsWith("#")) return HEX_COLOR.test(value);
2549
+ const open = value.indexOf("(");
2550
+ if (open === -1) return COLOR_KEYWORDS.has(value.toLowerCase());
2551
+ if (!value.endsWith(")")) return false;
2552
+ const fn = value.slice(0, open).toLowerCase();
2553
+ if (fn !== "var") return COLOR_FUNCTIONS.has(fn);
2554
+ const [name] = splitTopLevel(value.slice(open + 1, -1), ",");
2555
+ return name !== void 0 && colorVars.has(name);
2556
+ }
2486
2557
  /**
2487
- * Collapse a token's `base`/`_osDark` pair into a single `light-dark()` declaration.
2558
+ * Merge a token's light and dark values into one, folding only the parts that differ.
2559
+ *
2560
+ * `light-dark()` is a `<color>` function — `light-dark() = light-dark(<color>, <color>)` in
2561
+ * CSS Color 5 — and that is the whole constraint here. It cannot carry a shadow, a border
2562
+ * shorthand or a length, and a browser handed one drops the declaration outright: verified in
2563
+ * Chrome, `--t: light-dark(0 1px 2px red, 0 1px 2px black)` computes `box-shadow: none`,
2564
+ * `light-dark(4px, 8px)` computes `padding-top: 0px`. Nothing warns. The token is still in the
2565
+ * sheet and the class naming it still looks correct.
2566
+ *
2567
+ * So a value is folded component by component rather than whole. `0 1px 2px red` against
2568
+ * `0 1px 2px black` differs in one place, that place is a color, and the result keeps the
2569
+ * geometry outside the function where it parses:
2570
+ *
2571
+ * 0 1px 2px light-dark(red, black)
2572
+ *
2573
+ * That is also what makes a list foldable, which folding whole never could: `light-dark()`
2574
+ * takes exactly two arguments and CSS offers no way to group a comma-separated value into one
2575
+ * of them, so a two-shadow token splatted into `light-dark(a, b, c)` and was dropped. Per-item
2576
+ * folding sidesteps the arity problem entirely, because the commas stay in the value where
2577
+ * they belong. A list of whole `light-dark()` calls does *not* work and was measured not to —
2578
+ * the arms have to be colors either way.
2579
+ *
2580
+ * Bails to `undefined` — keep the `@media` block, which expresses anything — whenever the two
2581
+ * arms do not line up part for part, or a differing part is not provably a color.
2582
+ */
2583
+ function foldValue(light, dark, colorVars) {
2584
+ const lightItems = splitTopLevel(light, ",");
2585
+ const darkItems = splitTopLevel(dark, ",");
2586
+ if (lightItems.length !== darkItems.length) return;
2587
+ const items = [];
2588
+ for (let index = 0; index < lightItems.length; index++) {
2589
+ const lightParts = splitTopLevel(lightItems[index], " ");
2590
+ const darkParts = splitTopLevel(darkItems[index], " ");
2591
+ if (lightParts.length !== darkParts.length) return;
2592
+ const merged = [];
2593
+ for (let part = 0; part < lightParts.length; part++) {
2594
+ const lightPart = lightParts[part];
2595
+ const darkPart = darkParts[part];
2596
+ if (lightPart === darkPart) {
2597
+ merged.push(lightPart);
2598
+ continue;
2599
+ }
2600
+ if (!isColorValue(lightPart, colorVars) || !isColorValue(darkPart, colorVars)) return;
2601
+ merged.push(`light-dark(${lightPart}, ${darkPart})`);
2602
+ }
2603
+ items.push(merged.join(" "));
2604
+ }
2605
+ return items.join(", ");
2606
+ }
2607
+ /**
2608
+ * Collapse a token's `base`/`_osDark` pair into a single declaration.
2488
2609
  *
2489
2610
  * Two declarations and a whole `@media (prefers-color-scheme: dark)` block become one line,
2490
2611
  * which for a design system carrying a few hundred `_osDark` semantic tokens is the largest
@@ -2494,7 +2615,9 @@ const hasTopLevelComma = (value) => {
2494
2615
  * `[data-theme=dark]` selector is not, so the two are independent mechanisms that resolve
2495
2616
  * against each other by source order. `light-dark()` reads `color-scheme`, which is an
2496
2617
  * ordinary inherited property — so a toggle is `color-scheme: dark` on a subtree rather than
2497
- * a second copy of every token.
2618
+ * a second copy of every token. That argument is why the fold has to reach past colors: a
2619
+ * sheet that folds its colors and leaves its shadows on the media query gives a subtree
2620
+ * toggle half a theme, with the shadows still following the OS.
2498
2621
  *
2499
2622
  * A var carrying `_osLight` as well is left alone. Folding it would put the light arm of
2500
2623
  * `light-dark()` and an `@media (prefers-color-scheme: light)` block in play for the same
@@ -2503,7 +2626,7 @@ const hasTopLevelComma = (value) => {
2503
2626
  *
2504
2627
  * `view.vars` is shared with the JS theme artifacts, so this copies rather than mutates.
2505
2628
  */
2506
- function foldLightDark(vars, conditions) {
2629
+ function foldLightDark(vars, conditions, colorVars) {
2507
2630
  const base = vars.get(BASE);
2508
2631
  const osDark = vars.get(OS_DARK);
2509
2632
  if (!base || !osDark) return {
@@ -2520,8 +2643,9 @@ function foldLightDark(vars, conditions) {
2520
2643
  for (const [name, darkValue] of osDark) {
2521
2644
  const lightValue = base.get(name);
2522
2645
  if (lightValue === void 0 || osLight?.has(name)) continue;
2523
- if (hasTopLevelComma(lightValue) || hasTopLevelComma(darkValue)) continue;
2524
- nextBase.set(name, `light-dark(${lightValue}, ${darkValue})`);
2646
+ const folded = colorVars.has(name) && isSingleComponent(lightValue) && isSingleComponent(darkValue) ? `light-dark(${lightValue}, ${darkValue})` : foldValue(lightValue, darkValue, colorVars);
2647
+ if (folded === void 0) continue;
2648
+ nextBase.set(name, folded);
2525
2649
  nextDark.delete(name);
2526
2650
  }
2527
2651
  if (nextDark.size === osDark.size) return {
@@ -2537,12 +2661,18 @@ function foldLightDark(vars, conditions) {
2537
2661
  folded: true
2538
2662
  };
2539
2663
  }
2664
+ /** The custom properties bamboo emitted for `colors` tokens, by var name. */
2665
+ function getColorVars(tokens) {
2666
+ const names = /* @__PURE__ */ new Set();
2667
+ for (const token of tokens.view.categoryMap.get("colors")?.values() ?? []) names.add(token.extensions.var);
2668
+ return names;
2669
+ }
2540
2670
  function generateTokenCss(ctx, sheet) {
2541
2671
  const { config, conditions, tokens } = ctx;
2542
2672
  const { cssVarRoot, staticCss } = config;
2543
2673
  const root = cssVarRoot;
2544
2674
  const results = [];
2545
- const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions);
2675
+ const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions, getColorVars(tokens));
2546
2676
  /**
2547
2677
  * `light-dark()` returns the light value unless `color-scheme` names both, and a stylesheet
2548
2678
  * that never sets it looks exactly like one where dark mode is broken. This rides with the
package/dist/index.mjs CHANGED
@@ -2424,41 +2424,162 @@ const OS_DARK_QUERY = "@media (prefers-color-scheme: dark)";
2424
2424
  /** Compare a condition against a known query without tripping over spacing. */
2425
2425
  const normalize = (condition) => typeof condition === "string" ? condition.replace(/\s+/g, " ").trim() : void 0;
2426
2426
  /**
2427
- * Does this value carry a comma at the top level?
2427
+ * Split a value on its top-level `separator`, ignoring one nested inside a function or a
2428
+ * string.
2428
2429
  *
2429
- * `light-dark()` takes exactly two arguments, and CSS offers no way to group a
2430
- * comma-separated value into one of them there is no parenthesized form of a shadow list.
2431
- * So a token whose light or dark arm is itself a list cannot be folded: the arms splat into
2432
- * three or more arguments, the function is invalid, and the browser drops the whole
2433
- * declaration. That failure is silent and total — a `--shadows-sm` carrying two shadows
2434
- * emitted `light-dark(a, b, c)` and every element referencing it rendered with no shadow at
2435
- * all, while the class naming it looked perfectly correct.
2436
- *
2437
- * Multi-part `box-shadow` is the shape that bites, since a realistic elevation token is
2438
- * almost always two shadows; `transition` and `background` lists are the same story.
2439
- *
2440
- * Depth-aware, so `rgb(16, 19, 26)` still folds — its commas are the function's own.
2441
- * Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a separator.
2430
+ * Depth-aware, so `rgb(16, 19, 26)` is one part rather than three its commas are the
2431
+ * function's own. Quote-aware, so a font stack's `"Foo, Bar"` is not mistaken for a
2432
+ * separator. Splitting on `' '` collapses runs of whitespace, which is what makes
2433
+ * `0 1px 2px red` and `0 1px 2px red` compare component for component.
2442
2434
  */
2443
- const hasTopLevelComma = (value) => {
2435
+ function splitTopLevel(value, separator) {
2436
+ const parts = [];
2437
+ let current = "";
2444
2438
  let depth = 0;
2445
2439
  let quote;
2440
+ const isSeparator = (char) => separator === "," ? char === "," : char === " " || char === " " || char === "\n" || char === "\r";
2446
2441
  for (let index = 0; index < value.length; index++) {
2447
2442
  const char = value[index];
2448
2443
  if (quote) {
2449
- if (char === "\\") index++;
2450
- else if (char === quote) quote = void 0;
2444
+ current += char;
2445
+ if (char === "\\") {
2446
+ current += value[index + 1] ?? "";
2447
+ index++;
2448
+ } else if (char === quote) quote = void 0;
2449
+ continue;
2450
+ }
2451
+ if (char === "\"" || char === "'") {
2452
+ quote = char;
2453
+ current += char;
2451
2454
  continue;
2452
2455
  }
2453
- if (char === "\"" || char === "'") quote = char;
2454
- else if (char === "(") depth++;
2456
+ if (char === "(") depth++;
2455
2457
  else if (char === ")") depth--;
2456
- else if (char === "," && depth === 0) return true;
2458
+ else if (depth === 0 && isSeparator(char)) {
2459
+ if (separator === "," || current.trim()) parts.push(current.trim());
2460
+ current = "";
2461
+ continue;
2462
+ }
2463
+ current += char;
2457
2464
  }
2458
- return false;
2459
- };
2465
+ if (separator === "," || current.trim()) parts.push(current.trim());
2466
+ return parts;
2467
+ }
2468
+ /** Functions whose result is a `<color>`. */
2469
+ const COLOR_FUNCTIONS = new Set([
2470
+ "color",
2471
+ "color-mix",
2472
+ "device-cmyk",
2473
+ "hsl",
2474
+ "hsla",
2475
+ "hwb",
2476
+ "lab",
2477
+ "lch",
2478
+ "light-dark",
2479
+ "oklab",
2480
+ "oklch",
2481
+ "rgb",
2482
+ "rgba"
2483
+ ]);
2484
+ /** Keywords that are a `<color>`: the named set, the system set, and the two specials. */
2485
+ const COLOR_KEYWORDS = new Set(`transparent currentcolor
2486
+ aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue
2487
+ blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk
2488
+ crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki
2489
+ darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen
2490
+ darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue
2491
+ dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite
2492
+ gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki
2493
+ lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan
2494
+ lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen
2495
+ lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen
2496
+ magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen
2497
+ mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream
2498
+ mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid
2499
+ palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum
2500
+ powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown
2501
+ seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen
2502
+ steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow
2503
+ yellowgreen
2504
+ accentcolor accentcolortext activetext buttonborder buttonface buttontext canvas canvastext
2505
+ field fieldtext graytext highlight highlighttext linktext mark marktext selecteditem
2506
+ selecteditemtext visitedtext`.trim().split(/\s+/));
2507
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
2508
+ /** One value, rather than a list or a space-separated shorthand. */
2509
+ const isSingleComponent = (value) => splitTopLevel(value, ",").length === 1 && splitTopLevel(value, " ").length === 1;
2510
+ /**
2511
+ * Is this single component provably a `<color>`?
2512
+ *
2513
+ * Provably, not plausibly. A false negative costs a fold; a false positive emits CSS the
2514
+ * browser drops on the floor without a word, which is the failure this whole file is written
2515
+ * around. So anything unrecognised is not a color.
2516
+ *
2517
+ * A `var()` is only a color when it names a token bamboo emitted from the `colors` category —
2518
+ * the reference itself says nothing about its type, and guessing wrong is exactly the
2519
+ * silent-drop case.
2520
+ */
2521
+ function isColorValue(value, colorVars) {
2522
+ if (value.startsWith("#")) return HEX_COLOR.test(value);
2523
+ const open = value.indexOf("(");
2524
+ if (open === -1) return COLOR_KEYWORDS.has(value.toLowerCase());
2525
+ if (!value.endsWith(")")) return false;
2526
+ const fn = value.slice(0, open).toLowerCase();
2527
+ if (fn !== "var") return COLOR_FUNCTIONS.has(fn);
2528
+ const [name] = splitTopLevel(value.slice(open + 1, -1), ",");
2529
+ return name !== void 0 && colorVars.has(name);
2530
+ }
2460
2531
  /**
2461
- * Collapse a token's `base`/`_osDark` pair into a single `light-dark()` declaration.
2532
+ * Merge a token's light and dark values into one, folding only the parts that differ.
2533
+ *
2534
+ * `light-dark()` is a `<color>` function — `light-dark() = light-dark(<color>, <color>)` in
2535
+ * CSS Color 5 — and that is the whole constraint here. It cannot carry a shadow, a border
2536
+ * shorthand or a length, and a browser handed one drops the declaration outright: verified in
2537
+ * Chrome, `--t: light-dark(0 1px 2px red, 0 1px 2px black)` computes `box-shadow: none`,
2538
+ * `light-dark(4px, 8px)` computes `padding-top: 0px`. Nothing warns. The token is still in the
2539
+ * sheet and the class naming it still looks correct.
2540
+ *
2541
+ * So a value is folded component by component rather than whole. `0 1px 2px red` against
2542
+ * `0 1px 2px black` differs in one place, that place is a color, and the result keeps the
2543
+ * geometry outside the function where it parses:
2544
+ *
2545
+ * 0 1px 2px light-dark(red, black)
2546
+ *
2547
+ * That is also what makes a list foldable, which folding whole never could: `light-dark()`
2548
+ * takes exactly two arguments and CSS offers no way to group a comma-separated value into one
2549
+ * of them, so a two-shadow token splatted into `light-dark(a, b, c)` and was dropped. Per-item
2550
+ * folding sidesteps the arity problem entirely, because the commas stay in the value where
2551
+ * they belong. A list of whole `light-dark()` calls does *not* work and was measured not to —
2552
+ * the arms have to be colors either way.
2553
+ *
2554
+ * Bails to `undefined` — keep the `@media` block, which expresses anything — whenever the two
2555
+ * arms do not line up part for part, or a differing part is not provably a color.
2556
+ */
2557
+ function foldValue(light, dark, colorVars) {
2558
+ const lightItems = splitTopLevel(light, ",");
2559
+ const darkItems = splitTopLevel(dark, ",");
2560
+ if (lightItems.length !== darkItems.length) return;
2561
+ const items = [];
2562
+ for (let index = 0; index < lightItems.length; index++) {
2563
+ const lightParts = splitTopLevel(lightItems[index], " ");
2564
+ const darkParts = splitTopLevel(darkItems[index], " ");
2565
+ if (lightParts.length !== darkParts.length) return;
2566
+ const merged = [];
2567
+ for (let part = 0; part < lightParts.length; part++) {
2568
+ const lightPart = lightParts[part];
2569
+ const darkPart = darkParts[part];
2570
+ if (lightPart === darkPart) {
2571
+ merged.push(lightPart);
2572
+ continue;
2573
+ }
2574
+ if (!isColorValue(lightPart, colorVars) || !isColorValue(darkPart, colorVars)) return;
2575
+ merged.push(`light-dark(${lightPart}, ${darkPart})`);
2576
+ }
2577
+ items.push(merged.join(" "));
2578
+ }
2579
+ return items.join(", ");
2580
+ }
2581
+ /**
2582
+ * Collapse a token's `base`/`_osDark` pair into a single declaration.
2462
2583
  *
2463
2584
  * Two declarations and a whole `@media (prefers-color-scheme: dark)` block become one line,
2464
2585
  * which for a design system carrying a few hundred `_osDark` semantic tokens is the largest
@@ -2468,7 +2589,9 @@ const hasTopLevelComma = (value) => {
2468
2589
  * `[data-theme=dark]` selector is not, so the two are independent mechanisms that resolve
2469
2590
  * against each other by source order. `light-dark()` reads `color-scheme`, which is an
2470
2591
  * ordinary inherited property — so a toggle is `color-scheme: dark` on a subtree rather than
2471
- * a second copy of every token.
2592
+ * a second copy of every token. That argument is why the fold has to reach past colors: a
2593
+ * sheet that folds its colors and leaves its shadows on the media query gives a subtree
2594
+ * toggle half a theme, with the shadows still following the OS.
2472
2595
  *
2473
2596
  * A var carrying `_osLight` as well is left alone. Folding it would put the light arm of
2474
2597
  * `light-dark()` and an `@media (prefers-color-scheme: light)` block in play for the same
@@ -2477,7 +2600,7 @@ const hasTopLevelComma = (value) => {
2477
2600
  *
2478
2601
  * `view.vars` is shared with the JS theme artifacts, so this copies rather than mutates.
2479
2602
  */
2480
- function foldLightDark(vars, conditions) {
2603
+ function foldLightDark(vars, conditions, colorVars) {
2481
2604
  const base = vars.get(BASE);
2482
2605
  const osDark = vars.get(OS_DARK);
2483
2606
  if (!base || !osDark) return {
@@ -2494,8 +2617,9 @@ function foldLightDark(vars, conditions) {
2494
2617
  for (const [name, darkValue] of osDark) {
2495
2618
  const lightValue = base.get(name);
2496
2619
  if (lightValue === void 0 || osLight?.has(name)) continue;
2497
- if (hasTopLevelComma(lightValue) || hasTopLevelComma(darkValue)) continue;
2498
- nextBase.set(name, `light-dark(${lightValue}, ${darkValue})`);
2620
+ const folded = colorVars.has(name) && isSingleComponent(lightValue) && isSingleComponent(darkValue) ? `light-dark(${lightValue}, ${darkValue})` : foldValue(lightValue, darkValue, colorVars);
2621
+ if (folded === void 0) continue;
2622
+ nextBase.set(name, folded);
2499
2623
  nextDark.delete(name);
2500
2624
  }
2501
2625
  if (nextDark.size === osDark.size) return {
@@ -2511,12 +2635,18 @@ function foldLightDark(vars, conditions) {
2511
2635
  folded: true
2512
2636
  };
2513
2637
  }
2638
+ /** The custom properties bamboo emitted for `colors` tokens, by var name. */
2639
+ function getColorVars(tokens) {
2640
+ const names = /* @__PURE__ */ new Set();
2641
+ for (const token of tokens.view.categoryMap.get("colors")?.values() ?? []) names.add(token.extensions.var);
2642
+ return names;
2643
+ }
2514
2644
  function generateTokenCss(ctx, sheet) {
2515
2645
  const { config, conditions, tokens } = ctx;
2516
2646
  const { cssVarRoot, staticCss } = config;
2517
2647
  const root = cssVarRoot;
2518
2648
  const results = [];
2519
- const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions);
2649
+ const { vars: tokenVars, folded } = foldLightDark(tokens.view.vars, conditions, getColorVars(tokens));
2520
2650
  /**
2521
2651
  * `light-dark()` returns the light value unless `color-scheme` names both, and a stylesheet
2522
2652
  * that never sets it looks exactly like one where dark mode is broken. This rides with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/generator",
3
- "version": "1.52.0",
3
+ "version": "1.53.1",
4
4
  "description": "The css generator for css bamboo",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -38,12 +38,12 @@
38
38
  "pluralize": "8.0.0",
39
39
  "postcss": "8.5.26",
40
40
  "ts-pattern": "5.9.0",
41
- "@bamboocss/core": "1.52.0",
42
- "@bamboocss/is-valid-prop": "^1.52.0",
43
- "@bamboocss/logger": "1.52.0",
44
- "@bamboocss/shared": "1.52.0",
45
- "@bamboocss/token-dictionary": "1.52.0",
46
- "@bamboocss/types": "1.52.0"
41
+ "@bamboocss/core": "1.53.1",
42
+ "@bamboocss/is-valid-prop": "^1.53.1",
43
+ "@bamboocss/logger": "1.53.1",
44
+ "@bamboocss/shared": "1.53.1",
45
+ "@bamboocss/token-dictionary": "1.53.1",
46
+ "@bamboocss/types": "1.53.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/pluralize": "0.0.33"