@barocss/browser 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -178,8 +178,12 @@ function clearContextCaches(ctx) {
178
178
  }
179
179
  const utilityRegistry = [];
180
180
  function registerUtility(util, ctx) {
181
- utilityRegistry.push(util);
182
- {
181
+ const state = ctx && getContextState(ctx);
182
+ if (ctx && !state) throw new Error("Utility registration requires a context from createContext");
183
+ (state?.utilities || utilityRegistry).push(util);
184
+ if (ctx) {
185
+ clearContextCaches(ctx);
186
+ } else {
183
187
  parseResultCache.clear();
184
188
  utilityCache.clear();
185
189
  }
@@ -273,7 +277,12 @@ function staticUtility(name, decls, opts, ctx) {
273
277
  description: opts?.description,
274
278
  category: opts?.category,
275
279
  priority: opts?.priority
276
- });
280
+ }, ctx);
281
+ }
282
+ function spacingKeyValue(ctx, key, negative) {
283
+ if (key === "px" || !/^[a-zA-Z][\w-]*$/.test(key) || ctx.theme("spacing", key) == null) return null;
284
+ const ref = `var(--spacing-${key})`;
285
+ return negative ? `calc(${ref} * -1)` : ref;
277
286
  }
278
287
  function functionalUtility(opts, ctx) {
279
288
  registerUtility({
@@ -343,14 +352,17 @@ function functionalUtility(opts, ctx) {
343
352
  if (opts.supportsFraction && /^-?\d+\/\d+$/.test(value)) {
344
353
  finalValue = value;
345
354
  }
355
+ const spacingKey = opts.spacingKeys ? spacingKeyValue(ctx2, String(finalValue).replace(/^-/, ""), !!parsedUtility.negative) : null;
346
356
  if (parsedUtility.negative && opts.supportsNegative && opts.handleNegativeBareValue) {
347
- const bare = opts.handleNegativeBareValue({ value: String(finalValue).replace(/^-/, ""), ctx: ctx2, token, extra });
357
+ const bare = opts.handleNegativeBareValue({ value: String(finalValue).replace(/^-/, ""), ctx: ctx2, token, extra }) ?? spacingKey;
348
358
  if (bare == null) return [];
349
359
  finalValue = bare;
350
360
  } else if (opts.handleBareValue) {
351
- const bare = opts.handleBareValue({ value: finalValue, ctx: ctx2, token, extra });
361
+ const bare = opts.handleBareValue({ value: finalValue, ctx: ctx2, token, extra }) ?? spacingKey;
352
362
  if (bare == null) return [];
353
363
  finalValue = bare;
364
+ } else if (spacingKey) {
365
+ finalValue = spacingKey;
354
366
  } else if (!/^-?(\d|\.\d)/.test(String(finalValue))) {
355
367
  return [];
356
368
  }
@@ -366,7 +378,7 @@ function functionalUtility(opts, ctx) {
366
378
  description: opts.description,
367
379
  category: opts.category,
368
380
  priority: opts.priority
369
- });
381
+ }, ctx);
370
382
  }
371
383
  const MATH_FNS = /* @__PURE__ */ new Set(["calc", "min", "max", "clamp"]);
