@bamboocss/generator 1.53.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.
- package/dist/index.cjs +158 -28
- package/dist/index.mjs +158 -28
- 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
|
-
*
|
|
2453
|
+
* Split a value on its top-level `separator`, ignoring one nested inside a function or a
|
|
2454
|
+
* string.
|
|
2454
2455
|
*
|
|
2455
|
-
* `
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2476
|
-
|
|
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 === "
|
|
2480
|
-
else if (char === "(") depth++;
|
|
2482
|
+
if (char === "(") depth++;
|
|
2481
2483
|
else if (char === ")") depth--;
|
|
2482
|
-
else if (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2524
|
-
|
|
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
|
-
*
|
|
2427
|
+
* Split a value on its top-level `separator`, ignoring one nested inside a function or a
|
|
2428
|
+
* string.
|
|
2428
2429
|
*
|
|
2429
|
-
* `
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2450
|
-
|
|
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 === "
|
|
2454
|
-
else if (char === "(") depth++;
|
|
2456
|
+
if (char === "(") depth++;
|
|
2455
2457
|
else if (char === ")") depth--;
|
|
2456
|
-
else if (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2498
|
-
|
|
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.53.
|
|
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.53.
|
|
42
|
-
"@bamboocss/is-valid-prop": "^1.53.
|
|
43
|
-
"@bamboocss/logger": "1.53.
|
|
44
|
-
"@bamboocss/shared": "1.53.
|
|
45
|
-
"@bamboocss/token-dictionary": "1.53.
|
|
46
|
-
"@bamboocss/types": "1.53.
|
|
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"
|