@tenphi/tasty 3.4.0 → 3.5.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.
Files changed (83) hide show
  1. package/README.md +147 -129
  2. package/dist/astro-ib7E7V4Y.js +307 -0
  3. package/dist/astro-ib7E7V4Y.js.map +1 -0
  4. package/dist/{babel-BUQGeOXA.d.ts → babel-DWCsafYw.d.ts} +2 -2
  5. package/dist/{collector-DUaHCcTS.d.ts → collector-BTVXj2hS.d.ts} +21 -3
  6. package/dist/{collector-DTahQUiV.js → collector-C6TtL8HJ.js} +36 -13
  7. package/dist/collector-C6TtL8HJ.js.map +1 -0
  8. package/dist/{config-B5kHzuNz.js → config-B3gPdCqd.js} +305 -153
  9. package/dist/config-B3gPdCqd.js.map +1 -0
  10. package/dist/{config-YsxGv4tq.d.ts → config-LfIDmVHx.d.ts} +132 -32
  11. package/dist/core/index.d.ts +5 -5
  12. package/dist/core/index.js +6 -6
  13. package/dist/{core-Dr4u1NVD.js → core-Bq7w2kti.js} +215 -95
  14. package/dist/core-Bq7w2kti.js.map +1 -0
  15. package/dist/{css-writer-B-J87ncv.js → css-writer-Bgowobbx.js} +3 -3
  16. package/dist/{css-writer-B-J87ncv.js.map → css-writer-Bgowobbx.js.map} +1 -1
  17. package/dist/{format-rules-DKOA-6qu.js → format-rules-rCZ37rqY.js} +2 -2
  18. package/dist/format-rules-rCZ37rqY.js.map +1 -0
  19. package/dist/{hydrate-OeMX99We.js → hydrate-CNOmZprz.js} +3 -3
  20. package/dist/{hydrate-OeMX99We.js.map → hydrate-CNOmZprz.js.map} +1 -1
  21. package/dist/{index-PqN-DIpn.d.ts → index-Bm9ltvdH.d.ts} +38 -16
  22. package/dist/{index-Cd45t5NM.d.ts → index-BmogEzTo.d.ts} +69 -15
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +7 -7
  25. package/dist/index.js.map +1 -1
  26. package/dist/{keyframes-CV8azJf3.js → keyframes-DE-OE76F.js} +45 -3
  27. package/dist/keyframes-DE-OE76F.js.map +1 -0
  28. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BcLl1u_d.d.ts} +2 -2
  29. package/dist/{merge-styles-oklji0KB.js → merge-styles-DuoZEsm9.js} +2 -2
  30. package/dist/{merge-styles-oklji0KB.js.map → merge-styles-DuoZEsm9.js.map} +1 -1
  31. package/dist/{resolve-recipes-DTG81rzl.js → resolve-recipes-H9NqOQuP.js} +3 -3
  32. package/dist/{resolve-recipes-DTG81rzl.js.map → resolve-recipes-H9NqOQuP.js.map} +1 -1
  33. package/dist/ssr/astro-client.js +1 -1
  34. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  35. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  36. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  37. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  38. package/dist/ssr/astro-middleware-extract.js +9 -0
  39. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  40. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  41. package/dist/ssr/astro-middleware-static.js +1 -1
  42. package/dist/ssr/astro-middleware.d.ts +3 -1
  43. package/dist/ssr/astro-middleware.js +1 -1
  44. package/dist/ssr/astro.d.ts +37 -3
  45. package/dist/ssr/astro.js +1 -156
  46. package/dist/ssr/index.d.ts +2 -2
  47. package/dist/ssr/index.js +3 -3
  48. package/dist/ssr/index.js.map +1 -1
  49. package/dist/ssr/next.d.ts +1 -1
  50. package/dist/ssr/next.js +4 -4
  51. package/dist/static/index.d.ts +2 -2
  52. package/dist/static/index.js +1 -1
  53. package/dist/zero/babel.d.ts +1 -1
  54. package/dist/zero/babel.js +4 -4
  55. package/dist/zero/babel.js.map +1 -1
  56. package/dist/zero/index.d.ts +1 -1
  57. package/dist/zero/index.js +1 -1
  58. package/dist/zero/next.d.ts +1 -1
  59. package/docs/README.md +13 -8
  60. package/docs/adoption.md +56 -31
  61. package/docs/ai-agents.md +78 -77
  62. package/docs/comparison.md +69 -40
  63. package/docs/configuration.md +97 -87
  64. package/docs/debug.md +4 -2
  65. package/docs/design-system.md +41 -22
  66. package/docs/dsl.md +225 -195
  67. package/docs/getting-started.md +60 -39
  68. package/docs/injector.md +63 -26
  69. package/docs/methodology.md +42 -36
  70. package/docs/migration-v3.md +49 -49
  71. package/docs/plugins.md +37 -33
  72. package/docs/react-api.md +65 -44
  73. package/docs/runtime-benchmarks.md +178 -0
  74. package/docs/ssr.md +99 -61
  75. package/docs/styles.md +1 -1
  76. package/docs/tasty-static.md +137 -104
  77. package/package.json +26 -8
  78. package/dist/collector-DTahQUiV.js.map +0 -1
  79. package/dist/config-B5kHzuNz.js.map +0 -1
  80. package/dist/core-Dr4u1NVD.js.map +0 -1
  81. package/dist/format-rules-DKOA-6qu.js.map +0 -1
  82. package/dist/keyframes-CV8azJf3.js.map +0 -1
  83. package/dist/ssr/astro.js.map +0 -1