372
384
  function expandThemeFunctions(value) {
@@ -485,14 +497,23 @@ function parseClassName(className, ctx) {
485
497
  if (cache.has(className)) {
486
498
  return cache.get(className);
487
499
  }
488
- let important = false;
489
500
  let realClassName = className;
490
- if (className.startsWith("!")) {
501
+ const classPrefix = ctx ? configuredClassPrefix(ctx) : "";
502
+ if (classPrefix) {
503
+ if (!className.startsWith(classPrefix + ":")) {
504
+ const none = { modifiers: [], utility: null };
505
+ cache.set(className, none);
506
+ return none;
507
+ }
508
+ realClassName = className.slice(classPrefix.length + 1);
509
+ }
510
+ let important = false;
511
+ if (realClassName.startsWith("!")) {
491
512
  important = true;
492
- realClassName = className.slice(1);
493
- } else if (className.length > 1 && className.endsWith("!")) {
513
+ realClassName = realClassName.slice(1);
514
+ } else if (realClassName.length > 1 && realClassName.endsWith("!")) {
494
515
  important = true;
495
- realClassName = className.slice(0, -1);
516
+ realClassName = realClassName.slice(0, -1);
496
517
  }
497
518
  const tokens = tokenize(realClassName);
498
519
  const result = parseTokens(tokens, ctx);
@@ -502,6 +523,10 @@ function parseClassName(className, ctx) {
502
523
  cache.set(className, result);
503
524
  return result;
504
525
  }
526
+ function configuredClassPrefix(ctx) {
527
+ const configured = ctx.config("prefix");
528
+ return typeof configured === "string" && /^[a-z]+$/.test(configured) ? configured : "";
529
+ }
505
530
  function parseTokens(tokens, ctx) {
506
531
  const modifiers = [];
507
532
  let utility = null;
@@ -561,6 +586,18 @@ function isSafeVariantToken(value) {
561
586
  function hasCommentToken(value) {
562
587
  return value.includes("/*") || value.includes("*/");
563
588
  }
589
+ function hasCommentDelimiter(text) {
590
+ for (let i = 0; i < text.length - 1; i++) {
591
+ const c = text[i];
592
+ if (c === "\\") {
593
+ i++;
594
+ continue;
595
+ }
596
+ const n = text[i + 1];
597
+ if (c === "/" && n === "*" || c === "*" && n === "/") return true;
598
+ }
599
+ return false;
600
+ }
564
601
  function isStructureSafeValue(value) {
565
602
  if (hasCommentToken(value)) return false;
566
603
  return isSafeVariantValue(value, true);
@@ -708,6 +745,7 @@ function parseUtility(value, ctx) {
708
745
  priority
709
746
  };
710
747
  }
748
+ const isSafePrelude = (text) => !hasCommentDelimiter(String(text ?? ""));
711
749
  const isSafeDecl = (prop, value) => isStructureSafeValue(String(prop)) && isStructureSafeValue(String(value ?? ""));
712
750
  const importantPrefix = "!important";
713
751
  function astToCss(ast, baseSelector, opts, _indent = "") {
@@ -771,6 +809,7 @@ function astToCss(ast, baseSelector, opts, _indent = "") {
771
809
  }).join(", ");
772
810
  }
773
811
  }
812
+ if (!isSafePrelude(selector)) return "";
774
813
  if (minify) {
775
814
  const css = `${indent}${selector}{${astToCss(
776
815
  node.nodes,
@@ -795,6 +834,7 @@ ${astToCss(
795
834
  }
796
835
  }
797
836
  case "style-rule": {
837
+ if (!isSafePrelude(node.selector)) return "";
798
838
  if (minify) {
799
839
  const css = `${indent}${node.selector} {${astToCss(
800
840
  node.nodes,
@@ -819,6 +859,7 @@ ${astToCss(
819
859
  }
820
860
  }
821
861
  case "at-rule": {
862
+ if (!isSafePrelude(node.name) || !isSafePrelude(node.params)) return "";
822
863
  if (minify) {
823
864
  const css = `${indent}@${node.name} ${node.params}{${astToCss(
824
865
  node.nodes,
@@ -871,7 +912,7 @@ function rootToCss(nodes, opts) {
871
912
  if (isSafeDecl(node.prop, node.value)) {
872
913
  list.push(`${node.prop}: ${node.value};`);
873
914
  }
874
- } else if (node.type === "at-rule") {
915
+ } else if (node.type === "at-rule" && isSafePrelude(node.name) && isSafePrelude(node.params)) {
875
916
  {
876
917
  list.push(
877
918
  `@${node.name} ${node.params} {
@@ -1005,24 +1046,41 @@ function animationToCssVars(animations2) {
1005
1046
  }
1006
1047
  return result;
1007
1048
  }
1008
- function keyframesToCss(keyframes2) {
1009
- if (!keyframes2) return "";
1010
- let css = "";
1011
- for (const name in keyframes2) {
1012
- const frames = keyframes2[name];
1013
- css += `@keyframes ${name} {
1049
+ const COMMENT_OR_BLOCK = /\/\*|\*\/|[{};]/;
1050
+ function keyframesBlock(name, frames) {
1051
+ if (!name || COMMENT_OR_BLOCK.test(name) || /\s/.test(name) || !frames || typeof frames !== "object") return "";
1052
+ let body = "";
1053
+ for (const [step, props] of Object.entries(frames)) {
1054
+ if (COMMENT_OR_BLOCK.test(step) || !props || typeof props !== "object") return "";
1055
+ let decls = "";
1056
+ for (const [prop, value] of Object.entries(props)) {
1057
+ const v2 = String(value);
1058
+ if (COMMENT_OR_BLOCK.test(prop) || COMMENT_OR_BLOCK.test(v2)) return "";
1059
+ decls += ` ${prop}: ${v2};
1014
1060
  `;
1015
- for (const step in frames) {
1016
- css += ` ${step} {`;
1017
- const props = frames[step];
1018
- for (const prop in props) {
1019
- css += ` ${prop}: ${props[prop]};`;
1020
- }
1021
- css += " }\n";
1022
1061
  }
1023
- css += "}\n";
1062
+ body += ` ${step} {
1063
+ ${decls} }
1064
+ `;
1065
+ }
1066
+ return `@keyframes ${name} {
1067
+ ${body}}`;
1068
+ }
1069
+ function referencedKeyframes(css, ctx) {
1070
+ if (!css.includes("animation")) return [];
1071
+ const all = ctx.theme("keyframes");
1072
+ if (!all || typeof all !== "object") return [];
1073
+ const names = /* @__PURE__ */ new Set();
1074
+ for (const m of css.matchAll(/(?:^|[\s;{])animation(?:-name)?\s*:\s*([^;}]+)/g)) {
1075
+ const value = m[1].replace(/var\(--animate-([\w-]+)\)/g, (whole, key) => {
1076
+ const v2 = ctx.theme("animations", key) ?? ctx.theme("animation", key);
1077
+ return typeof v2 === "string" ? v2 : whole;
1078
+ });
1079
+ for (const word of value.split(/[\s,()]+/)) {
1080
+ if (word && Object.prototype.hasOwnProperty.call(all, word)) names.add(word);
1081
+ }
1024
1082
  }
1025
- return css;
1083
+ return [...names].map((n) => keyframesBlock(n, all[n])).filter(Boolean);
1026
1084
  }
1027
1085
  function transitionTimingFunctionToCssVars(transition) {
1028
1086
  const result = {};
@@ -1090,7 +1148,7 @@ function themeToCssVarsAll(theme) {
1090
1148
  ...borderRadiusToCssVars(theme.borderRadius),
1091
1149
  ...zIndexToCssVars(theme.zIndex),
1092
1150
  ...opacityToCssVars(theme.opacity),
1093
- ...animationToCssVars(theme.animations),
1151
+ ...animationToCssVars({ ...theme.animations, ...theme.animation }),
1094
1152
  ...transitionTimingFunctionToCssVars(theme.transitionTimingFunction),
1095
1153
  ...transitionDurationToCssVars(theme.transitionDuration),
1096
1154
  ...transitionDelayToCssVars(theme.transitionDelay),
@@ -1099,8 +1157,13 @@ function themeToCssVarsAll(theme) {
1099
1157
  // keyframes handled separately
1100
1158
  };
1101
1159
  }
1160
+ function isSelfReferencingVar(name, value) {
1161
+ if (typeof value !== "string") return false;
1162
+ const m = /^var\(\s*(--[\w-]+)\s*(?:,[\s\S]*)?\)$/.exec(value.trim());
1163
+ return !!m && m[1] === name.trim();
1164
+ }
1102
1165
  function toCssVarsBlock(vars, extra = "") {
1103
- return ":root,:host {\n" + Object.entries(vars).map(([k, v2]) => ` ${k}: ${v2};`).join("\n") + "\n}\n" + extra + "\n";
1166
+ return ":root,:host {\n" + Object.entries(vars).filter(([k, v2]) => !isSelfReferencingVar(k, v2)).map(([k, v2]) => ` ${k}: ${v2};`).join("\n") + "\n}\n" + extra + "\n";
1104
1167
  }
1105
1168
  const BARO_VAR = /--baro-/g;
1106
1169
  const PREFIXED_KEYS = /* @__PURE__ */ new Set(["prop", "value", "params", "selector", "nodes", "items"]);
@@ -1451,6 +1514,7 @@ function generateCssRules(classList, ctx, opts) {
1451
1514
  const css = rootToCss([node]);
1452
1515
  rootCssList.push(css);
1453
1516
  }
1517
+ rootCssList.push(...referencedKeyframes(cssList.join("\n"), ctx));
1454
1518
  return {
1455
1519
  cls,
1456
1520
  ast: allCleanAst,
@@ -1713,6 +1777,15 @@ class IncrementalParser {
1713
1777
  markProcessed(cls) {
1714
1778
  this.processedClasses.add(cls);
1715
1779
  }
1780
+ /**
1781
+ * Forgets that a class was processed, so a later request generates it again
1782
+ * (used when the browser runtime reclaims an unused class's rules, #269).
1783
+ *
1784
+ * @param cls - The CSS class name to forget
1785
+ */
1786
+ unmarkProcessed(cls) {
1787
+ this.processedClasses.delete(cls);
1788
+ }
1716
1789
  /**
1717
1790
  * Process classes synchronously and update BrowserRuntime cache
1718
1791
  * This method is used by ChangeDetector for scan operations
@@ -2238,11 +2311,7 @@ const keyframes = {
2238
2311
  }
2239
2312
  },
2240
2313
  ping: {
2241
- "75%": {
2242
- transform: "scale(2)",
2243
- opacity: "0"
2244
- },
2245
- "100%": {
2314
+ "75%, 100%": {
2246
2315
  transform: "scale(2)",
2247
2316
  opacity: "0"
2248
2317
  }
@@ -2252,14 +2321,15 @@ const keyframes = {
2252
2321
  opacity: "0.5"
2253
2322
  }
2254
2323
  },
2324
+ // #274: Tailwind 4.1.13's frames (0%/100% share the up position; 50% is the floor).
2255
2325
  bounce: {
2256
- "0%": {
2326
+ "0%, 100%": {
2257
2327
  transform: "translateY(-25%)",
2258
- "animation-timing-function": "cubic-bezier(0.8,0,1,1)"
2328
+ "animation-timing-function": "cubic-bezier(0.8, 0, 1, 1)"
2259
2329
  },
2260
- "100%": {
2330
+ "50%": {
2261
2331
  transform: "none",
2262
- "animation-timing-function": "cubic-bezier(0,0,0.2,1)"
2332
+ "animation-timing-function": "cubic-bezier(0, 0, 0.2, 1)"
2263
2333
  }
2264
2334
  }
2265
2335
  };
@@ -2334,6 +2404,48 @@ const defaultTheme = {
2334
2404
  // Tailwind 4.1.13 --aspect-* (aspect-video → var(--aspect-video))
2335
2405
  aspect: { video: "16 / 9" }
2336
2406
  };
2407
+ const customUtilityName = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2408
+ const customUtilityProp = /^(--[A-Za-z0-9_-]+|-?[A-Za-z][A-Za-z0-9-]*)$/;
2409
+ function validateCustomUtility(name, decls) {
2410
+ if (typeof name !== "string" || !customUtilityName.test(name)) return null;
2411
+ if (!decls || typeof decls !== "object" || Array.isArray(decls)) return null;
2412
+ const out = [];
2413
+ for (const [prop, raw] of Object.entries(decls)) {
2414
+ if (typeof raw !== "string" && typeof raw !== "number") return null;
2415
+ const value = String(raw).trim();
2416
+ if (!customUtilityProp.test(prop) || !value || !isStructureSafeValue(value) || hasCommentDelimiter(value)) return null;
2417
+ out.push([prop, value]);
2418
+ }
2419
+ return out.length ? out : null;
2420
+ }
2421
+ function registerCustomUtilities(ctx, utilities) {
2422
+ if (!utilities || typeof utilities !== "object" || Array.isArray(utilities)) return;
2423
+ const list = getUtility(ctx);
2424
+ const builtins = [...list];
2425
+ const before = list.length;
2426
+ for (const [name, decls] of Object.entries(utilities)) {
2427
+ const safe = validateCustomUtility(name, decls);
2428
+ if (!safe) {
2429
+ debugWarn(`[BAROCSS] Ignoring invalid custom utility "${name}"`);
2430
+ continue;
2431
+ }
2432
+ const shadowed = builtins.filter((u) => u.match(name));
2433
+ registerUtility({
2434
+ name,
2435
+ category: "custom",
2436
+ match: (className) => className === name,
2437
+ handler: (value, c, token) => {
2438
+ let base = [];
2439
+ for (const reg of shadowed) {
2440
+ base = reg.handler(value, c, token, reg) || [];
2441
+ if (base.length > 0) break;
2442
+ }
2443
+ return [...base, ...safe.map(([prop, v]) => decl(prop, v))];
2444
+ }
2445
+ }, ctx);
2446
+ }
2447
+ if (list.length > before) list.unshift(...list.splice(before));
2448
+ }
2337
2449
  const preflightMinimalCSS = `
2338
2450
  /* BaroCSS Preflight - Minimal Reset */
2339
2451
  /* ================================= */
@@ -3216,9 +3328,7 @@ function resolveTheme(config) {
3216
3328
  }
3217
3329
  function themeToCssVars(theme) {
3218
3330
  const vars = themeToCssVarsAll(theme);
3219
- const result = toCssVarsBlock(vars, `
3220
- ${keyframesToCss(theme.keyframes || {})}
3221
- `);
3331
+ const result = toCssVarsBlock(vars);
3222
3332
  return result;
3223
3333
  }
3224
3334
  function createContext(configObj) {
@@ -3272,6 +3382,7 @@ function createContext(configObj) {
3272
3382
  }
3273
3383
  };
3274
3384
  initializeContextState(ctx, getUtility(), getModifier());
3385
+ registerCustomUtilities(ctx, configObj.utilities);
3275
3386
  return ctx;
3276
3387
  }
3277
3388
  function parseFraction(input) {
@@ -3682,6 +3793,7 @@ staticUtility("snap-proximity", [["--baro-scroll-snap-strictness", "proximity"]]
3682
3793
  ].forEach(([name, prop]) => {
3683
3794
  functionalUtility({
3684
3795
  name: `scroll-${name}`,
3796
+ spacingKeys: true,
3685
3797
  prop,
3686
3798
  supportsArbitrary: true,
3687
3799
  supportsCustomProperty: true,
@@ -3709,6 +3821,7 @@ staticUtility("snap-proximity", [["--baro-scroll-snap-strictness", "proximity"]]
3709
3821
  ].forEach(([name, prop]) => {
3710
3822
  functionalUtility({
3711
3823
  name: `scroll-${name}`,
3824
+ spacingKeys: true,
3712
3825
  prop,
3713
3826
  supportsArbitrary: true,
3714
3827
  supportsCustomProperty: true,
@@ -3867,10 +3980,12 @@ staticUtility("animate-bounce", [["animation", "var(--animate-bounce)"]], { cate
3867
3980
  staticUtility("animate-none", [["animation", "none"]], { category: "transitions" });
3868
3981
  functionalUtility({
3869
3982
  name: "animate",
3870
- prop: "animation",
3983
+ // #274: theme.animations (and Tailwind's theme.animation) names, e.g. theme.extend.animation.wiggle.
3984
+ themeKeys: ["animations", "animation"],
3871
3985
  supportsArbitrary: true,
3872
3986
  supportsCustomProperty: true,
3873
- handle: (value, ctx, token) => {
3987
+ handle: (value, ctx, token, extra) => {
3988
+ if (extra?.realThemeValue) return [decl("animation", `var(--animate-${extra.realThemeValue})`)];
3874
3989
  if (token.customProperty) {
3875
3990
  return [decl("animation", `var(${value})`)];
3876
3991
  }
@@ -5242,6 +5357,7 @@ staticUtility("sticky", [["position", "sticky"]], { category: "layout" });
5242
5357
  staticUtility(`-${name}-px`, [[prop, "-1px"]], { category: "layout" });
5243
5358
  functionalUtility({
5244
5359
  name,
5360
+ spacingKeys: true,
5245
5361
  prop,
5246
5362
  supportsNegative: true,
5247
5363
  supportsFraction: true,
@@ -5272,6 +5388,7 @@ staticUtility("invisible", [["visibility", "hidden"]], { category: "layout" });
5272
5388
  staticUtility("collapse", [["visibility", "collapse"]], { category: "layout" });
5273
5389
  functionalUtility({
5274
5390
  name: "gap-x",
5391
+ spacingKeys: true,
5275
5392
  prop: "column-gap",
5276
5393
  supportsArbitrary: true,
5277
5394
  // gap-x-[10vw]
@@ -5288,6 +5405,7 @@ functionalUtility({
5288
5405
  });
5289
5406
  functionalUtility({
5290
5407
  name: "gap-y",
5408
+ spacingKeys: true,
5291
5409
  prop: "row-gap",
5292
5410
  supportsArbitrary: true,
5293
5411
  // gap-y-[10vw]
@@ -5304,6 +5422,7 @@ functionalUtility({
5304
5422
  });
5305
5423
  functionalUtility({
5306
5424
  name: "gap",
5425
+ spacingKeys: true,
5307
5426
  prop: "gap",
5308
5427
  supportsArbitrary: true,
5309
5428
  // gap-[10vw]
@@ -5821,6 +5940,7 @@ functionalUtility({
5821
5940
  ].forEach(([name, prop]) => {
5822
5941
  staticUtility(`${name}-px`, [[prop, "1px"]], { category: "spacing" });
5823
5942
  functionalUtility({
5943
+ spacingKeys: true,
5824
5944
  name,
5825
5945
  prop,
5826
5946
  supportsArbitrary: true,
@@ -5845,6 +5965,7 @@ functionalUtility({
5845
5965
  staticUtility(`${name}-px`, [[prop, "1px"]], { category: "spacing" });
5846
5966
  staticUtility(`-${name}-px`, [[prop, "-1px"]], { category: "spacing" });
5847
5967
  functionalUtility({
5968
+ spacingKeys: true,
5848
5969
  name,
5849
5970
  prop,
5850
5971
  supportsNegative: true,
@@ -5875,6 +5996,7 @@ const SPACE_SELECTOR = ":where(& > :not(:last-child))";
5875
5996
  () => rule(SPACE_SELECTOR, [decl(rev, "1")])
5876
5997
  ], { category: "spacing" });
5877
5998
  functionalUtility({
5999
+ spacingKeys: true,
5878
6000
  name,
5879
6001
  supportsNegative: true,
5880
6002
  supportsArbitrary: true,
@@ -5924,6 +6046,7 @@ const SPACE_SELECTOR = ":where(& > :not(:last-child))";
5924
6046
  staticUtility(name, [["width", value]]);
5925
6047
  });
5926
6048
  functionalUtility({
6049
+ spacingKeys: true,
5927
6050
  name: "w",
5928
6051
  prop: "width",
5929
6052
  supportsArbitrary: true,
@@ -5956,6 +6079,7 @@ functionalUtility({
5956
6079
  staticUtility(name, [["width", w], ["height", h]]);
5957
6080
  });
5958
6081
  functionalUtility({
6082
+ spacingKeys: true,
5959
6083
  name: "size",
5960
6084
  supportsArbitrary: true,
5961
6085
  supportsCustomProperty: true,
@@ -5995,6 +6119,7 @@ functionalUtility({
5995
6119
  staticUtility(name, [["height", value]]);
5996
6120
  });
5997
6121
  functionalUtility({
6122
+ spacingKeys: true,
5998
6123
  name: "h",
5999
6124
  prop: "height",
6000
6125
  supportsArbitrary: true,
@@ -6025,6 +6150,7 @@ functionalUtility({
6025
6150
  staticUtility(name, [["min-height", value]], { category: "sizing" });
6026
6151
  });
6027
6152
  functionalUtility({
6153
+ spacingKeys: true,
6028
6154
  name: "min-h",
6029
6155
  prop: "min-height",
6030
6156
  supportsArbitrary: true,
@@ -6055,6 +6181,7 @@ functionalUtility({
6055
6181
  staticUtility(name, [["max-height", value]], { category: "sizing" });
6056
6182
  });
6057
6183
  functionalUtility({
6184
+ spacingKeys: true,
6058
6185
  name: "max-h",
6059
6186
  prop: "max-height",
6060
6187
  supportsArbitrary: true,
@@ -6101,6 +6228,7 @@ functionalUtility({
6101
6228
  staticUtility(name, [["min-width", value]], { category: "sizing" });
6102
6229
  });
6103
6230
  functionalUtility({
6231
+ spacingKeys: true,
6104
6232
  name: "min-w",
6105
6233
  prop: "min-width",
6106
6234
  supportsArbitrary: true,
@@ -6136,6 +6264,7 @@ functionalUtility({
6136
6264
  staticUtility(name, [["max-width", value]], { category: "sizing" });
6137
6265
  });
6138
6266
  functionalUtility({
6267
+ spacingKeys: true,
6139
6268
  name: "max-w",
6140
6269
  prop: "max-width",
6141
6270
  supportsArbitrary: true,
@@ -8795,6 +8924,41 @@ class StylePartitionManager {
8795
8924
  }
8796
8925
  return { success, failed };
8797
8926
  }
8927
+ /**
8928
+ * Remove one generated rule (#269 GC). Keeps `styles`, the #254 `keys` and the
8929
+ * sheet's cssRules parallel: one deleteRule at the rule's index, or a text
8930
+ * rebuild when the sheet isn't solely ours / has no CSSOM. Returns whether
8931
+ * the rule was found.
8932
+ */
8933
+ removeRule(rule2, category) {
8934
+ let partition;
8935
+ if (category) {
8936
+ if (this.classToCategoryPartitionMap.get(rule2) !== category) return false;
8937
+ partition = this.categoryPartitions.get(category);
8938
+ } else {
8939
+ const partitionIndex = this.classToPartitionMap.get(rule2);
8940
+ partition = partitionIndex === void 0 ? void 0 : this.partitions[partitionIndex];
8941
+ }
8942
+ if (!partition) return false;
8943
+ const index = partition.styles.indexOf(rule2);
8944
+ if (index === -1) return false;
8945
+ const sheet = partition.styleElement.sheet;
8946
+ const inSync = !!sheet && sheet.cssRules.length === partition.styles.length;
8947
+ partition.styles.splice(index, 1);
8948
+ partition.keys?.splice(index, 1);
8949
+ if (inSync && sheet) {
8950
+ sheet.deleteRule(index);
8951
+ } else {
8952
+ partition.styleElement.textContent = partition.styles.length ? partition.styles.join("\n") + "\n" : "";
8953
+ }
8954
+ if (category) this.classToCategoryPartitionMap.delete(rule2);
8955
+ else this.classToPartitionMap.delete(rule2);
8956
+ return true;
8957
+ }
8958
+ /** Number of generated (non-root, non-preflight) rules currently held. */
8959
+ get ruleCount() {
8960
+ return this.classToPartitionMap.size + this.classToCategoryPartitionMap.size;
8961
+ }
8798
8962
  /**
8799
8963
  * 특정 규칙이 어느 파티션에 있는지 찾기
8800
8964
  */
@@ -8860,10 +9024,14 @@ class ChangeDetector {
8860
9024
  */
8861
9025
  constructor(incrementalParser, BrowserRuntime2, getCategory = (cls) => parseClassName(cls).utility?.category) {
8862
9026
  this.observer = null;
9027
+ this.gc = null;
8863
9028
  this.incrementalParser = incrementalParser;
8864
9029
  this.BrowserRuntime = BrowserRuntime2;
8865
9030
  this.getCategory = getCategory;
8866
9031
  }
9032
+ setGc(gc) {
9033
+ this.gc = gc;
9034
+ }
8867
9035
  setParser(parser) {
8868
9036
  this.incrementalParser = parser;
8869
9037
  }
@@ -8890,9 +9058,19 @@ class ChangeDetector {
8890
9058
  if (this.observer) {
8891
9059
  this.observer.disconnect();
8892
9060
  }
9061
+ this.gc?.setRoot(root);
8893
9062
  this.observer = new MutationObserver((mutations) => {
8894
9063
  const newClasses = /* @__PURE__ */ new Set();
9064
+ const gc = this.gc;
8895
9065
  mutations.forEach((mutation) => {
9066
+ if (gc) {
9067
+ if (mutation.type === "attributes") {
9068
+ gc.reconcile(mutation.target);
9069
+ } else if (mutation.type === "childList") {
9070
+ mutation.removedNodes.forEach((node) => gc.reconcileTree(node));
9071
+ mutation.addedNodes.forEach((node) => gc.reconcileTree(node));
9072
+ }
9073
+ }
8896
9074
  if (mutation.type === "attributes" && mutation.attributeName === "class" && root.contains(mutation.target)) {
8897
9075
  const target = mutation.target;
8898
9076
  if (target.className) {
@@ -8923,6 +9101,7 @@ class ChangeDetector {
8923
9101
  } else {
8924
9102
  this.BrowserRuntime?.applyParseResults([]);
8925
9103
  }
9104
+ gc?.afterBatch();
8926
9105
  });
8927
9106
  this.observer.observe(root, {
8928
9107
  attributes: true,
@@ -9010,7 +9189,7 @@ class ChangeDetector {
9010
9189
  function unescapeCssIdent(s) {
9011
9190
  return s.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g, (_m, hex, ch) => hex ? String.fromCodePoint(parseInt(hex, 16)) : ch);
9012
9191
  }
9013
- const LEADING_CLASS = /^\s*\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/;
9192
+ const LEADING_CLASS = /^\s*(?::(?:where|is)\(\s*)?\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/;
9014
9193
  function splitTopLevel(sel) {
9015
9194
  const parts = [];
9016
9195
  let depth = 0, start = 0;
@@ -9027,6 +9206,17 @@ function splitTopLevel(sel) {
9027
9206
  parts.push(sel.slice(start));
9028
9207
  return parts;
9029
9208
  }
9209
+ function collectKeyframeNames(rules, out = /* @__PURE__ */ new Set()) {
9210
+ for (const rule2 of Array.from(rules)) {
9211
+ if (rule2.type === 7 && typeof rule2.name === "string") {
9212
+ out.add(rule2.name);
9213
+ continue;
9214
+ }
9215
+ const inner = rule2.cssRules;
9216
+ if (inner && inner.length) collectKeyframeNames(inner, out);
9217
+ }
9218
+ return out;
9219
+ }
9030
9220
  function collectLeadingClasses(rules, out = /* @__PURE__ */ new Set()) {
9031
9221
  for (const rule2 of Array.from(rules)) {
9032
9222
  const selectorText = rule2.selectorText;
@@ -9041,6 +9231,116 @@ function collectLeadingClasses(rules, out = /* @__PURE__ */ new Set()) {
9041
9231
  }
9042
9232
  return out;
9043
9233
  }
9234
+ class ClassGc {
9235
+ constructor(host, graceMs, maxRules, now = () => Date.now()) {
9236
+ this.host = host;
9237
+ this.graceMs = graceMs;
9238
+ this.maxRules = maxRules;
9239
+ this.now = now;
9240
+ this.counts = /* @__PURE__ */ new Map();
9241
+ this.counted = /* @__PURE__ */ new WeakMap();
9242
+ this.candidates = /* @__PURE__ */ new Map();
9243
+ this.timer = null;
9244
+ this.root = null;
9245
+ }
9246
+ /** Start counting for a new root: count every element currently inside it. */
9247
+ setRoot(root) {
9248
+ this.counts.clear();
9249
+ this.counted = /* @__PURE__ */ new WeakMap();
9250
+ this.candidates.clear();
9251
+ this.cancel();
9252
+ this.root = root;
9253
+ this.reconcileTree(root);
9254
+ }
9255
+ count(cls) {
9256
+ return this.counts.get(cls) ?? 0;
9257
+ }
9258
+ /** Re-count `el` and (optionally) all its descendants from their current state. */
9259
+ reconcileTree(node) {
9260
+ if (node.nodeType !== 1) return;
9261
+ const el = node;
9262
+ this.reconcile(el);
9263
+ el.querySelectorAll("[class]").forEach((child) => this.reconcile(child));
9264
+ }
9265
+ reconcile(el) {
9266
+ const root = this.root;
9267
+ const live = !!root && root.contains(el);
9268
+ const next = live ? Array.from(new Set(normalizeClassNameList(el.getAttribute("class")))) : [];
9269
+ const prev = this.counted.get(el);
9270
+ if (!prev && next.length === 0) return;
9271
+ const prevSet = new Set(prev ?? []);
9272
+ const nextSet = new Set(next);
9273
+ for (const cls of nextSet) {
9274
+ if (prevSet.has(cls)) continue;
9275
+ const c = (this.counts.get(cls) ?? 0) + 1;
9276
+ this.counts.set(cls, c);
9277
+ this.candidates.delete(cls);
9278
+ }
9279
+ for (const cls of prevSet) {
9280
+ if (nextSet.has(cls)) continue;
9281
+ const c = (this.counts.get(cls) ?? 0) - 1;
9282
+ if (c > 0) {
9283
+ this.counts.set(cls, c);
9284
+ } else {
9285
+ this.counts.delete(cls);
9286
+ this.candidates.delete(cls);
9287
+ this.candidates.set(cls, this.now());
9288
+ }
9289
+ }
9290
+ if (next.length) this.counted.set(el, next);
9291
+ else this.counted.delete(el);
9292
+ }
9293
+ /** Call after a mutation batch has been counted and its classes inserted. */
9294
+ afterBatch() {
9295
+ if (this.candidates.size === 0) return;
9296
+ if (this.host.cachedCount() > this.maxRules) {
9297
+ this.schedule(0);
9298
+ } else {
9299
+ this.schedule(this.graceMs);
9300
+ }
9301
+ }
9302
+ schedule(delay) {
9303
+ if (this.timer !== null) {
9304
+ if (delay > 0) return;
9305
+ clearTimeout(this.timer);
9306
+ }
9307
+ this.timer = setTimeout(() => {
9308
+ this.timer = null;
9309
+ this.sweep();
9310
+ }, delay);
9311
+ }
9312
+ /** Reclaim candidates whose grace period elapsed (plus LRU overflow). Public for tests. */
9313
+ sweep() {
9314
+ const now = this.now();
9315
+ const overflow = Math.max(0, this.host.cachedCount() - this.maxRules);
9316
+ const doomed = [];
9317
+ let evicted = 0;
9318
+ for (const [cls, since] of this.candidates) {
9319
+ const expired = now - since >= this.graceMs;
9320
+ if (!expired && evicted >= overflow) continue;
9321
+ this.candidates.delete(cls);
9322
+ if (this.count(cls) > 0 || this.inDom(cls) || this.host.isPermanent(cls)) continue;
9323
+ doomed.push(cls);
9324
+ if (!expired) evicted++;
9325
+ }
9326
+ if (doomed.length) this.host.reclaim(doomed);
9327
+ if (this.candidates.size) this.schedule(this.graceMs);
9328
+ }
9329
+ inDom(cls) {
9330
+ const root = this.root;
9331
+ if (!root) return false;
9332
+ const doc = root.ownerDocument ?? document;
9333
+ return root.classList.contains(cls) || doc.documentElement.classList.contains(cls) || doc.getElementsByClassName(cls).length > 0;
9334
+ }
9335
+ cancel() {
9336
+ if (this.timer !== null) clearTimeout(this.timer);
9337
+ this.timer = null;
9338
+ }
9339
+ stats() {
9340
+ return { trackedClasses: this.counts.size, candidates: this.candidates.size };
9341
+ }
9342
+ }
9343
+ const SSR_STYLE_SELECTOR = "style[data-barocss-ssr]";
9044
9344
  const LAYER_ORDER = "@layer theme, base, components, utilities;";
9045
9345
  class BrowserRuntime {
9046
9346
  constructor(options = {}) {
@@ -9048,7 +9348,14 @@ class BrowserRuntime {
9048
9348
  this.rootCache = /* @__PURE__ */ new Set();
9049
9349
  this.isDestroyed = false;
9050
9350
  this.existing = null;
9351
+ this.existingKeyframes = /* @__PURE__ */ new Set();
9051
9352
  this.existingSheetCount = -1;
9353
+ this.pinned = /* @__PURE__ */ new Set();
9354
+ this.gc = null;
9355
+ this.reclaimedCount = 0;
9356
+ this.ssrRules = [];
9357
+ this.ssrClasses = /* @__PURE__ */ new Set();
9358
+ this.observedOnce = false;
9052
9359
  this.getCategory = (cls) => parseClassName(cls, this.context).utility?.category;
9053
9360
  const defaultConfig = {};
9054
9361
  this.options = {
@@ -9056,12 +9363,23 @@ class BrowserRuntime {
9056
9363
  styleId: options.styleId || "barocss-runtime",
9057
9364
  insertionPoint: options.insertionPoint || "head",
9058
9365
  maxRulesPerPartition: options.maxRulesPerPartition || 50,
9059
- skipExisting: options.skipExisting ?? false
9366
+ skipExisting: options.skipExisting ?? false,
9367
+ gc: options.gc ?? true,
9368
+ gcGraceMs: options.gcGraceMs ?? 3e3,
9369
+ maxRules: options.maxRules ?? Infinity
9060
9370
  };
9061
9371
  this.context = createContext(this.options.config);
9062
9372
  this.incrementalParser = new IncrementalParser(this.context);
9063
9373
  this.changeDetector = new ChangeDetector(this.incrementalParser, this, this.getCategory);
9064
9374
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9375
+ if (this.options.gc) {
9376
+ this.gc = new ClassGc({
9377
+ reclaim: (classes) => this.reclaim(classes),
9378
+ isPermanent: (cls) => this.isPermanent(cls),
9379
+ cachedCount: () => this.cache.size
9380
+ }, this.options.gcGraceMs, this.options.maxRules);
9381
+ this.changeDetector.setGc(this.gc);
9382
+ }
9065
9383
  this.init();
9066
9384
  }
9067
9385
  // Debugging and logging helpers
@@ -9076,6 +9394,44 @@ class BrowserRuntime {
9076
9394
  console.log("[BrowserRuntime] init");
9077
9395
  this.injectPreflightCSS();
9078
9396
  this.ensureCssVars();
9397
+ this.adoptSsrSheets();
9398
+ }
9399
+ /**
9400
+ * #268: adopt the class rules of server-rendered `<style data-barocss-ssr>` sheets in <head>, at startup
9401
+ * (constructor and the first observe()). Each rule moves
9402
+ * (same task, so no paint in between) into the partition its class would get if generated here, at
9403
+ * its #254 sorted position, so a later client `sm:` rule lands before a server `lg:` rule. Its classes
9404
+ * are never regenerated and never reclaimed. `:root`, `@property` and `@keyframes` stay in the sheet.
9405
+ */
9406
+ adoptSsrSheets() {
9407
+ if (typeof document === "undefined") return;
9408
+ const adopted = [];
9409
+ if (!document.head) return;
9410
+ for (const el of Array.from(document.head.querySelectorAll(`${SSR_STYLE_SELECTOR}:not([data-barocss-adopted])`))) {
9411
+ const sheet = el.sheet;
9412
+ if (!sheet) continue;
9413
+ el.setAttribute("data-barocss-adopted", "");
9414
+ const moved = [];
9415
+ for (let i = sheet.cssRules.length - 1; i >= 0; i--) {
9416
+ const rule2 = sheet.cssRules[i];
9417
+ const classes = collectLeadingClasses([rule2]);
9418
+ if (classes.size === 0) continue;
9419
+ classes.forEach((cls) => this.ssrClasses.add(cls));
9420
+ moved.unshift({ css: rule2.cssText, cls: classes.values().next().value });
9421
+ sheet.deleteRule(i);
9422
+ }
9423
+ adopted.push(...moved);
9424
+ }
9425
+ if (adopted.length === 0) return;
9426
+ this.ssrRules.push(...adopted);
9427
+ this.insertSsrRules(adopted);
9428
+ }
9429
+ insertSsrRules(rules) {
9430
+ for (const { css, cls } of rules) {
9431
+ const category = this.getCategory(cls);
9432
+ if (category) this.stylePartitionManager.addCategoryRule(css, category);
9433
+ else this.stylePartitionManager.addRule(css);
9434
+ }
9079
9435
  }
9080
9436
  injectPreflightCSS() {
9081
9437
  const level = this.options.config.preflight ?? true;
@@ -9113,7 +9469,8 @@ ${preflightCSS}
9113
9469
  */
9114
9470
  addClass(classes) {
9115
9471
  if (this.isDestroyed) return;
9116
- const classList = this.normalizeClasses(classes);
9472
+ const classList = this.normalizeClasses(classes).filter(Boolean);
9473
+ classList.forEach((cls) => this.pinned.add(cls));
9117
9474
  this.processClasses(classList);
9118
9475
  }
9119
9476
  /**
@@ -9142,6 +9499,7 @@ ${preflightCSS}
9142
9499
  results = [...existingResults, ...results];
9143
9500
  results.forEach((result) => this.incrementalParser.markProcessed(result.cls));
9144
9501
  }
9502
+ if (this.ssrClasses.size > 0) results = results.filter((result) => !this.ssrClasses.has(result.cls));
9145
9503
  if (this.options.skipExisting && results.length > 0 && typeof document !== "undefined") {
9146
9504
  const existing = this.getExistingClasses();
9147
9505
  results = results.filter((result) => !existing.has(result.cls));
@@ -9149,6 +9507,7 @@ ${preflightCSS}
9149
9507
  if (results.length === 0) return;
9150
9508
  const cssRules = [];
9151
9509
  const rootCssRules = [];
9510
+ const pageKeyframes = this.options.skipExisting && typeof document !== "undefined" ? (this.getExistingClasses(), this.existingKeyframes) : null;
9152
9511
  for (const result of results) {
9153
9512
  if (result.css && Array.isArray(result.cssList)) {
9154
9513
  cssRules.push(result);
@@ -9156,6 +9515,10 @@ ${preflightCSS}
9156
9515
  }
9157
9516
  if (result.rootCss && Array.isArray(result.rootCssList)) {
9158
9517
  for (const rootCss of result.rootCssList) {
9518
+ if (pageKeyframes?.size) {
9519
+ const kf = /^\s*@keyframes\s+([^\s{]+)/.exec(rootCss)?.[1];
9520
+ if (kf && pageKeyframes.has(kf)) continue;
9521
+ }
9159
9522
  if (!this.rootCache.has(rootCss)) {
9160
9523
  this.rootCache.add(rootCss);
9161
9524
  rootCssRules.push(rootCss);
@@ -9174,14 +9537,46 @@ ${preflightCSS}
9174
9537
  rootCssCount: rootCssRules.length
9175
9538
  });
9176
9539
  }
9540
+ /** #269: a class that must never be reclaimed. */
9541
+ isPermanent(cls) {
9542
+ if (this.pinned.has(cls) || this.ssrClasses.has(cls)) return true;
9543
+ if (typeof document === "undefined") return true;
9544
+ return this.getExistingClasses().has(cls);
9545
+ }
9546
+ /**
9547
+ * #269: delete the generated rules of classes no live element uses. Root/@property/@keyframes rules stay
9548
+ * (they are shared and harmless); a rule text another cached class still emits is kept.
9549
+ */
9550
+ reclaim(classes) {
9551
+ if (this.isDestroyed) return;
9552
+ const victims = classes.filter((cls) => this.cache.has(cls));
9553
+ if (victims.length === 0) return;
9554
+ const results = victims.map((cls) => this.cache.get(cls));
9555
+ victims.forEach((cls) => {
9556
+ this.cache.delete(cls);
9557
+ this.incrementalParser.unmarkProcessed(cls);
9558
+ });
9559
+ const stillUsed = /* @__PURE__ */ new Set();
9560
+ for (const result of this.cache.values()) result.cssList.forEach((css) => stillUsed.add(css));
9561
+ for (const result of results) {
9562
+ const category = this.getCategory(result.cls);
9563
+ for (const css of result.cssList) {
9564
+ if (!stillUsed.has(css)) this.stylePartitionManager.removeRule(css, category);
9565
+ }
9566
+ }
9567
+ this.reclaimedCount += victims.length;
9568
+ }
9177
9569
  /** Class names defined by the page's own stylesheets (BaroCSS's sheets and cross-origin sheets excluded). */
9178
9570
  getExistingClasses() {
9571
+ const own = new Set(Array.from(document.querySelectorAll("style[data-barocss]"), (s) => s.sheet));
9179
9572
  const sheets = Array.from(document.styleSheets).filter((sheet) => {
9573
+ if (own.has(sheet)) return false;
9180
9574
  const owner = sheet.ownerNode;
9181
9575
  return !(owner && typeof owner.hasAttribute === "function" && (owner.hasAttribute("data-barocss") || (owner.id || "").startsWith(this.options.styleId)));
9182
9576
  });
9183
9577
  if (this.existing && sheets.length === this.existingSheetCount) return this.existing;
9184
9578
  const out = /* @__PURE__ */ new Set();
9579
+ const keyframes2 = /* @__PURE__ */ new Set();
9185
9580
  for (const sheet of sheets) {
9186
9581
  let rules;
9187
9582
  try {
@@ -9190,7 +9585,9 @@ ${preflightCSS}
9190
9585
  continue;
9191
9586
  }
9192
9587
  collectLeadingClasses(rules, out);
9588
+ collectKeyframeNames(rules, keyframes2);
9193
9589
  }
9590
+ this.existingKeyframes = keyframes2;
9194
9591
  this.existing = out;
9195
9592
  this.existingSheetCount = sheets.length;
9196
9593
  return out;
@@ -9199,6 +9596,10 @@ ${preflightCSS}
9199
9596
  * MutationObserver instance method to automatically call addClass when class attributes change in DOM
9200
9597
  */
9201
9598
  observe(root = document.body, options) {
9599
+ if (!this.observedOnce) {
9600
+ this.observedOnce = true;
9601
+ this.adoptSsrSheets();
9602
+ }
9202
9603
  return this.changeDetector.observe(root, options);
9203
9604
  }
9204
9605
  normalizeClasses(classes) {
@@ -9228,7 +9629,10 @@ ${preflightCSS}
9228
9629
  return {
9229
9630
  runtime: {
9230
9631
  cachedClasses: this.cache.size,
9231
- rootCacheSize: this.rootCache.size
9632
+ rootCacheSize: this.rootCache.size,
9633
+ ruleCount: this.stylePartitionManager.ruleCount,
9634
+ reclaimedClasses: this.reclaimedCount,
9635
+ gc: this.gc?.stats() ?? null
9232
9636
  },
9233
9637
  ast: incremental.cacheStats.ast,
9234
9638
  incremental
@@ -9247,6 +9651,7 @@ ${preflightCSS}
9247
9651
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9248
9652
  this.injectPreflightCSS();
9249
9653
  this.ensureCssVars();
9654
+ this.insertSsrRules(this.ssrRules);
9250
9655
  }
9251
9656
  reset() {
9252
9657
  if (this.isDestroyed) return;
@@ -9257,6 +9662,7 @@ ${preflightCSS}
9257
9662
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9258
9663
  this.injectPreflightCSS();
9259
9664
  this.ensureCssVars();
9665
+ this.insertSsrRules(this.ssrRules);
9260
9666
  }
9261
9667
  updateConfig(newConfig) {
9262
9668
  if (this.isDestroyed) return;
@@ -9282,6 +9688,7 @@ ${preflightCSS}
9282
9688
  destroy() {
9283
9689
  if (this.isDestroyed) return;
9284
9690
  this.changeDetector.disconnect();
9691
+ this.gc?.cancel();
9285
9692
  this.stylePartitionManager.cleanup();
9286
9693
  this.cache.clear();
9287
9694
  this.rootCache.clear();
@@ -9398,6 +9805,7 @@ export {
9398
9805
  BrowserRuntime,
9399
9806
  ChangeDetector,
9400
9807
  LAYER_ORDER,
9808
+ SSR_STYLE_SELECTOR,
9401
9809
  StylePartitionManager,
9402
9810
  baroBoot,
9403
9811
  baroStart,