@barocss/browser 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,8 @@ renderJsonUi(spec); // mount your json-render Renderer
33
33
 
34
34
  The five settings: `skipExisting: true`, `cssVarPrefix: 'tw'`, `theme: { extend: shadcnTheme }`, `preloadJsonRenderClasses(spec, runtime)` before mount, and the default layered preflight (don't set `preflight: false`).
35
35
 
36
+ **Non-shadcn site theme:** put the site's own tokens in `theme.extend` (e.g. `colors: { brand: { 600: '#2563eb' } }`). Literal values are safe. Pointing a token at the build's own var name (`brand: { 600: 'var(--color-brand-600)' }`) is also fine: BaroCSS skips that self-referencing `:root` var, so the build's value wins and `bg-brand-600` still uses it.
37
+
36
38
  **Verify it rendered** (DevTools console, after mount):
37
39
 
38
40
  ```js
@@ -275,6 +275,11 @@ function staticUtility(name, decls, opts, ctx) {
275
275
  priority: opts?.priority
276
276
  });
277
277
  }
278
+ function spacingKeyValue(ctx, key, negative) {
279
+ if (key === "px" || !/^[a-zA-Z][\w-]*$/.test(key) || ctx.theme("spacing", key) == null) return null;
280
+ const ref = `var(--spacing-${key})`;
281
+ return negative ? `calc(${ref} * -1)` : ref;
282
+ }
278
283
  function functionalUtility(opts, ctx) {
279
284
  registerUtility({
280
285
  name: opts.name,
@@ -343,14 +348,17 @@ function functionalUtility(opts, ctx) {
343
348
  if (opts.supportsFraction && /^-?\d+\/\d+$/.test(value)) {
344
349
  finalValue = value;
345
350
  }
351
+ const spacingKey = opts.spacingKeys ? spacingKeyValue(ctx2, String(finalValue).replace(/^-/, ""), !!parsedUtility.negative) : null;
346
352
  if (parsedUtility.negative && opts.supportsNegative && opts.handleNegativeBareValue) {
347
- const bare = opts.handleNegativeBareValue({ value: String(finalValue).replace(/^-/, ""), ctx: ctx2, token, extra });
353
+ const bare = opts.handleNegativeBareValue({ value: String(finalValue).replace(/^-/, ""), ctx: ctx2, token, extra }) ?? spacingKey;
348
354
  if (bare == null) return [];
349
355
  finalValue = bare;
350
356
  } else if (opts.handleBareValue) {
351
- const bare = opts.handleBareValue({ value: finalValue, ctx: ctx2, token, extra });
357
+ const bare = opts.handleBareValue({ value: finalValue, ctx: ctx2, token, extra }) ?? spacingKey;
352
358
  if (bare == null) return [];
353
359
  finalValue = bare;
360
+ } else if (spacingKey) {
361
+ finalValue = spacingKey;
354
362
  } else if (!/^-?(\d|\.\d)/.test(String(finalValue))) {
355
363
  return [];
356
364
  }
@@ -561,6 +569,18 @@ function isSafeVariantToken(value) {
561
569
  function hasCommentToken(value) {
562
570
  return value.includes("/*") || value.includes("*/");
563
571
  }
572
+ function hasCommentDelimiter(text) {
573
+ for (let i = 0; i < text.length - 1; i++) {
574
+ const c = text[i];
575
+ if (c === "\\") {
576
+ i++;
577
+ continue;
578
+ }
579
+ const n = text[i + 1];
580
+ if (c === "/" && n === "*" || c === "*" && n === "/") return true;
581
+ }
582
+ return false;
583
+ }
564
584
  function isStructureSafeValue(value) {
565
585
  if (hasCommentToken(value)) return false;
566
586
  return isSafeVariantValue(value, true);
@@ -708,6 +728,7 @@ function parseUtility(value, ctx) {
708
728
  priority
709
729
  };
710
730
  }
731
+ const isSafePrelude = (text) => !hasCommentDelimiter(String(text ?? ""));
711
732
  const isSafeDecl = (prop, value) => isStructureSafeValue(String(prop)) && isStructureSafeValue(String(value ?? ""));
712
733
  const importantPrefix = "!important";
713
734
  function astToCss(ast, baseSelector, opts, _indent = "") {
@@ -771,6 +792,7 @@ function astToCss(ast, baseSelector, opts, _indent = "") {
771
792
  }).join(", ");
772
793
  }
773
794
  }
795
+ if (!isSafePrelude(selector)) return "";
774
796
  if (minify) {
775
797
  const css = `${indent}${selector}{${astToCss(
776
798
  node.nodes,
@@ -795,6 +817,7 @@ ${astToCss(
795
817
  }
796
818
  }
797
819
  case "style-rule": {
820
+ if (!isSafePrelude(node.selector)) return "";
798
821
  if (minify) {
799
822
  const css = `${indent}${node.selector} {${astToCss(
800
823
  node.nodes,
@@ -819,6 +842,7 @@ ${astToCss(
819
842
  }
820
843
  }
821
844
  case "at-rule": {
845
+ if (!isSafePrelude(node.name) || !isSafePrelude(node.params)) return "";
822
846
  if (minify) {
823
847
  const css = `${indent}@${node.name} ${node.params}{${astToCss(
824
848
  node.nodes,
@@ -871,7 +895,7 @@ function rootToCss(nodes, opts) {
871
895
  if (isSafeDecl(node.prop, node.value)) {
872
896
  list.push(`${node.prop}: ${node.value};`);
873
897
  }
874
- } else if (node.type === "at-rule") {
898
+ } else if (node.type === "at-rule" && isSafePrelude(node.name) && isSafePrelude(node.params)) {
875
899
  {
876
900
  list.push(
877
901
  `@${node.name} ${node.params} {
@@ -1099,8 +1123,13 @@ function themeToCssVarsAll(theme) {
1099
1123
  // keyframes handled separately
1100
1124
  };
1101
1125
  }
1126
+ function isSelfReferencingVar(name, value) {
1127
+ if (typeof value !== "string") return false;
1128
+ const m = /^var\(\s*(--[\w-]+)\s*(?:,[\s\S]*)?\)$/.exec(value.trim());
1129
+ return !!m && m[1] === name.trim();
1130
+ }
1102
1131
  function toCssVarsBlock(vars, extra = "") {
1103
- return ":root,:host {\n" + Object.entries(vars).map(([k, v2]) => ` ${k}: ${v2};`).join("\n") + "\n}\n" + extra + "\n";
1132
+ 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
1133
  }
1105
1134
  const BARO_VAR = /--baro-/g;
1106
1135
  const PREFIXED_KEYS = /* @__PURE__ */ new Set(["prop", "value", "params", "selector", "nodes", "items"]);
@@ -1713,6 +1742,15 @@ class IncrementalParser {
1713
1742
  markProcessed(cls) {
1714
1743
  this.processedClasses.add(cls);
1715
1744
  }
1745
+ /**
1746
+ * Forgets that a class was processed, so a later request generates it again
1747
+ * (used when the browser runtime reclaims an unused class's rules, #269).
1748
+ *
1749
+ * @param cls - The CSS class name to forget
1750
+ */
1751
+ unmarkProcessed(cls) {
1752
+ this.processedClasses.delete(cls);
1753
+ }
1716
1754
  /**
1717
1755
  * Process classes synchronously and update BrowserRuntime cache
1718
1756
  * This method is used by ChangeDetector for scan operations
@@ -3682,6 +3720,7 @@ staticUtility("snap-proximity", [["--baro-scroll-snap-strictness", "proximity"]]
3682
3720
  ].forEach(([name, prop]) => {
3683
3721
  functionalUtility({
3684
3722
  name: `scroll-${name}`,
3723
+ spacingKeys: true,
3685
3724
  prop,
3686
3725
  supportsArbitrary: true,
3687
3726
  supportsCustomProperty: true,
@@ -3709,6 +3748,7 @@ staticUtility("snap-proximity", [["--baro-scroll-snap-strictness", "proximity"]]
3709
3748
  ].forEach(([name, prop]) => {
3710
3749
  functionalUtility({
3711
3750
  name: `scroll-${name}`,
3751
+ spacingKeys: true,
3712
3752
  prop,
3713
3753
  supportsArbitrary: true,
3714
3754
  supportsCustomProperty: true,
@@ -5242,6 +5282,7 @@ staticUtility("sticky", [["position", "sticky"]], { category: "layout" });
5242
5282
  staticUtility(`-${name}-px`, [[prop, "-1px"]], { category: "layout" });
5243
5283
  functionalUtility({
5244
5284
  name,
5285
+ spacingKeys: true,
5245
5286
  prop,
5246
5287
  supportsNegative: true,
5247
5288
  supportsFraction: true,
@@ -5272,6 +5313,7 @@ staticUtility("invisible", [["visibility", "hidden"]], { category: "layout" });
5272
5313
  staticUtility("collapse", [["visibility", "collapse"]], { category: "layout" });
5273
5314
  functionalUtility({
5274
5315
  name: "gap-x",
5316
+ spacingKeys: true,
5275
5317
  prop: "column-gap",
5276
5318
  supportsArbitrary: true,
5277
5319
  // gap-x-[10vw]
@@ -5288,6 +5330,7 @@ functionalUtility({
5288
5330
  });
5289
5331
  functionalUtility({
5290
5332
  name: "gap-y",
5333
+ spacingKeys: true,
5291
5334
  prop: "row-gap",
5292
5335
  supportsArbitrary: true,
5293
5336
  // gap-y-[10vw]
@@ -5304,6 +5347,7 @@ functionalUtility({
5304
5347
  });
5305
5348
  functionalUtility({
5306
5349
  name: "gap",
5350
+ spacingKeys: true,
5307
5351
  prop: "gap",
5308
5352
  supportsArbitrary: true,
5309
5353
  // gap-[10vw]
@@ -5821,6 +5865,7 @@ functionalUtility({
5821
5865
  ].forEach(([name, prop]) => {
5822
5866
  staticUtility(`${name}-px`, [[prop, "1px"]], { category: "spacing" });
5823
5867
  functionalUtility({
5868
+ spacingKeys: true,
5824
5869
  name,
5825
5870
  prop,
5826
5871
  supportsArbitrary: true,
@@ -5845,6 +5890,7 @@ functionalUtility({
5845
5890
  staticUtility(`${name}-px`, [[prop, "1px"]], { category: "spacing" });
5846
5891
  staticUtility(`-${name}-px`, [[prop, "-1px"]], { category: "spacing" });
5847
5892
  functionalUtility({
5893
+ spacingKeys: true,
5848
5894
  name,
5849
5895
  prop,
5850
5896
  supportsNegative: true,
@@ -5875,6 +5921,7 @@ const SPACE_SELECTOR = ":where(& > :not(:last-child))";
5875
5921
  () => rule(SPACE_SELECTOR, [decl(rev, "1")])
5876
5922
  ], { category: "spacing" });
5877
5923
  functionalUtility({
5924
+ spacingKeys: true,
5878
5925
  name,
5879
5926
  supportsNegative: true,
5880
5927
  supportsArbitrary: true,
@@ -5924,6 +5971,7 @@ const SPACE_SELECTOR = ":where(& > :not(:last-child))";
5924
5971
  staticUtility(name, [["width", value]]);
5925
5972
  });
5926
5973
  functionalUtility({
5974
+ spacingKeys: true,
5927
5975
  name: "w",
5928
5976
  prop: "width",
5929
5977
  supportsArbitrary: true,
@@ -5956,6 +6004,7 @@ functionalUtility({
5956
6004
  staticUtility(name, [["width", w], ["height", h]]);
5957
6005
  });
5958
6006
  functionalUtility({
6007
+ spacingKeys: true,
5959
6008
  name: "size",
5960
6009
  supportsArbitrary: true,
5961
6010
  supportsCustomProperty: true,
@@ -5995,6 +6044,7 @@ functionalUtility({
5995
6044
  staticUtility(name, [["height", value]]);
5996
6045
  });
5997
6046
  functionalUtility({
6047
+ spacingKeys: true,
5998
6048
  name: "h",
5999
6049
  prop: "height",
6000
6050
  supportsArbitrary: true,
@@ -6025,6 +6075,7 @@ functionalUtility({
6025
6075
  staticUtility(name, [["min-height", value]], { category: "sizing" });
6026
6076
  });
6027
6077
  functionalUtility({
6078
+ spacingKeys: true,
6028
6079
  name: "min-h",
6029
6080
  prop: "min-height",
6030
6081
  supportsArbitrary: true,
@@ -6055,6 +6106,7 @@ functionalUtility({
6055
6106
  staticUtility(name, [["max-height", value]], { category: "sizing" });
6056
6107
  });
6057
6108
  functionalUtility({
6109
+ spacingKeys: true,
6058
6110
  name: "max-h",
6059
6111
  prop: "max-height",
6060
6112
  supportsArbitrary: true,
@@ -6101,6 +6153,7 @@ functionalUtility({
6101
6153
  staticUtility(name, [["min-width", value]], { category: "sizing" });
6102
6154
  });
6103
6155
  functionalUtility({
6156
+ spacingKeys: true,
6104
6157
  name: "min-w",
6105
6158
  prop: "min-width",
6106
6159
  supportsArbitrary: true,
@@ -6136,6 +6189,7 @@ functionalUtility({
6136
6189
  staticUtility(name, [["max-width", value]], { category: "sizing" });
6137
6190
  });
6138
6191
  functionalUtility({
6192
+ spacingKeys: true,
6139
6193
  name: "max-w",
6140
6194
  prop: "max-width",
6141
6195
  supportsArbitrary: true,
@@ -8795,6 +8849,41 @@ class StylePartitionManager {
8795
8849
  }
8796
8850
  return { success, failed };
8797
8851
  }
8852
+ /**
8853
+ * Remove one generated rule (#269 GC). Keeps `styles`, the #254 `keys` and the
8854
+ * sheet's cssRules parallel: one deleteRule at the rule's index, or a text
8855
+ * rebuild when the sheet isn't solely ours / has no CSSOM. Returns whether
8856
+ * the rule was found.
8857
+ */
8858
+ removeRule(rule2, category) {
8859
+ let partition;
8860
+ if (category) {
8861
+ if (this.classToCategoryPartitionMap.get(rule2) !== category) return false;
8862
+ partition = this.categoryPartitions.get(category);
8863
+ } else {
8864
+ const partitionIndex = this.classToPartitionMap.get(rule2);
8865
+ partition = partitionIndex === void 0 ? void 0 : this.partitions[partitionIndex];
8866
+ }
8867
+ if (!partition) return false;
8868
+ const index = partition.styles.indexOf(rule2);
8869
+ if (index === -1) return false;
8870
+ const sheet = partition.styleElement.sheet;
8871
+ const inSync = !!sheet && sheet.cssRules.length === partition.styles.length;
8872
+ partition.styles.splice(index, 1);
8873
+ partition.keys?.splice(index, 1);
8874
+ if (inSync && sheet) {
8875
+ sheet.deleteRule(index);
8876
+ } else {
8877
+ partition.styleElement.textContent = partition.styles.length ? partition.styles.join("\n") + "\n" : "";
8878
+ }
8879
+ if (category) this.classToCategoryPartitionMap.delete(rule2);
8880
+ else this.classToPartitionMap.delete(rule2);
8881
+ return true;
8882
+ }
8883
+ /** Number of generated (non-root, non-preflight) rules currently held. */
8884
+ get ruleCount() {
8885
+ return this.classToPartitionMap.size + this.classToCategoryPartitionMap.size;
8886
+ }
8798
8887
  /**
8799
8888
  * 특정 규칙이 어느 파티션에 있는지 찾기
8800
8889
  */
@@ -8860,10 +8949,14 @@ class ChangeDetector {
8860
8949
  */
8861
8950
  constructor(incrementalParser, BrowserRuntime2, getCategory = (cls) => parseClassName(cls).utility?.category) {
8862
8951
  this.observer = null;
8952
+ this.gc = null;
8863
8953
  this.incrementalParser = incrementalParser;
8864
8954
  this.BrowserRuntime = BrowserRuntime2;
8865
8955
  this.getCategory = getCategory;
8866
8956
  }
8957
+ setGc(gc) {
8958
+ this.gc = gc;
8959
+ }
8867
8960
  setParser(parser) {
8868
8961
  this.incrementalParser = parser;
8869
8962
  }
@@ -8890,9 +8983,19 @@ class ChangeDetector {
8890
8983
  if (this.observer) {
8891
8984
  this.observer.disconnect();
8892
8985
  }
8986
+ this.gc?.setRoot(root);
8893
8987
  this.observer = new MutationObserver((mutations) => {
8894
8988
  const newClasses = /* @__PURE__ */ new Set();
8989
+ const gc = this.gc;
8895
8990
  mutations.forEach((mutation) => {
8991
+ if (gc) {
8992
+ if (mutation.type === "attributes") {
8993
+ gc.reconcile(mutation.target);
8994
+ } else if (mutation.type === "childList") {
8995
+ mutation.removedNodes.forEach((node) => gc.reconcileTree(node));
8996
+ mutation.addedNodes.forEach((node) => gc.reconcileTree(node));
8997
+ }
8998
+ }
8896
8999
  if (mutation.type === "attributes" && mutation.attributeName === "class" && root.contains(mutation.target)) {
8897
9000
  const target = mutation.target;
8898
9001
  if (target.className) {
@@ -8923,6 +9026,7 @@ class ChangeDetector {
8923
9026
  } else {
8924
9027
  this.BrowserRuntime?.applyParseResults([]);
8925
9028
  }
9029
+ gc?.afterBatch();
8926
9030
  });
8927
9031
  this.observer.observe(root, {
8928
9032
  attributes: true,
@@ -9041,6 +9145,115 @@ function collectLeadingClasses(rules, out = /* @__PURE__ */ new Set()) {
9041
9145
  }
9042
9146
  return out;
9043
9147
  }
9148
+ class ClassGc {
9149
+ constructor(host, graceMs, maxRules, now = () => Date.now()) {
9150
+ this.host = host;
9151
+ this.graceMs = graceMs;
9152
+ this.maxRules = maxRules;
9153
+ this.now = now;
9154
+ this.counts = /* @__PURE__ */ new Map();
9155
+ this.counted = /* @__PURE__ */ new WeakMap();
9156
+ this.candidates = /* @__PURE__ */ new Map();
9157
+ this.timer = null;
9158
+ this.root = null;
9159
+ }
9160
+ /** Start counting for a new root: count every element currently inside it. */
9161
+ setRoot(root) {
9162
+ this.counts.clear();
9163
+ this.counted = /* @__PURE__ */ new WeakMap();
9164
+ this.candidates.clear();
9165
+ this.cancel();
9166
+ this.root = root;
9167
+ this.reconcileTree(root);
9168
+ }
9169
+ count(cls) {
9170
+ return this.counts.get(cls) ?? 0;
9171
+ }
9172
+ /** Re-count `el` and (optionally) all its descendants from their current state. */
9173
+ reconcileTree(node) {
9174
+ if (node.nodeType !== 1) return;
9175
+ const el = node;
9176
+ this.reconcile(el);
9177
+ el.querySelectorAll("[class]").forEach((child) => this.reconcile(child));
9178
+ }
9179
+ reconcile(el) {
9180
+ const root = this.root;
9181
+ const live = !!root && root.contains(el);
9182
+ const next = live ? Array.from(new Set(normalizeClassNameList(el.getAttribute("class")))) : [];
9183
+ const prev = this.counted.get(el);
9184
+ if (!prev && next.length === 0) return;
9185
+ const prevSet = new Set(prev ?? []);
9186
+ const nextSet = new Set(next);
9187
+ for (const cls of nextSet) {
9188
+ if (prevSet.has(cls)) continue;
9189
+ const c = (this.counts.get(cls) ?? 0) + 1;
9190
+ this.counts.set(cls, c);
9191
+ this.candidates.delete(cls);
9192
+ }
9193
+ for (const cls of prevSet) {
9194
+ if (nextSet.has(cls)) continue;
9195
+ const c = (this.counts.get(cls) ?? 0) - 1;
9196
+ if (c > 0) {
9197
+ this.counts.set(cls, c);
9198
+ } else {
9199
+ this.counts.delete(cls);
9200
+ this.candidates.delete(cls);
9201
+ this.candidates.set(cls, this.now());
9202
+ }
9203
+ }
9204
+ if (next.length) this.counted.set(el, next);
9205
+ else this.counted.delete(el);
9206
+ }
9207
+ /** Call after a mutation batch has been counted and its classes inserted. */
9208
+ afterBatch() {
9209
+ if (this.candidates.size === 0) return;
9210
+ if (this.host.cachedCount() > this.maxRules) {
9211
+ this.schedule(0);
9212
+ } else {
9213
+ this.schedule(this.graceMs);
9214
+ }
9215
+ }
9216
+ schedule(delay) {
9217
+ if (this.timer !== null) {
9218
+ if (delay > 0) return;
9219
+ clearTimeout(this.timer);
9220
+ }
9221
+ this.timer = setTimeout(() => {
9222
+ this.timer = null;
9223
+ this.sweep();
9224
+ }, delay);
9225
+ }
9226
+ /** Reclaim candidates whose grace period elapsed (plus LRU overflow). Public for tests. */
9227
+ sweep() {
9228
+ const now = this.now();
9229
+ const overflow = Math.max(0, this.host.cachedCount() - this.maxRules);
9230
+ const doomed = [];
9231
+ let evicted = 0;
9232
+ for (const [cls, since] of this.candidates) {
9233
+ const expired = now - since >= this.graceMs;
9234
+ if (!expired && evicted >= overflow) continue;
9235
+ this.candidates.delete(cls);
9236
+ if (this.count(cls) > 0 || this.inDom(cls) || this.host.isPermanent(cls)) continue;
9237
+ doomed.push(cls);
9238
+ if (!expired) evicted++;
9239
+ }
9240
+ if (doomed.length) this.host.reclaim(doomed);
9241
+ if (this.candidates.size) this.schedule(this.graceMs);
9242
+ }
9243
+ inDom(cls) {
9244
+ const root = this.root;
9245
+ if (!root) return false;
9246
+ const doc = root.ownerDocument ?? document;
9247
+ return root.classList.contains(cls) || doc.documentElement.classList.contains(cls) || doc.getElementsByClassName(cls).length > 0;
9248
+ }
9249
+ cancel() {
9250
+ if (this.timer !== null) clearTimeout(this.timer);
9251
+ this.timer = null;
9252
+ }
9253
+ stats() {
9254
+ return { trackedClasses: this.counts.size, candidates: this.candidates.size };
9255
+ }
9256
+ }
9044
9257
  const LAYER_ORDER = "@layer theme, base, components, utilities;";
9045
9258
  class BrowserRuntime {
9046
9259
  constructor(options = {}) {
@@ -9049,6 +9262,9 @@ class BrowserRuntime {
9049
9262
  this.isDestroyed = false;
9050
9263
  this.existing = null;
9051
9264
  this.existingSheetCount = -1;
9265
+ this.pinned = /* @__PURE__ */ new Set();
9266
+ this.gc = null;
9267
+ this.reclaimedCount = 0;
9052
9268
  this.getCategory = (cls) => parseClassName(cls, this.context).utility?.category;
9053
9269
  const defaultConfig = {};
9054
9270
  this.options = {
@@ -9056,12 +9272,23 @@ class BrowserRuntime {
9056
9272
  styleId: options.styleId || "barocss-runtime",
9057
9273
  insertionPoint: options.insertionPoint || "head",
9058
9274
  maxRulesPerPartition: options.maxRulesPerPartition || 50,
9059
- skipExisting: options.skipExisting ?? false
9275
+ skipExisting: options.skipExisting ?? false,
9276
+ gc: options.gc ?? true,
9277
+ gcGraceMs: options.gcGraceMs ?? 3e3,
9278
+ maxRules: options.maxRules ?? Infinity
9060
9279
  };
9061
9280
  this.context = createContext(this.options.config);
9062
9281
  this.incrementalParser = new IncrementalParser(this.context);
9063
9282
  this.changeDetector = new ChangeDetector(this.incrementalParser, this, this.getCategory);
9064
9283
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9284
+ if (this.options.gc) {
9285
+ this.gc = new ClassGc({
9286
+ reclaim: (classes) => this.reclaim(classes),
9287
+ isPermanent: (cls) => this.isPermanent(cls),
9288
+ cachedCount: () => this.cache.size
9289
+ }, this.options.gcGraceMs, this.options.maxRules);
9290
+ this.changeDetector.setGc(this.gc);
9291
+ }
9065
9292
  this.init();
9066
9293
  }
9067
9294
  // Debugging and logging helpers
@@ -9113,7 +9340,8 @@ ${preflightCSS}
9113
9340
  */
9114
9341
  addClass(classes) {
9115
9342
  if (this.isDestroyed) return;
9116
- const classList = this.normalizeClasses(classes);
9343
+ const classList = this.normalizeClasses(classes).filter(Boolean);
9344
+ classList.forEach((cls) => this.pinned.add(cls));
9117
9345
  this.processClasses(classList);
9118
9346
  }
9119
9347
  /**
@@ -9174,9 +9402,40 @@ ${preflightCSS}
9174
9402
  rootCssCount: rootCssRules.length
9175
9403
  });
9176
9404
  }
9405
+ /** #269: a class that must never be reclaimed. */
9406
+ isPermanent(cls) {
9407
+ if (this.pinned.has(cls)) return true;
9408
+ if (typeof document === "undefined") return true;
9409
+ return this.getExistingClasses().has(cls);
9410
+ }
9411
+ /**
9412
+ * #269: delete the generated rules of classes no live element uses. Root/@property rules stay
9413
+ * (they are shared and harmless); a rule text another cached class still emits is kept.
9414
+ */
9415
+ reclaim(classes) {
9416
+ if (this.isDestroyed) return;
9417
+ const victims = classes.filter((cls) => this.cache.has(cls));
9418
+ if (victims.length === 0) return;
9419
+ const results = victims.map((cls) => this.cache.get(cls));
9420
+ victims.forEach((cls) => {
9421
+ this.cache.delete(cls);
9422
+ this.incrementalParser.unmarkProcessed(cls);
9423
+ });
9424
+ const stillUsed = /* @__PURE__ */ new Set();
9425
+ for (const result of this.cache.values()) result.cssList.forEach((css) => stillUsed.add(css));
9426
+ for (const result of results) {
9427
+ const category = this.getCategory(result.cls);
9428
+ for (const css of result.cssList) {
9429
+ if (!stillUsed.has(css)) this.stylePartitionManager.removeRule(css, category);
9430
+ }
9431
+ }
9432
+ this.reclaimedCount += victims.length;
9433
+ }
9177
9434
  /** Class names defined by the page's own stylesheets (BaroCSS's sheets and cross-origin sheets excluded). */
9178
9435
  getExistingClasses() {
9436
+ const own = new Set(Array.from(document.querySelectorAll("style[data-barocss]"), (s) => s.sheet));
9179
9437
  const sheets = Array.from(document.styleSheets).filter((sheet) => {
9438
+ if (own.has(sheet)) return false;
9180
9439
  const owner = sheet.ownerNode;
9181
9440
  return !(owner && typeof owner.hasAttribute === "function" && (owner.hasAttribute("data-barocss") || (owner.id || "").startsWith(this.options.styleId)));
9182
9441
  });
@@ -9228,7 +9487,10 @@ ${preflightCSS}
9228
9487
  return {
9229
9488
  runtime: {
9230
9489
  cachedClasses: this.cache.size,
9231
- rootCacheSize: this.rootCache.size
9490
+ rootCacheSize: this.rootCache.size,
9491
+ ruleCount: this.stylePartitionManager.ruleCount,
9492
+ reclaimedClasses: this.reclaimedCount,
9493
+ gc: this.gc?.stats() ?? null
9232
9494
  },
9233
9495
  ast: incremental.cacheStats.ast,
9234
9496
  incremental
@@ -9282,6 +9544,7 @@ ${preflightCSS}
9282
9544
  destroy() {
9283
9545
  if (this.isDestroyed) return;
9284
9546
  this.changeDetector.disconnect();
9547
+ this.gc?.cancel();
9285
9548
  this.stylePartitionManager.cleanup();
9286
9549
  this.cache.clear();
9287
9550
  this.rootCache.clear();