@@ -3186,7 +3186,14 @@ function resetStyleBatch() {
3186
3186
  * Automatically infers CSS @property types from custom property values.
3187
3187
  * Supports deferred resolution for var() reference chains of arbitrary depth.
3188
3188
  */
3189
- const CUSTOM_PROP_DECL = /^\s*(--[a-z0-9_-]+)\s*:\s*(.+?)\s*$/i;
3189
+ /**
3190
+ * One `--name: value` declaration, with the surrounding whitespace dropped.
3191
+ *
3192
+ * The value is anchored on `\S` at both ends so no two quantifiers can claim
3193
+ * the same character. A lazy `(.+?)\s*$` is quadratic on a value of nothing
3194
+ * but spaces — every split point gets tried (CodeQL `js/polynomial-redos`).
3195
+ */
3196
+ const CUSTOM_PROP_DECL = /^\s*(--[a-z0-9_-]+)\s*:\s*(\S|\S.*\S)\s*$/i;
3190
3197
  const SINGLE_VAR_REF = /^var\((--[a-z0-9_-]+)\)$/i;
3191
3198
  var PropertyTypeResolver = class {
3192
3199
  /** propName → the prop it depends on */
@@ -5040,7 +5047,7 @@ var SheetManager = class {
5040
5047
  } : void 0;
5041
5048
  registry = {
5042
5049
  sheets: [],
5043
- refCounts: /* @__PURE__ */ new Map(),
5050
+ pinCounts: /* @__PURE__ */ new Map(),
5044
5051
  rules: /* @__PURE__ */ new Map(),
5045
5052
  cacheKeyToClassName: /* @__PURE__ */ new Map(),
5046
5053
  ruleTextSet: /* @__PURE__ */ new Set(),
@@ -5054,10 +5061,9 @@ var SheetManager = class {
5054
5061
  injectedFunctions: /* @__PURE__ */ new Map(),
5055
5062
  globalRules: /* @__PURE__ */ new Map(),
5056
5063
  propertyTypeResolver: new PropertyTypeResolver(),
5057
- usageMap: /* @__PURE__ */ new Map(),
5064
+ unusedSince: /* @__PURE__ */ new Map(),
5065
+ localKeyframes: /* @__PURE__ */ new Map(),
5058
5066
  touchCount: 0,
5059
- touchTick: -1,
5060
- touchedTick: /* @__PURE__ */ new Set(),
5061
5067
  serverClassSyncIndex: 0,
5062
5068
  rscStylesScanned: false,
5063
5069
  injectionMode: this.detectInjectionMode(root)
@@ -5439,25 +5445,26 @@ var SheetManager = class {
5439
5445
  return registry.atPropertySupported;
5440
5446
  }
5441
5447
  /**
5442
- * Force cleanup of unused styles
5443
- */
5444
- forceCleanup(registry) {
5445
- this.performBulkCleanup(registry);
5446
- }
5447
- /**
5448
- * Perform bulk cleanup of all unused styles (refCount = 0).
5448
+ * Delete the given classes: their rules leave the sheets and every registry
5449
+ * entry pointing at them is dropped.
5450
+ *
5451
+ * Deciding *what* is unused belongs to `StyleInjector.gc()`, which owns the
5452
+ * DOM scan and the capacity policy. This only re-checks that each class is
5453
+ * still safe to delete, and reports how many were.
5454
+ *
5455
+ * @returns Number of classes actually deleted.
5449
5456
  */
5450
- performBulkCleanup(registry) {
5457
+ deleteClasses(registry, classNames) {
5451
5458
  const cleanupStartTime = Date.now();
5452
- const unusedClassNames = Array.from(registry.refCounts.entries()).filter(([className, refCount]) => refCount === 0 && !registry.usageMap.has(className)).map(([className]) => className);
5453
- if (unusedClassNames.length === 0) return;
5454
- const selected = unusedClassNames.map((className) => {
5459
+ const selected = Array.from(classNames).map((className) => {
5455
5460
  const ruleInfo = registry.rules.get(className);
5456
5461
  return ruleInfo ? {
5457
5462
  className,
5458
5463
  ruleInfo
5459
5464
  } : null;
5460
5465
  }).filter((entry) => entry != null);
5466
+ if (selected.length === 0) return 0;
5467
+ const deleted = /* @__PURE__ */ new Set();
5461
5468
  let cleanedUpCount = 0;
5462
5469
  let totalCssSize = 0;
5463
5470
  let totalRulesDeleted = 0;
@@ -5478,7 +5485,7 @@ var SheetManager = class {
5478
5485
  for (const [_sheetIndex, rulesInSheet] of rulesBySheet) {
5479
5486
  rulesInSheet.sort((a, b) => b.ruleInfo.ruleIndex - a.ruleInfo.ruleIndex);
5480
5487
  for (const { className, ruleInfo } of rulesInSheet) {
5481
- if ((registry.refCounts.get(className) || 0) > 0) continue;
5488
+ if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
5482
5489
  if (registry.rules.get(className) !== ruleInfo) continue;
5483
5490
  const sheetInfo = registry.sheets[ruleInfo.sheetIndex];
5484
5491
  if (!sheetInfo || !sheetInfo.sheet && !sheetInfo.constructableSheet) continue;
@@ -5490,13 +5497,22 @@ var SheetManager = class {
5490
5497
  if (startIdx < 0 || endIdx > maxRuleIndex || startIdx > endIdx) continue;
5491
5498
  this.deleteRule(registry, ruleInfo);
5492
5499
  registry.rules.delete(className);
5493
- registry.refCounts.delete(className);
5494
- const keysToDelete = [];
5495
- for (const [key, mappedClassName] of registry.cacheKeyToClassName.entries()) if (mappedClassName === className) keysToDelete.push(key);
5496
- for (const key of keysToDelete) registry.cacheKeyToClassName.delete(key);
5500
+ registry.pinCounts.delete(className);
5501
+ registry.unusedSince.delete(className);
5502
+ for (const [key, entry] of registry.localKeyframes) {
5503
+ if (!entry.owners.delete(className)) continue;
5504
+ if (entry.owners.size === 0) {
5505
+ entry.dispose();
5506
+ registry.localKeyframes.delete(key);
5507
+ }
5508
+ }
5509
+ deleted.add(className);
5497
5510
  cleanedUpCount++;
5498
5511
  }
5499
5512
  }
5513
+ if (deleted.size > 0) {
5514
+ for (const [key, mappedClassName] of registry.cacheKeyToClassName) if (deleted.has(mappedClassName)) registry.cacheKeyToClassName.delete(key);
5515
+ }
5500
5516
  if (registry.metrics) {
5501
5517
  registry.metrics.bulkCleanups++;
5502
5518
  registry.metrics.stylesCleanedUp += cleanedUpCount;
@@ -5507,6 +5523,7 @@ var SheetManager = class {
5507
5523
  rulesDeleted: totalRulesDeleted
5508
5524
  });
5509
5525
  }
5526
+ return cleanedUpCount;
5510
5527
  }
5511
5528
  /**
5512
5529
  * Get total number of rules across all sheets
@@ -5515,25 +5532,31 @@ var SheetManager = class {
5515
5532
  return registry.sheets.reduce((total, sheet) => total + sheet.ruleCount - sheet.holes.length, 0);
5516
5533
  }
5517
5534
  /**
5518
- * Get CSS text from all sheets (for SSR)
5535
+ * The CSS of one managed sheet, or `null` when it holds none this can read —
5536
+ * an empty style element, or a sheet the engine refuses to hand over.
5519
5537
  */
5520
- getCSSText(registry) {
5521
- const cssChunks = [];
5522
- for (const sheetInfo of registry.sheets) try {
5523
- if (sheetInfo.constructableSheet) {
5524
- const rules = Array.from(sheetInfo.constructableSheet.cssRules);
5525
- cssChunks.push(rules.map((rule) => rule.cssText).join("\n"));
5526
- } else if (sheetInfo.sheet) {
5538
+ readSheetCSS(sheetInfo) {
5539
+ try {
5540
+ if (sheetInfo.constructableSheet) return Array.from(sheetInfo.constructableSheet.cssRules).map((rule) => rule.cssText).join("\n");
5541
+ if (sheetInfo.sheet) {
5527
5542
  const styleElement = sheetInfo.sheet;
5528
- if (styleElement.textContent) cssChunks.push(styleElement.textContent);
5529
- else if (styleElement.sheet) {
5530
- const rules = Array.from(styleElement.sheet.cssRules);
5531
- cssChunks.push(rules.map((rule) => rule.cssText).join("\n"));
5532
- }
5543
+ if (styleElement.textContent) return styleElement.textContent;
5544
+ if (styleElement.sheet) return Array.from(styleElement.sheet.cssRules).map((rule) => rule.cssText).join("\n");
5533
5545
  }
5534
5546
  } catch (error) {
5535
5547
  console.warn("[Tasty] Failed to read CSS from sheet:", error);
5536
5548
  }
5549
+ return null;
5550
+ }
5551
+ /**
5552
+ * Get CSS text from all sheets (for SSR)
5553
+ */
5554
+ getCSSText(registry) {
5555
+ const cssChunks = [];
5556
+ for (const sheetInfo of registry.sheets) {
5557
+ const css = this.readSheetCSS(sheetInfo);
5558
+ if (css !== null) cssChunks.push(css);
5559
+ }
5537
5560
  return cssChunks.join("\n");
5538
5561
  }
5539
5562
  /**
@@ -5541,10 +5564,9 @@ var SheetManager = class {
5541
5564
  */
5542
5565
  getMetrics(registry) {
5543
5566
  if (!registry.metrics) return null;
5544
- const unusedRulesCount = Array.from(registry.refCounts.values()).filter((count) => count === 0).length;
5545
5567
  return {
5546
5568
  ...registry.metrics,
5547
- unusedHits: unusedRulesCount
5569
+ unusedHits: 0
5548
5570
  };
5549
5571
  }
5550
5572
  /**
@@ -5846,6 +5868,49 @@ var SheetManager = class {
5846
5868
  /**
5847
5869
  * Get the raw CSS content
5848
5870
  */
5871
+ /**
5872
+ * The CSS this injector owns in `root`, one entry per sheet, in the order the
5873
+ * engine applies them.
5874
+ *
5875
+ * `getCSSText()` walks the managed sheets only, and raw CSS has its own —
5876
+ * which sits before, after, or *between* them depending on how it got there:
5877
+ * prepended to `adoptedStyleSheets`, or wherever in `<head>` the first raw
5878
+ * injection happened to land, with every managed sheet opened afterwards
5879
+ * following it. Reporting a fixed order would describe the opposite winner
5880
+ * from the live page whenever two rules of equal specificity meet, so the raw
5881
+ * sheet is spliced in at the position the DOM actually gives it.
5882
+ */
5883
+ getOwnedCSSInOrder(registry, root) {
5884
+ const raw = this.getRawCSSText(root);
5885
+ const rawElement = this.isAdoptedMode(root) ? null : this.rawStyleElements.get(root);
5886
+ let rawIndex = raw && !rawElement ? 0 : -1;
5887
+ const chunks = [];
5888
+ for (const sheetInfo of registry.sheets) {
5889
+ const css = this.readSheetCSS(sheetInfo);
5890
+ if (css === null) continue;
5891
+ if (rawIndex < 0 && rawElement && sheetInfo.sheet && rawElement.compareDocumentPosition(sheetInfo.sheet) & Node.DOCUMENT_POSITION_FOLLOWING) rawIndex = chunks.length;
5892
+ chunks.push(css);
5893
+ }
5894
+ if (!raw) return chunks;
5895
+ chunks.splice(rawIndex < 0 ? chunks.length : rawIndex, 0, raw);
5896
+ return chunks;
5897
+ }
5898
+ /**
5899
+ * Top-level rules in the raw sheet.
5900
+ *
5901
+ * Read from the sheet the engine parsed, not from the text: a raw block is
5902
+ * one string but any number of rules, and one rule — `@keyframes`, `@media` —
5903
+ * can contain any number of blocks, so counting braces answers neither
5904
+ * question.
5905
+ */
5906
+ getRawRuleCount(root) {
5907
+ const sheet = this.isAdoptedMode(root) ? this.rawConstructableSheets.get(root) : this.rawStyleElements.get(root)?.sheet;
5908
+ try {
5909
+ return sheet?.cssRules.length ?? 0;
5910
+ } catch {
5911
+ return 0;
5912
+ }
5913
+ }
5849
5914
  getRawCSSText(root) {
5850
5915
  if (this.isAdoptedMode(root)) {
5851
5916
  const blocksMap = this.rawCSSBlocks.get(root);
@@ -5968,6 +6033,13 @@ function formatCounterStyleRule(name, descriptors) {
5968
6033
  //#endregion
5969
6034
  //#region src/injector/injector.ts
5970
6035
  /**
6036
+ * How long a class is left alone after it was last known to be wanted.
6037
+ * Long enough that a render cannot plausibly still be waiting to commit one.
6038
+ */
6039
+ const DEFAULT_GC_GRACE = 1e4;
6040
+ /** Dispose handle handed back for injections that took no pin. */
6041
+ const noop = () => {};
6042
+ /**
5971
6043
  * Placeholder `KeyframesInfo` for a cache entry reserved by a batched write and
5972
6044
  * not yet backed by a real rule. Only ever read through `entry.pending`, which
5973
6045
  * is checked before the info is used.
@@ -6024,13 +6096,14 @@ function registerHydratedClass(registry, className) {
6024
6096
  ruleIndex: -2,
6025
6097
  sheetIndex: -2
6026
6098
  });
6027
- registry.refCounts.set(className, 0);
6099
+ registry.pinCounts.set(className, 0);
6028
6100
  }
6029
6101
  var StyleInjector = class {
6030
6102
  sheetManager;
6031
6103
  config;
6032
6104
  globalRuleCounter = 0;
6033
- pendingGCHandle = null;
6105
+ /** Cancels the scheduled sweep, whichever timer scheduled it. */
6106
+ cancelPendingGC = null;
6034
6107
  namePrefix;
6035
6108
  classRegex;
6036
6109
  rscClassRegex;
@@ -6159,6 +6232,7 @@ var StyleInjector = class {
6159
6232
  inject(rules, options) {
6160
6233
  const root = options?.root || document;
6161
6234
  const registry = this.sheetManager.getRegistry(root);
6235
+ const pin = options?.pin ?? true;
6162
6236
  if (rules.length === 0) return {
6163
6237
  className: "",
6164
6238
  dispose: () => {}
@@ -6169,30 +6243,34 @@ var StyleInjector = class {
6169
6243
  className = registry.cacheKeyToClassName.get(cacheKey);
6170
6244
  const existingRuleInfo = registry.rules.get(className);
6171
6245
  if (existingRuleInfo.ruleIndex === -3) {
6172
- registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
6246
+ registry.unusedSince.delete(className);
6247
+ if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
6173
6248
  if (registry.metrics) registry.metrics.hits++;
6174
6249
  return {
6175
6250
  className,
6176
- dispose: () => this.dispose(className, registry)
6251
+ dispose: pin ? () => this.unpin(className, registry) : noop
6177
6252
  };
6178
6253
  }
6179
6254
  if (!(existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1)) {
6180
- const currentRefCount = registry.refCounts.get(className) || 0;
6181
- registry.refCounts.set(className, currentRefCount + 1);
6255
+ registry.unusedSince.delete(className);
6256
+ if (pin) {
6257
+ const pins = registry.pinCounts.get(className) || 0;
6258
+ registry.pinCounts.set(className, pins + 1);
6259
+ }
6182
6260
  if (registry.metrics) registry.metrics.hits++;
6183
6261
  return {
6184
6262
  className,
6185
- dispose: () => this.dispose(className, registry)
6263
+ dispose: pin ? () => this.unpin(className, registry) : noop
6186
6264
  };
6187
6265
  }
6188
6266
  } else if (cacheKey) {
6189
6267
  className = this.generateClassName(cacheKey);
6190
6268
  if (this.tryHydratedHit(registry, cacheKey, className)) {
6191
- registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
6269
+ if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
6192
6270
  if (registry.metrics) registry.metrics.hits++;
6193
6271
  return {
6194
6272
  className,
6195
- dispose: () => this.dispose(className, registry)
6273
+ dispose: pin ? () => this.unpin(className, registry) : noop
6196
6274
  };
6197
6275
  }
6198
6276
  } else {
@@ -6253,7 +6331,8 @@ var StyleInjector = class {
6253
6331
  sheetIndex: -3
6254
6332
  });
6255
6333
  if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
6256
- registry.refCounts.set(className, 1);
6334
+ if (pin) registry.pinCounts.set(className, 1);
6335
+ registry.unusedSince.set(className, Date.now());
6257
6336
  const queued = enqueueStyleWrite(() => {
6258
6337
  if (applySheetWrite()) return;
6259
6338
  registry.rules.set(className, {
@@ -6264,26 +6343,27 @@ var StyleInjector = class {
6264
6343
  });
6265
6344
  return {
6266
6345
  className,
6267
- dispose: () => {
6268
- if (!queued.done && registry.refCounts.get(className) === 1 && registry.rules.get(className)?.ruleIndex === -3) {
6346
+ dispose: pin ? () => {
6347
+ if (!queued.done && registry.pinCounts.get(className) === 1 && registry.rules.get(className)?.ruleIndex === -3) {
6269
6348
  queued.cancelled = true;
6270
6349
  registry.rules.delete(className);
6271
6350
  if (cacheKey) registry.cacheKeyToClassName.delete(cacheKey);
6272
- registry.refCounts.set(className, 0);
6351
+ registry.pinCounts.set(className, 0);
6273
6352
  return;
6274
6353
  }
6275
- this.dispose(className, registry);
6276
- }
6354
+ this.unpin(className, registry);
6355
+ } : noop
6277
6356
  };
6278
6357
  }
6279
6358
  if (!applySheetWrite()) return {
6280
6359
  className,
6281
6360
  dispose: () => {}
6282
6361
  };
6283
- registry.refCounts.set(className, 1);
6362
+ if (pin) registry.pinCounts.set(className, 1);
6363
+ registry.unusedSince.set(className, Date.now());
6284
6364
  return {
6285
6365
  className,
6286
- dispose: () => this.dispose(className, registry)
6366
+ dispose: pin ? () => this.unpin(className, registry) : noop
6287
6367
  };
6288
6368
  }
6289
6369
  /**
@@ -6363,41 +6443,86 @@ var StyleInjector = class {
6363
6443
  return this.sheetManager.getRawCSSText(root);
6364
6444
  }
6365
6445
  /**
6366
- * Increment refCount for an already-injected cacheKey and return a dispose.
6367
- * Used by useStyles on cache hits (hydration or runtime reuse) where
6368
- * the pipeline was skipped but refCount tracking is still needed.
6369
- * Returns null if the cacheKey is not found.
6446
+ * Take a reference on local `@keyframes`, under the deterministic names the
6447
+ * caller resolved.
6448
+ *
6449
+ * One reference per distinct set of steps, shared by every class that ends up
6450
+ * animating it, so a repeat render takes nothing further. Ownership is
6451
+ * assigned by `ownKeyframes()` once the rules exist to be inspected.
6452
+ */
6453
+ holdKeyframes(steps, names, options) {
6454
+ const root = options?.root || document;
6455
+ const registry = this.sheetManager.getRegistry(root);
6456
+ const keys = /* @__PURE__ */ new Map();
6457
+ for (const [authored, definition] of Object.entries(steps)) {
6458
+ const resolved = names.get(authored) ?? authored;
6459
+ let entry = registry.localKeyframes.get(resolved);
6460
+ if (!entry) {
6461
+ const injected = this.keyframes(definition, {
6462
+ name: resolved,
6463
+ root,
6464
+ distinctByName: true
6465
+ });
6466
+ entry = {
6467
+ name: injected.toString(),
6468
+ dispose: injected.dispose,
6469
+ owners: /* @__PURE__ */ new Set()
6470
+ };
6471
+ registry.localKeyframes.set(resolved, entry);
6472
+ }
6473
+ keys.set(authored, resolved);
6474
+ }
6475
+ return keys;
6476
+ }
6477
+ /**
6478
+ * Record that `className` animates these keyframes, so the reference is
6479
+ * released when the last such class is collected.
6480
+ *
6481
+ * Only classes whose rules actually reference the animation should be here:
6482
+ * a class that merely rendered alongside one would otherwise keep the
6483
+ * keyframes alive for as long as it lives.
6484
+ */
6485
+ ownKeyframes(key, className, options) {
6486
+ this.sheetManager.getRegistry(options?.root || document).localKeyframes.get(key)?.owners.add(className);
6487
+ }
6488
+ /**
6489
+ * Pin an already-injected cacheKey and return the handle that releases it.
6490
+ * For callers that skipped the pipeline on a cache hit but still need the
6491
+ * class held. Returns null if the cacheKey is not found.
6370
6492
  */
6371
6493
  trackRef(cacheKey, options) {
6372
6494
  const root = options?.root || document;
6373
6495
  const registry = this.sheetManager.getRegistry(root);
6374
6496
  if (!registry.cacheKeyToClassName.has(cacheKey)) return null;
6375
6497
  const className = registry.cacheKeyToClassName.get(cacheKey);
6376
- const currentRefCount = registry.refCounts.get(className) || 0;
6377
- registry.refCounts.set(className, currentRefCount + 1);
6498
+ const pins = registry.pinCounts.get(className) || 0;
6499
+ registry.pinCounts.set(className, pins + 1);
6378
6500
  if (registry.metrics) registry.metrics.hits++;
6379
6501
  return {
6380
6502
  className,
6381
- dispose: () => this.dispose(className, registry)
6503
+ dispose: () => this.unpin(className, registry)
6382
6504
  };
6383
6505
  }
6384
6506
  /**
6385
- * Dispose of a className (decrements refCount only).
6507
+ * Release one pin on a className. At zero pins the class is not deleted — it
6508
+ * stays cached and collectible, and `gc()` decides when it actually goes.
6386
6509
  */
6387
- dispose(className, registry) {
6388
- const currentRefCount = registry.refCounts.get(className);
6389
- if (currentRefCount == null || currentRefCount <= 0) return;
6390
- const newRefCount = currentRefCount - 1;
6391
- registry.refCounts.set(className, newRefCount);
6392
- if (newRefCount === 0 && registry.metrics) registry.metrics.totalUnused++;
6510
+ unpin(className, registry) {
6511
+ const pins = registry.pinCounts.get(className);
6512
+ if (pins == null || pins <= 0) return;
6513
+ const remaining = pins - 1;
6514
+ registry.pinCounts.set(className, remaining);
6515
+ if (remaining === 0 && registry.metrics) registry.metrics.totalUnused++;
6393
6516
  }
6394
6517
  /**
6395
- * Force bulk cleanup of unused styles
6518
+ * Remove every style that is neither in the DOM nor referenced by an
6519
+ * outstanding `inject()` handle, ignoring the GC capacity threshold.
6396
6520
  */
6397
6521
  cleanup(root) {
6398
- flushStyles();
6399
- const registry = this.sheetManager.getRegistry(root || document);
6400
- this.sheetManager.forceCleanup(registry);
6522
+ this.gc({
6523
+ root,
6524
+ force: true
6525
+ });
6401
6526
  }
6402
6527
  /**
6403
6528
  * Get CSS text from all sheets (for SSR)
@@ -6440,7 +6565,9 @@ var StyleInjector = class {
6440
6565
  flushStyles();
6441
6566
  const root = options?.root || document;
6442
6567
  const registry = this.sheetManager.getRegistry(root);
6443
- return this.sheetManager.getMetrics(registry);
6568
+ const metrics = this.sheetManager.getMetrics(registry);
6569
+ if (metrics && typeof document !== "undefined") metrics.unusedHits = this.collectUnused(registry, root).length;
6570
+ return metrics;
6444
6571
  }
6445
6572
  /**
6446
6573
  * Reset cache performance metrics
@@ -6614,13 +6741,14 @@ var StyleInjector = class {
6614
6741
  keyframes(steps, nameOrOptions) {
6615
6742
  const isStringName = typeof nameOrOptions === "string";
6616
6743
  const providedName = isStringName ? nameOrOptions : nameOrOptions?.name;
6744
+ const distinctByName = isStringName ? false : nameOrOptions?.distinctByName ?? false;
6617
6745
  const root = isStringName ? document : nameOrOptions?.root || document;
6618
6746
  const registry = this.sheetManager.getRegistry(root);
6619
6747
  if (Object.keys(steps).length === 0) return {
6620
6748
  toString: () => "",
6621
6749
  dispose: () => {}
6622
6750
  };
6623
- const contentHash = JSON.stringify(steps);
6751
+ const contentHash = distinctByName && providedName ? `${providedName}\u0000${JSON.stringify(steps)}` : JSON.stringify(steps);
6624
6752
  const existing = registry.keyframesCache.get(contentHash);
6625
6753
  if (existing) {
6626
6754
  existing.refCount++;
@@ -6712,108 +6840,132 @@ var StyleInjector = class {
6712
6840
  }
6713
6841
  }
6714
6842
  /**
6715
- * Record a render-time usage hit for one or more classNames.
6716
- * Handles space-separated multi-chunk classNames.
6717
- * When the global touch counter reaches `touchInterval`, schedules a GC
6718
- * via `requestIdleCallback`.
6719
- * No-op on the server.
6843
+ * Count a render, and schedule a collection pass every `touchInterval` of
6844
+ * them. Nothing else: what a class is worth keeping is decided by the sweep's
6845
+ * own DOM scan, so rendering does not track usage at all.
6846
+ *
6847
+ * @deprecated The class name is ignored — pass anything, or stop calling it.
6848
+ * Collection no longer records per-class usage, and scheduling does not need
6849
+ * to know which class was rendered. A class handed back by `inject()` is
6850
+ * marked wanted there, which is what reuse actually goes through.
6720
6851
  */
6721
- touch(className, options) {
6852
+ touch(_className, options) {
6722
6853
  if (typeof document === "undefined") return;
6723
6854
  if (!this.config.gc) return;
6724
- const root = options?.root || document;
6725
- const registry = this.sheetManager.getRegistry(root);
6726
- const now = Date.now();
6727
- if (registry.touchTick !== now) {
6728
- registry.touchTick = now;
6729
- registry.touchedTick.clear();
6730
- } else if (registry.touchedTick.has(className)) return;
6731
- registry.touchedTick.add(className);
6732
- const parts = className.indexOf(" ") === -1 ? [className] : className.split(" ");
6733
- for (const cls of parts) {
6734
- if (!this.classRegex.test(cls)) continue;
6735
- if (!registry.rules.has(cls)) continue;
6736
- const entry = registry.usageMap.get(cls);
6737
- if (entry) entry.lastTouchedAt = now;
6738
- else registry.usageMap.set(cls, { lastTouchedAt: now });
6739
- registry.touchCount++;
6740
- }
6741
- const touchInterval = this.config.gc.touchInterval ?? 1e3;
6742
- if (registry.touchCount >= touchInterval) {
6855
+ const registry = this.sheetManager.getRegistry(options?.root || document);
6856
+ if (++registry.touchCount >= (this.config.gc.touchInterval ?? 1e3)) {
6743
6857
  registry.touchCount = 0;
6744
6858
  this.scheduleGC();
6745
6859
  }
6746
6860
  }
6747
6861
  /**
6748
- * Schedule a GC via `requestIdleCallback` (or synchronously as fallback).
6749
- * Runs GC on all active roots. Avoids double-scheduling via `pendingGCHandle`.
6862
+ * Schedule a GC in idle time. Runs GC on all active roots, and avoids
6863
+ * double-scheduling.
6864
+ *
6865
+ * Idle only. Without `requestIdleCallback` nothing is collected
6866
+ * automatically: running the sweep inline here would put it inside the render
6867
+ * that touched the class, and collection is never urgent enough for that.
6750
6868
  */
6751
6869
  scheduleGC() {
6752
- if (this.pendingGCHandle != null) return;
6870
+ if (this.cancelPendingGC) return;
6753
6871
  const runGC = () => {
6754
- this.pendingGCHandle = null;
6872
+ this.cancelPendingGC = null;
6755
6873
  this.sheetManager.pruneDisconnectedRoots();
6756
6874
  for (const root of this.sheetManager.getActiveRoots()) this.gc({ root });
6757
6875
  };
6758
- if (typeof requestIdleCallback !== "undefined") this.pendingGCHandle = requestIdleCallback(() => runGC());
6759
- else runGC();
6876
+ if (typeof requestIdleCallback !== "undefined") {
6877
+ const handle = requestIdleCallback(() => runGC());
6878
+ this.cancelPendingGC = () => cancelIdleCallback(handle);
6879
+ }
6880
+ }
6881
+ /**
6882
+ * The one definition of "unused": this injector owns the class's rules, no
6883
+ * element in `root` carries it, and nobody pinned it.
6884
+ *
6885
+ * `gc()` evicts from this set, `getMetrics()` counts it, and `tastyDebug`
6886
+ * reports it — all through here, so the three can never disagree about what
6887
+ * "unused" means. Entries carry their last `touch()` so callers can drop the
6888
+ * oldest first; a class that was never touched sorts as oldest.
6889
+ */
6890
+ /**
6891
+ * Everything the injector holds falls into one of five bands, and only the
6892
+ * last of them is ever deleted:
6893
+ *
6894
+ * 1. rendered — some element carries the class right now
6895
+ * 2. not ours — queued for a batched write, pre-allocated, or server-rendered
6896
+ * 3. hot — nothing carries it, but that was noticed less than `grace` ago
6897
+ * 4. cached — cold, but within `capacity` when ordered by when it went cold
6898
+ * 5. the rest
6899
+ *
6900
+ * This returns bands 4 and 5 together, most recently cold first; `gc()` draws
6901
+ * the capacity line through them. Band 3 is what makes collection safe
6902
+ * without a commit signal: a render can resolve a class and commit it a
6903
+ * little later, and during that gap nothing on the page carries it.
6904
+ */
6905
+ collectUnused(registry, root) {
6906
+ const now = Date.now();
6907
+ const grace = this.config.gc?.grace ?? DEFAULT_GC_GRACE;
6908
+ const liveClasses = /* @__PURE__ */ new Set();
6909
+ for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
6910
+ const unused = [];
6911
+ for (const [className, ruleInfo] of registry.rules) {
6912
+ if (ruleInfo.sheetIndex < 0) continue;
6913
+ if (liveClasses.has(className)) {
6914
+ registry.unusedSince.delete(className);
6915
+ continue;
6916
+ }
6917
+ if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
6918
+ let since = registry.unusedSince.get(className);
6919
+ if (since === void 0) {
6920
+ since = now;
6921
+ registry.unusedSince.set(className, now);
6922
+ }
6923
+ if (now - since < grace) continue;
6924
+ unused.push(className);
6925
+ }
6926
+ unused.sort((a, b) => (registry.unusedSince.get(b) ?? 0) - (registry.unusedSince.get(a) ?? 0));
6927
+ return unused;
6928
+ }
6929
+ /**
6930
+ * Class names this injector holds CSS for that nothing renders and nobody
6931
+ * pinned — exactly what `gc({ force: true })` would delete. Unordered.
6932
+ */
6933
+ getUnusedClasses(options) {
6934
+ flushStyles();
6935
+ if (typeof document === "undefined") return [];
6936
+ const root = options?.root || document;
6937
+ const registry = this.sheetManager.getRegistry(root);
6938
+ return this.collectUnused(registry, root);
6760
6939
  }
6761
6940
  /**
6762
6941
  * Synchronous garbage collection.
6763
6942
  *
6764
- * 1. Quick upper-bound check: skip if unused count can't exceed capacity.
6765
- * 2. Scans the DOM for live tasty classNames (safety guard).
6766
- * 3. With `force: true`: deletes all unused entries inline.
6767
- * Without `force`: collects unused, sorts oldest-first, evicts over capacity.
6943
+ * 1. Quick upper-bound check: skip if the registry is smaller than capacity.
6944
+ * 2. Scans the DOM for live tasty classNames the DOM, not a ref count, is
6945
+ * what says a class rendered by a component is still in use.
6946
+ * 3. With `force: true`: deletes every unused class.
6947
+ * Without `force`: keeps the `capacity` most recently touched and deletes
6948
+ * the rest, oldest first.
6768
6949
  *
6769
6950
  * @returns Number of styles evicted.
6770
6951
  */
6771
6952
  gc(options) {
6772
6953
  flushStyles();
6773
6954
  if (typeof document === "undefined") return 0;
6774
- if (this.pendingGCHandle != null) {
6775
- if (typeof cancelIdleCallback !== "undefined") cancelIdleCallback(this.pendingGCHandle);
6776
- this.pendingGCHandle = null;
6777
- }
6955
+ this.cancelPendingGC?.();
6956
+ this.cancelPendingGC = null;
6778
6957
  const root = options?.root || document;
6779
- const force = options?.force ?? false;
6958
+ const force = options?.force;
6780
6959
  const registry = this.sheetManager.getRegistry(root);
6781
6960
  const capacity = this.config.gc?.capacity ?? 1e3;
6782
- if (!force) {
6783
- let activeCount = 0;
6784
- for (const refCount of registry.refCounts.values()) if (refCount > 0) activeCount++;
6785
- if (registry.usageMap.size - activeCount <= capacity) return 0;
6786
- }
6787
- const liveClasses = /* @__PURE__ */ new Set();
6788
- for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
6789
- let swept = 0;
6790
- if (force) for (const [className] of registry.usageMap) {
6791
- if (liveClasses.has(className)) continue;
6792
- if ((registry.refCounts.get(className) ?? 0) > 0) continue;
6793
- registry.usageMap.delete(className);
6794
- swept++;
6795
- }
6796
- else {
6797
- const unused = [];
6798
- for (const [className, usage] of registry.usageMap) {
6799
- if (liveClasses.has(className)) continue;
6800
- if ((registry.refCounts.get(className) ?? 0) > 0) continue;
6801
- unused.push({
6802
- className,
6803
- lastTouchedAt: usage.lastTouchedAt
6804
- });
6805
- }
6806
- if (unused.length > capacity) {
6807
- unused.sort((a, b) => a.lastTouchedAt - b.lastTouchedAt);
6808
- const toEvict = unused.length - capacity;
6809
- for (let i = 0; i < toEvict; i++) {
6810
- registry.usageMap.delete(unused[i].className);
6811
- swept++;
6812
- }
6813
- }
6814
- }
6815
- if (swept > 0) this.sheetManager.forceCleanup(registry);
6816
- return swept;
6961
+ if (!force && registry.rules.size <= capacity) return 0;
6962
+ const unused = this.collectUnused(registry, root);
6963
+ let doomed;
6964
+ if (force) doomed = unused;
6965
+ else if (unused.length > capacity) doomed = unused.slice(capacity);
6966
+ else return 0;
6967
+ if (doomed.length === 0) return 0;
6968
+ return this.sheetManager.deleteClasses(registry, doomed);
6817
6969
  }
6818
6970
  /**
6819
6971
  * Destroy all resources for a root
@@ -6822,9 +6974,9 @@ var StyleInjector = class {
6822
6974
  flushStyles();
6823
6975
  const targetRoot = root || document;
6824
6976
  this.sheetManager.cleanup(targetRoot);
6825
- if (this.pendingGCHandle != null && !this.sheetManager.hasActiveRoots()) {
6826
- if (typeof cancelIdleCallback !== "undefined") cancelIdleCallback(this.pendingGCHandle);
6827
- this.pendingGCHandle = null;
6977
+ if (this.cancelPendingGC && !this.sheetManager.hasActiveRoots()) {
6978
+ this.cancelPendingGC();
6979
+ this.cancelPendingGC = null;
6828
6980
  }
6829
6981
  }
6830
6982
  };
@@ -11697,7 +11849,7 @@ function createDefaultConfig(isTest) {
11697
11849
  function markStylesGenerated() {
11698
11850
  if (stylesGenerated) return;
11699
11851
  stylesGenerated = true;
11700
- if (typeof document !== "undefined" && document.querySelector("style[data-tasty-ssr]")) {
11852
+ if (typeof document !== "undefined" && document.querySelector("[data-tasty-ssr]")) {
11701
11853
  warnOnce("ssr-globals-skip", "[Tasty] SSR styles detected — skipping client-side global CSS injection to avoid duplicates.");
11702
11854
  return;
11703
11855
  }
@@ -12266,4 +12418,4 @@ function resetConfig() {
12266
12418
  //#endregion
12267
12419
  export { SheetManager as $, StyleParser as $t, FLOW_STYLES as A, STYLE_TO_CHUNK as At, createStateParserContext as B, getGlobalParseFunctions as Bt, BASE_STYLES as C, APPEARANCE_CHUNK_STYLES as Ct, COLOR_STYLES as D, FONT_CHUNK_STYLES as Dt, BLOCK_STYLES as E, DISPLAY_CHUNK_STYLES as Et, hasPipelineCacheEntry as F, registerFunctionPolyfill as Ft, StyleInjector as G, parseStyle as Gt, extractPredefinedStateRefs as H, getGlobalPredefinedTokens as Ht, isSelector as I, registerLocalFunctionPolyfills as It, hasLocalCounterStyle as J, okhstPlugin as Jt, extractLocalCounterStyle as K, stringifyStyles as Kt, renderStyles as L, CUSTOM_UNITS as Lt, OUTER_STYLES as M, formatFunctionRule as Mt, POSITION_STYLES as N, hasLocalFunctions as Nt, CONTAINER_STYLES as O, LAYOUT_CHUNK_STYLES as Ot, TEXT_STYLES as P, parseFunctionName as Pt, hasLocalFontFace as Q, isDevEnv as Qt, parseStateKey as R, DIRECTIONS as Rt, baseStylePropsRegistry as S, propHandlerRegistry as St, BLOCK_OUTER_STYLES as T, DIMENSION_CHUNK_STYLES as Tt, getGlobalPredefinedStates as U, normalizeColorTokenValue as Ut, extractLocalPredefinedStates as V, getGlobalParser as Vt, setGlobalPredefinedStates as W, parseColor as Wt, fontFaceContentHash as X, okhslPlugin as Xt, extractLocalFontFace as Y, okhslFunction as Yt, formatFontFaceRule as Z, createColorFunc as Zt, isFunctionsPolyfillEnabled as _, hashString as _t, getGlobalCounterStyles as a, hslToRgbValues as an, closeBatchWindow as at, resetConfig as b, hasLocalProperties as bt, getGlobalInjector as c, openBatchWindow as ct, getGlobalStyles as d, DEFAULT_ZERO_NAME_PREFIX as dt, Lru as en, STYLE_HANDLER_MAP as et, getNamePrefix as f, makeClassName as ft, isConfigLocked as g, validateNamePrefix as gt, hasStylesGenerated as h, tastyClassRegex as ht, getGlobalConfigTokens as i, hexToRgb as in, PropertyTypeResolver as it, INNER_STYLES as j, extractLocalFunctions as jt, DIMENSION_STYLES as k, POSITION_CHUNK_STYLES as kt, getGlobalKeyframes as l, resetStyleBatch as lt, hasGlobalRecipes as m, makeKeyframeName as mt, getConfig as n, getNamedColorHex as nn, styleHandlers as nt, getGlobalFontFaces as o, strToRgb as on, flushStyles as ot, hasGlobalKeyframes as p, makeCounterStyleName as pt, formatCounterStyleRule as q, okhstFunction as qt, getEffectiveProperties as r, getRgbValuesFromRgbaString as rn, createStyle as rt, getGlobalFunctions as s, normalizeDslName as sn, hasPendingStyleWrites as st, configure as t, overrideColorAlpha as tn, defineHandler as tt, getGlobalRecipes as u, DEFAULT_NAME_PREFIX as ut, isTestEnvironment as v, extractLocalProperties as vt, BLOCK_INNER_STYLES as w, CHUNK_NAMES as wt, generateTypographyTokens as x, parsePropertyToken as xt, markStylesGenerated as y, getEffectiveDefinition as yt, camelToKebab as z, filterMods as zt };
12268
12420
 
12269
- //# sourceMappingURL=config-B5kHzuNz.js.map
12421
+ //# sourceMappingURL=config-B3gPdCqd.js.map