@tenphi/tasty 0.0.0-snapshot.27b6708 → 0.0.0-snapshot.28b6339

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 (109) hide show
  1. package/README.md +38 -54
  2. package/dist/{babel-BUQGeOXA.d.ts → babel-Zs1Q03qt.d.ts} +2 -2
  3. package/dist/chunks/async-storage-DKK-wTD4.js.map +1 -0
  4. package/dist/chunks/build-config-Mk7waOsw.js +45 -0
  5. package/dist/chunks/build-config-Mk7waOsw.js.map +1 -0
  6. package/dist/{collector-DTahQUiV.js → chunks/collector-Ducc5l3E.js} +29 -15
  7. package/dist/chunks/collector-Ducc5l3E.js.map +1 -0
  8. package/dist/chunks/config-engine-tKK7WxJO.js +331 -0
  9. package/dist/chunks/config-engine-tKK7WxJO.js.map +1 -0
  10. package/dist/chunks/css-definitions-Crstltrf.js +1229 -0
  11. package/dist/chunks/css-definitions-Crstltrf.js.map +1 -0
  12. package/dist/chunks/css-resources-Cyl_axbI.js +149 -0
  13. package/dist/chunks/css-resources-Cyl_axbI.js.map +1 -0
  14. package/dist/chunks/debug-HyyLIf6O.js +543 -0
  15. package/dist/chunks/debug-HyyLIf6O.js.map +1 -0
  16. package/dist/chunks/dsl-D7kBow--.js +1771 -0
  17. package/dist/chunks/dsl-D7kBow--.js.map +1 -0
  18. package/dist/chunks/hydration-CLdVKcH2.js +59 -0
  19. package/dist/chunks/hydration-CLdVKcH2.js.map +1 -0
  20. package/dist/chunks/react-runtime-UlZcbsEN.js +877 -0
  21. package/dist/chunks/react-runtime-UlZcbsEN.js.map +1 -0
  22. package/dist/chunks/runtime-engine-D2WOLvd6.js +3439 -0
  23. package/dist/chunks/runtime-engine-D2WOLvd6.js.map +1 -0
  24. package/dist/chunks/runtime-state-DOeH6rsG.js +138 -0
  25. package/dist/chunks/runtime-state-DOeH6rsG.js.map +1 -0
  26. package/dist/{merge-styles-oklji0KB.js → chunks/shared-utils-Bgl8FNGJ.js} +64 -24
  27. package/dist/chunks/shared-utils-Bgl8FNGJ.js.map +1 -0
  28. package/dist/chunks/style-engine-C4ZcOAPb.js +6915 -0
  29. package/dist/chunks/style-engine-C4ZcOAPb.js.map +1 -0
  30. package/dist/{css-writer-B-J87ncv.js → chunks/zero-engine-D4Rluo7u.js} +3 -3
  31. package/dist/chunks/zero-engine-D4Rluo7u.js.map +1 -0
  32. package/dist/{collector-DUaHCcTS.d.ts → collector-hl5KNH7k.d.ts} +23 -3
  33. package/dist/{config-YsxGv4tq.d.ts → config-DWL1ZYue.d.ts} +150 -55
  34. package/dist/core/index.d.ts +5 -5
  35. package/dist/core/index.js +9 -6
  36. package/dist/{index-PqN-DIpn.d.ts → index-D9N9GV24.d.ts} +129 -93
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +11 -922
  39. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-C_oMoJVd.d.ts} +2 -2
  40. package/dist/ssr/astro-client.js +1 -1
  41. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  42. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  43. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  44. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  45. package/dist/ssr/astro-middleware-extract.js +9 -0
  46. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  47. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  48. package/dist/ssr/astro-middleware.d.ts +3 -1
  49. package/dist/ssr/astro.d.ts +45 -3
  50. package/dist/ssr/astro.js +158 -15
  51. package/dist/ssr/astro.js.map +1 -1
  52. package/dist/ssr/index.d.ts +7 -7
  53. package/dist/ssr/index.js +4 -4
  54. package/dist/ssr/index.js.map +1 -1
  55. package/dist/ssr/next-config.d.ts +66 -0
  56. package/dist/ssr/next-config.js +135 -0
  57. package/dist/ssr/next-config.js.map +1 -0
  58. package/dist/ssr/next.d.ts +9 -2
  59. package/dist/ssr/next.js +27 -11
  60. package/dist/ssr/next.js.map +1 -1
  61. package/dist/static/index.d.ts +2 -2
  62. package/dist/static/index.js +1 -1
  63. package/dist/{index-Cd45t5NM.d.ts → types-jKZNpias.d.ts} +898 -803
  64. package/dist/zero/babel.d.ts +1 -1
  65. package/dist/zero/babel.js +25 -24
  66. package/dist/zero/babel.js.map +1 -1
  67. package/dist/zero/index.d.ts +1 -1
  68. package/dist/zero/index.js +1 -1
  69. package/dist/zero/next.d.ts +1 -1
  70. package/docs/README.md +5 -3
  71. package/docs/adoption.md +3 -3
  72. package/docs/ai-agents.md +83 -78
  73. package/docs/comparison.md +12 -12
  74. package/docs/configuration.md +47 -47
  75. package/docs/debug.md +19 -7
  76. package/docs/dsl.md +3 -3
  77. package/docs/getting-started.md +10 -10
  78. package/docs/injector.md +62 -25
  79. package/docs/methodology.md +3 -3
  80. package/docs/migration-v3.md +49 -49
  81. package/docs/plugins.md +37 -33
  82. package/docs/react-api.md +2 -2
  83. package/docs/runtime-benchmarks.md +351 -0
  84. package/docs/ssr.md +219 -61
  85. package/docs/styles.md +43 -11
  86. package/docs/tasty-static.md +136 -103
  87. package/package.json +80 -11
  88. package/dist/async-storage-DKK-wTD4.js.map +0 -1
  89. package/dist/collector-DTahQUiV.js.map +0 -1
  90. package/dist/config-B5kHzuNz.js +0 -12269
  91. package/dist/config-B5kHzuNz.js.map +0 -1
  92. package/dist/context-CA8YKeMn.js +0 -24
  93. package/dist/context-CA8YKeMn.js.map +0 -1
  94. package/dist/core-Dr4u1NVD.js +0 -1566
  95. package/dist/core-Dr4u1NVD.js.map +0 -1
  96. package/dist/css-writer-B-J87ncv.js.map +0 -1
  97. package/dist/format-global-rules-DklyaXv-.js +0 -22
  98. package/dist/format-global-rules-DklyaXv-.js.map +0 -1
  99. package/dist/format-rules-DKOA-6qu.js +0 -130
  100. package/dist/format-rules-DKOA-6qu.js.map +0 -1
  101. package/dist/hydrate-OeMX99We.js +0 -37
  102. package/dist/hydrate-OeMX99We.js.map +0 -1
  103. package/dist/index.js.map +0 -1
  104. package/dist/keyframes-CV8azJf3.js +0 -493
  105. package/dist/keyframes-CV8azJf3.js.map +0 -1
  106. package/dist/merge-styles-oklji0KB.js.map +0 -1
  107. package/dist/resolve-recipes-DTG81rzl.js +0 -144
  108. package/dist/resolve-recipes-DTG81rzl.js.map +0 -1
  109. /package/dist/{async-storage-DKK-wTD4.js → chunks/async-storage-DKK-wTD4.js} +0 -0
@@ -0,0 +1,3439 @@
1
+ import { f as parseStyle, j as normalizeDslName, m as resetGlobalPredefinedTokens, p as resetGlobalParseFunctions, u as normalizeColorTokenValue, w as Lru } from "./dsl-D7kBow--.js";
2
+ import { i as warn, n as isSelector, o as isDevEnv, r as resetStyleWarnings } from "./shared-utils-Bgl8FNGJ.js";
3
+ import { A as validateNamePrefix, B as formatFunctionPrelude, D as makeKeyframeName, F as normalizePropertyDefinition, G as registerLocalFunctionPolyfills, I as parsePropertyToken, K as resetFunctionPolyfills, M as extractLocalProperties, N as getEffectiveDefinition, O as rscClassRegexGlobal, R as extractLocalFunctions, S as PropertyTypeResolver, T as makeClassName, U as parseFunctionName, V as formatFunctionRule, _ as extractLocalFontFace, a as formatPropertyCSS, b as formatFontFaceRule, c as filterUsedKeyframes, d as replaceAnimationNames, f as resolveKeyframesNames, h as formatCounterStyleRule, i as formatKeyframesCSS, j as hashString, k as tastyClassRegex, l as hasLocalKeyframes, m as formatCounterStyleDeclarations, o as extractAnimationNamesFromStyles, p as extractLocalCounterStyle, r as formatRules, s as extractLocalKeyframes, u as mergeKeyframes, v as fontFaceContentHash, y as formatFontFaceDeclarations, z as formatFunctionDeclarations } from "./css-definitions-Crstltrf.js";
4
+ import { C as STYLE_HANDLER_MAP, D as resetHandlers, G as resetGlobalPolyfillsState, H as hasStylesGenerated, K as resetStylesGeneratedState, R as resetStyleChunks, S as camelToKebab, U as isFunctionsPolyfillEnabled, W as markStylesGeneratedState, a as registerBaseStyleProps, b as renderStyles, k as createStyle, n as generateChunkCacheKey, o as resetBaseStyleProps, r as categorizeStyleKeys, t as renderStylesForChunk, y as clearPipelineCache } from "./style-engine-C4ZcOAPb.js";
5
+ import { n as applyStyleConfig, r as normalizeConfig, t as resolveRecipesWith } from "./config-engine-tKK7WxJO.js";
6
+ import { _ as resetConfigResources, a as getGlobalConfigTokens, c as getGlobalFunctions, d as mergeGlobalConfigTokens, f as mergeGlobalCounterStyles, g as mergeGlobalStyles, h as mergeGlobalProperties, i as getEffectiveProperties, l as getGlobalStyles, m as mergeGlobalFunctions, o as getGlobalCounterStyles, p as mergeGlobalFontFaces, s as getGlobalFontFaces, t as getRegisteredSSRCollector, v as setRuntimeConfigState } from "./runtime-state-DOeH6rsG.js";
7
+ import { cache } from "react";
8
+ //#region src/prop-handlers.ts
9
+ /**
10
+ * Props middleware for tasty components.
11
+ *
12
+ * A prop handler receives a component's props and returns them, changed or not.
13
+ * That makes it the extension point for props that are *not* style properties:
14
+ * it can read a custom prop, strip it so it never reaches the DOM, and fold its
15
+ * meaning into `styles`, `mods`, `tokens`, `variant`, or `as`.
16
+ *
17
+ * ```ts
18
+ * configure({
19
+ * propHandlers: {
20
+ * glaze: (props) => {
21
+ * const { glaze, ...rest } = props;
22
+ * if (!glaze) return rest;
23
+ * return { ...rest, styles: mergeStyles(glazeStyles(glaze), rest.styles) };
24
+ * },
25
+ * },
26
+ * });
27
+ * ```
28
+ *
29
+ * Handlers run on every render of every tasty component, so the registry is a
30
+ * pre-composed chain that is `null` while nothing is registered — the cost when
31
+ * unused is one property load and one branch.
32
+ */
33
+ const GTKEY = "__tasty_prop_handlers__";
34
+ const globalStore = globalThis;
35
+ const propHandlerRegistry = globalStore[GTKEY] ?? (globalStore[GTKEY] = {
36
+ apply: null,
37
+ list: []
38
+ });
39
+ function normalize(key, definition, options) {
40
+ const source = options?.source ?? "configure()";
41
+ if (typeof definition === "function") return {
42
+ key,
43
+ triggers: [key],
44
+ fn: definition,
45
+ source
46
+ };
47
+ if (Array.isArray(definition)) {
48
+ const [first, fn] = definition;
49
+ if (typeof fn !== "function") throw new Error(`[Tasty] Invalid prop handler definition for "${key}". Tuple must have a function as the second element: [string, function] or [string[], function].`);
50
+ if (first === "*") return {
51
+ key,
52
+ triggers: null,
53
+ fn,
54
+ source
55
+ };
56
+ if (typeof first === "string") return {
57
+ key,
58
+ triggers: [first],
59
+ fn,
60
+ source
61
+ };
62
+ if (Array.isArray(first)) return {
63
+ key,
64
+ triggers: first.includes("*") ? null : first,
65
+ fn,
66
+ source
67
+ };
68
+ throw new Error(`[Tasty] Invalid prop handler definition for "${key}". First element must be a string or string array.`);
69
+ }
70
+ throw new Error(`[Tasty] Invalid prop handler definition for "${key}". Expected function, [string, function], or [string[], function].`);
71
+ }
72
+ /**
73
+ * Run one handler, treating a nothing/non-object return as "unchanged".
74
+ *
75
+ * `isDevEnv()` is called lazily rather than captured at module load so the
76
+ * warnings are assertable in tests; it is only reached on the misuse path.
77
+ */
78
+ function runOne(handler, props) {
79
+ const next = handler.fn(props);
80
+ if (next != null && typeof next === "object" && !Array.isArray(next)) return next;
81
+ if (isDevEnv()) console.warn(next == null ? `[Tasty] propHandlers["${handler.key}"] (from ${handler.source}) returned ${String(next)}. Props are treated as unchanged — did you forget to \`return props\`?` : `[Tasty] propHandlers["${handler.key}"] (from ${handler.source}) returned ${Array.isArray(next) ? "an array" : typeof next}. A prop handler must return a props object. The result was ignored.`);
82
+ return props;
83
+ }
84
+ function compose(list) {
85
+ if (list.length === 0) return null;
86
+ if (list.length === 1) {
87
+ const only = list[0];
88
+ if (only.triggers === null) return (props) => runOne(only, props);
89
+ if (only.triggers.length === 1) {
90
+ const trigger = only.triggers[0];
91
+ return (props) => trigger in props ? runOne(only, props) : props;
92
+ }
93
+ }
94
+ return (props) => {
95
+ for (const handler of list) {
96
+ const triggers = handler.triggers;
97
+ if (triggers !== null) {
98
+ let triggered = false;
99
+ for (const trigger of triggers) if (trigger in props) {
100
+ triggered = true;
101
+ break;
102
+ }
103
+ if (!triggered) continue;
104
+ }
105
+ props = runOne(handler, props);
106
+ }
107
+ return props;
108
+ };
109
+ }
110
+ /**
111
+ * Register a props middleware under `key`, replacing any handler already
112
+ * registered under the same key while keeping its position in the chain.
113
+ */
114
+ function registerPropHandler(key, definition, options) {
115
+ const normalized = normalize(key, definition, options);
116
+ const { list } = propHandlerRegistry;
117
+ const existing = list.findIndex((handler) => handler.key === key);
118
+ if (existing === -1) list.push(normalized);
119
+ else list[existing] = normalized;
120
+ propHandlerRegistry.apply = compose(list);
121
+ }
122
+ /** Drop every registered props middleware. Called by `resetConfig()`. */
123
+ function resetPropHandlers() {
124
+ if (propHandlerRegistry.list.length === 0) return;
125
+ propHandlerRegistry.list.length = 0;
126
+ propHandlerRegistry.apply = null;
127
+ }
128
+ //#endregion
129
+ //#region src/injector/batch.ts
130
+ /**
131
+ * Deferred CSSOM writes ("batched injection").
132
+ *
133
+ * Every `insertRule()` on a live stylesheet invalidates style for the sheet's
134
+ * scope, so Blink recalculates style the next time anything reads layout or
135
+ * computed style. When components inject during React's render phase and other
136
+ * components read layout in the same pass, the two interleave:
137
+ *
138
+ * inject -> read (forced recalc) -> inject -> read (forced recalc) -> ...
139
+ *
140
+ * Batching moves every sheet write out from between those reads. Writes are
141
+ * queued in FIFO order and drained in one go, so the tree is invalidated once
142
+ * per flush instead of once per component.
143
+ *
144
+ * ## Ordering
145
+ *
146
+ * A single queue holds *all* writes — component rules, global rules, keyframes,
147
+ * `@property`, `@font-face`, `@counter-style`, `@function` and raw CSS. Draining
148
+ * it in insertion order keeps the sheet byte-identical to unbatched output,
149
+ * which matters because equal-specificity rules resolve by document order.
150
+ *
151
+ * ## Batch windows — why queuing is safe
152
+ *
153
+ * Deferring a write past React's layout phase would let a `useLayoutEffect`
154
+ * measure an element whose rules are not in the sheet yet, reading the unstyled
155
+ * box. `<TastyBatchProvider>` closes that hole by opening a *window* during its
156
+ * render and closing it — flushing — in its `useInsertionEffect`, which React
157
+ * runs in the mutation phase, before any layout effect:
158
+ *
159
+ * provider renders -> window OPEN
160
+ * children render -> injections queued
161
+ * provider insertionEffect -> FLUSH, window CLOSED
162
+ * layout effects run -> rules are in the sheet
163
+ *
164
+ * So in the default (`batchInjection: true`) mode a write is only ever queued
165
+ * inside a commit whose flush is already guaranteed by that same commit. Every
166
+ * injection outside a window — a deep update the provider did not re-render
167
+ * for, a `useLayoutEffect` that injects, an event handler, an async callback —
168
+ * is written straight through, exactly as with batching off.
169
+ *
170
+ * `batchInjection: 'always'` opts out of the gate and queues unconditionally.
171
+ * That wins on more commits, at the cost of the measurement hazard above.
172
+ *
173
+ * ## Flush points (earliest wins)
174
+ *
175
+ * 1. `<TastyBatchProvider>`'s `useInsertionEffect` — closes the window.
176
+ * 2. A microtask — a backstop for a render that was aborted or suspended and
177
+ * therefore never reached its insertion effect. An aborted render mounts
178
+ * nothing, so nothing can measure what it queued. Microtasks also always
179
+ * drain before paint, so styles are never visually missing.
180
+ * 3. `flushStyles()` — explicit, and called internally by every injector read
181
+ * API (`getCSSText`, `cleanup`, `gc`, `destroy`, ...).
182
+ */
183
+ /**
184
+ * FIFO of pending writes. Drained with a moving `head` index rather than
185
+ * `shift()` so a large batch stays O(n) instead of O(n²).
186
+ */
187
+ let queue = [];
188
+ let head = 0;
189
+ let microtaskScheduled = false;
190
+ let flushing = false;
191
+ /**
192
+ * Whether a `<TastyBatchProvider>` has rendered in the current commit and will
193
+ * therefore flush in its insertion effect, making it safe to queue a write.
194
+ *
195
+ * A flag rather than a depth count, because renders and insertion effects do
196
+ * not pair up one-to-one. StrictMode double-invokes render but runs the
197
+ * insertion effect once, so a counter ends the commit stuck above zero — and a
198
+ * window that stays open past its commit turns the next provider-less commit
199
+ * into `'always'` mode behind the user's back, which is exactly the measurement
200
+ * hazard `true` exists to avoid.
201
+ *
202
+ * Clearing on the first close is safe with nested or sibling providers: React
203
+ * finishes every render in a commit before running any insertion effect, so once
204
+ * one closes, no further render-phase injection can arrive in that commit.
205
+ */
206
+ let windowOpen = false;
207
+ /** Whether any window has ever been opened, i.e. a provider is in the tree. */
208
+ let everOpened = false;
209
+ /** Dev-only: warn at most once that batching is on with no provider mounted. */
210
+ let warnedNoProvider = false;
211
+ /**
212
+ * Queue a sheet write.
213
+ *
214
+ * Returns a handle whose `cancel()` drops the write if the caller disposes
215
+ * before the flush. The handle is intentionally tiny — one is allocated per
216
+ * injected class, on the cache-miss path only.
217
+ */
218
+ function enqueueStyleWrite(run) {
219
+ if (flushing) {
220
+ run();
221
+ return {
222
+ run,
223
+ cancelled: false,
224
+ done: true
225
+ };
226
+ }
227
+ const entry = {
228
+ run,
229
+ cancelled: false,
230
+ done: false
231
+ };
232
+ queue.push(entry);
233
+ scheduleMicrotaskFlush();
234
+ return entry;
235
+ }
236
+ /** Whether any write is still waiting to hit a stylesheet. */
237
+ function hasPendingStyleWrites() {
238
+ return head < queue.length;
239
+ }
240
+ /**
241
+ * Open a batch window. Called from `<TastyBatchProvider>`'s render, so every
242
+ * descendant injection in this commit is covered by the provider's insertion
243
+ * effect.
244
+ *
245
+ * Called during render on purpose: the window must be open before children
246
+ * render. It is idempotent — so StrictMode's double render costs nothing — and
247
+ * carries no other side effect. A render that is thrown away is recovered by the
248
+ * microtask backstop.
249
+ *
250
+ * A no-op without a `document`. On the server there is no sheet to batch
251
+ * against, and `useInsertionEffect` never runs, so nothing would ever close a
252
+ * window this opened. Skipping it keeps the provider inert during SSR and RSC
253
+ * instead of merely harmless.
254
+ */
255
+ function openBatchWindow() {
256
+ if (typeof document === "undefined") return;
257
+ windowOpen = true;
258
+ everOpened = true;
259
+ }
260
+ /**
261
+ * Close a batch window and flush. Called from the provider's
262
+ * `useInsertionEffect`, i.e. after every render in the commit and before any
263
+ * layout effect.
264
+ */
265
+ function closeBatchWindow() {
266
+ windowOpen = false;
267
+ flushStyles();
268
+ }
269
+ /** Whether a batch window is currently open. */
270
+ function isBatchWindowOpen() {
271
+ return windowOpen;
272
+ }
273
+ function scheduleMicrotaskFlush() {
274
+ if (microtaskScheduled || flushing) return;
275
+ microtaskScheduled = true;
276
+ queueMicrotask(() => {
277
+ microtaskScheduled = false;
278
+ windowOpen = false;
279
+ flushStyles();
280
+ });
281
+ }
282
+ /**
283
+ * Drain every pending sheet write, in insertion order.
284
+ *
285
+ * Safe to call when the queue is empty (the common case, so the guard comes
286
+ * first) and safe to call re-entrantly: a nested `flushStyles()` is a no-op,
287
+ * and work a draining write triggers is written in place by
288
+ * `enqueueStyleWrite` rather than queued behind the rest of the batch.
289
+ */
290
+ function flushStyles() {
291
+ if (head >= queue.length) return;
292
+ if (flushing) return;
293
+ flushing = true;
294
+ try {
295
+ while (head < queue.length) {
296
+ const entry = queue[head++];
297
+ if (entry.cancelled) {
298
+ entry.done = true;
299
+ continue;
300
+ }
301
+ entry.run();
302
+ entry.done = true;
303
+ }
304
+ } finally {
305
+ queue = [];
306
+ head = 0;
307
+ flushing = false;
308
+ }
309
+ }
310
+ /**
311
+ * Dev-only: `batchInjection: true` batches nothing unless a window opens, and no
312
+ * window has ever opened, so no provider is in the tree. Called by the injector
313
+ * the first time it declines to batch.
314
+ */
315
+ function warnBatchProviderMissing() {
316
+ if (everOpened || warnedNoProvider || !isDevEnv()) return;
317
+ warnedNoProvider = true;
318
+ warn("[Tasty] batchInjection needs <TastyBatchProvider> mounted to batch.");
319
+ }
320
+ /**
321
+ * Drop every pending write without applying it. Test helper — production code
322
+ * should call `flushStyles()` instead.
323
+ */
324
+ function resetStyleBatch() {
325
+ queue = [];
326
+ head = 0;
327
+ microtaskScheduled = false;
328
+ flushing = false;
329
+ windowOpen = false;
330
+ everOpened = false;
331
+ warnedNoProvider = false;
332
+ }
333
+ //#endregion
334
+ //#region src/injector/sheet-manager.ts
335
+ const supportsConstructableSheets = typeof CSSStyleSheet !== "undefined" && (() => {
336
+ try {
337
+ new CSSStyleSheet();
338
+ return true;
339
+ } catch {
340
+ return false;
341
+ }
342
+ })();
343
+ function wrapAtRules(css, atRules) {
344
+ return atRules?.reduce((wrapped, atRule) => `${atRule} { ${wrapped} }`, css) ?? css;
345
+ }
346
+ /** Split a selector list without treating commas inside [] / () / strings as separators. */
347
+ function splitSelectorsSafely(selectorList) {
348
+ const parts = [];
349
+ let buffer = "";
350
+ let squareDepth = 0;
351
+ let parenDepth = 0;
352
+ let quote = "";
353
+ for (let i = 0; i < selectorList.length; i++) {
354
+ const char = selectorList[i];
355
+ if (quote) {
356
+ if (char === quote && selectorList[i - 1] !== "\\") quote = "";
357
+ buffer += char;
358
+ continue;
359
+ }
360
+ if (char === "\"" || char === "'") {
361
+ quote = char;
362
+ buffer += char;
363
+ continue;
364
+ }
365
+ if (char === "[") squareDepth++;
366
+ else if (char === "]") squareDepth = Math.max(0, squareDepth - 1);
367
+ else if (char === "(") parenDepth++;
368
+ else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
369
+ if (char === "," && squareDepth === 0 && parenDepth === 0) {
370
+ const part = buffer.trim();
371
+ if (part) parts.push(part);
372
+ buffer = "";
373
+ } else buffer += char;
374
+ }
375
+ const tail = buffer.trim();
376
+ if (tail) parts.push(tail);
377
+ return parts;
378
+ }
379
+ function rulesToCSS(sheet) {
380
+ return Array.from(sheet.cssRules, (rule) => rule.cssText).join("\n");
381
+ }
382
+ function rawBlocksToCSS(blocks) {
383
+ return [...blocks.values()].sort((a, b) => a.startOffset - b.startOffset).map((block) => block.css).join("\n");
384
+ }
385
+ var SheetManager = class {
386
+ rootRegistries = /* @__PURE__ */ new WeakMap();
387
+ /** Strong set of active roots so background GC can iterate them all */
388
+ activeRoots = /* @__PURE__ */ new Set();
389
+ config;
390
+ /** Dedicated style elements for raw CSS per root */
391
+ rawStyleElements = /* @__PURE__ */ new WeakMap();
392
+ /** Constructable sheets for raw CSS in adopted mode */
393
+ rawConstructableSheets = /* @__PURE__ */ new WeakMap();
394
+ /** Tracking for raw CSS blocks per root */
395
+ rawCSSBlocks = /* @__PURE__ */ new WeakMap();
396
+ /** Counter for generating unique raw CSS IDs */
397
+ rawCSSCounter = 0;
398
+ constructor(config) {
399
+ this.config = config;
400
+ }
401
+ /**
402
+ * Resolve the underlying CSSStyleSheet from a SheetInfo,
403
+ * abstracting away adopted vs style-element modes.
404
+ */
405
+ getCSSSheet(sheetInfo) {
406
+ if (sheetInfo.constructableSheet) return sheetInfo.constructableSheet;
407
+ return sheetInfo.sheet?.sheet ?? null;
408
+ }
409
+ /**
410
+ * Record an inserted rule text at its rule index (text mode only).
411
+ *
412
+ * `textRules` mirrors the sheet's rule order. `textContent` cannot be edited
413
+ * rule-by-rule the way CSSOM can, so keeping the texts is what makes
414
+ * deletion possible at all in this mode.
415
+ */
416
+ trackTextRule(sheet, ruleIndex, ruleText) {
417
+ if (!sheet.textMode) return;
418
+ const rules = sheet.textRules ??= [];
419
+ while (rules.length < ruleIndex) rules.push("");
420
+ rules[ruleIndex] = ruleText;
421
+ }
422
+ /**
423
+ * Remove rule indices from a text-mode sheet and rewrite the element's text.
424
+ * Returns the indices that were actually removed.
425
+ */
426
+ deleteTextRules(sheet, indices) {
427
+ const rules = sheet.textRules;
428
+ if (!rules) return [];
429
+ const removed = [...new Set(indices)].filter((idx) => idx >= 0 && idx < rules.length).sort((a, b) => b - a);
430
+ for (const idx of removed) rules.splice(idx, 1);
431
+ if (removed.length > 0 && sheet.sheet) sheet.sheet.textContent = rules.length ? "\n" + rules.join("\n") : "";
432
+ return removed;
433
+ }
434
+ /**
435
+ * Determine the injection mode for a root.
436
+ * ShadowRoot uses adopted stylesheets when supported; Document uses <style> elements.
437
+ */
438
+ detectInjectionMode(root) {
439
+ if (root instanceof ShadowRoot && supportsConstructableSheets && !this.config.forceTextInjection) return "adopted";
440
+ return "style-element";
441
+ }
442
+ /**
443
+ * Get or create registry for a root (Document or ShadowRoot)
444
+ */
445
+ getRegistry(root) {
446
+ let registry = this.rootRegistries.get(root);
447
+ if (!registry) {
448
+ const metrics = this.config.devMode ? {
449
+ hits: 0,
450
+ misses: 0,
451
+ bulkCleanups: 0,
452
+ totalInsertions: 0,
453
+ totalUnused: 0,
454
+ stylesCleanedUp: 0,
455
+ cleanupHistory: [],
456
+ startTime: Date.now()
457
+ } : void 0;
458
+ registry = {
459
+ sheets: [],
460
+ pinCounts: /* @__PURE__ */ new Map(),
461
+ rules: /* @__PURE__ */ new Map(),
462
+ cacheKeyToClassName: /* @__PURE__ */ new Map(),
463
+ ruleTextSet: /* @__PURE__ */ new Set(),
464
+ metrics,
465
+ keyframesCache: /* @__PURE__ */ new Map(),
466
+ keyframesNameToContent: /* @__PURE__ */ new Map(),
467
+ keyframesCounter: 0,
468
+ injectedProperties: /* @__PURE__ */ new Map(),
469
+ injectedFontFaces: /* @__PURE__ */ new Set(),
470
+ injectedCounterStyles: /* @__PURE__ */ new Map(),
471
+ injectedFunctions: /* @__PURE__ */ new Map(),
472
+ globalRules: /* @__PURE__ */ new Map(),
473
+ propertyTypeResolver: new PropertyTypeResolver(),
474
+ unusedSince: /* @__PURE__ */ new Map(),
475
+ localKeyframes: /* @__PURE__ */ new Map(),
476
+ touchCount: 0,
477
+ serverClassSyncIndex: 0,
478
+ rscStylesScanned: false,
479
+ injectionMode: this.detectInjectionMode(root)
480
+ };
481
+ this.rootRegistries.set(root, registry);
482
+ this.activeRoots.add(root);
483
+ }
484
+ return registry;
485
+ }
486
+ /** Return all roots with active registries (for background GC sweep). */
487
+ getActiveRoots() {
488
+ return this.activeRoots;
489
+ }
490
+ /** Check whether any roots have active registries. */
491
+ hasActiveRoots() {
492
+ return this.activeRoots.size > 0;
493
+ }
494
+ /** Remove registries for ShadowRoots whose host has been detached from the DOM. */
495
+ pruneDisconnectedRoots() {
496
+ for (const root of this.activeRoots) if (root !== document && !root.host?.isConnected) this.cleanup(root);
497
+ }
498
+ /**
499
+ * Create a new stylesheet for the registry.
500
+ * In adopted mode (ShadowRoot), creates a constructable CSSStyleSheet and
501
+ * pushes it to adoptedStyleSheets. Otherwise creates a <style> element.
502
+ */
503
+ createSheet(registry, root) {
504
+ if (registry.injectionMode === "adopted") {
505
+ const constructableSheet = new CSSStyleSheet();
506
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, constructableSheet];
507
+ const sheetInfo = {
508
+ sheet: null,
509
+ constructableSheet,
510
+ ruleCount: 0,
511
+ holes: []
512
+ };
513
+ registry.sheets.push(sheetInfo);
514
+ return sheetInfo;
515
+ }
516
+ const sheet = this.createStyleElement(root);
517
+ const textMode = this.config.forceTextInjection === true || sheet.sheet == null;
518
+ const sheetInfo = {
519
+ sheet,
520
+ ruleCount: 0,
521
+ holes: [],
522
+ textMode,
523
+ ...textMode ? { textRules: [] } : {}
524
+ };
525
+ registry.sheets.push(sheetInfo);
526
+ return sheetInfo;
527
+ }
528
+ /**
529
+ * Create a style element and append to document
530
+ */
531
+ createStyleElement(root, attribute = "data-tasty") {
532
+ const style = root.createElement?.("style") || document.createElement("style");
533
+ if (this.config.nonce) style.nonce = this.config.nonce;
534
+ style.setAttribute(attribute, "");
535
+ ("head" in root && root.head ? root.head : root).appendChild(style);
536
+ if (attribute === "data-tasty" && !style.isConnected && !this.config.forceTextInjection) console.error("[Tasty] SheetManager: style element failed to connect to the DOM.", {
537
+ parentNode: style.parentNode?.nodeName,
538
+ isConnected: style.isConnected
539
+ });
540
+ return style;
541
+ }
542
+ /** Append a rule to a text-mode sheet while preserving its index mirror. */
543
+ appendTextRule(sheet, ruleIndex, ruleText) {
544
+ this.trackTextRule(sheet, ruleIndex, ruleText);
545
+ const style = sheet.sheet;
546
+ style.textContent = (style.textContent || "") + "\n" + ruleText;
547
+ }
548
+ /**
549
+ * Insert CSS rules as a single block
550
+ */
551
+ insertRule(registry, flattenedRules, className, root) {
552
+ let targetSheet = this.findAvailableSheet(registry);
553
+ if (!targetSheet) targetSheet = this.createSheet(registry, root);
554
+ const sheetIndex = registry.sheets.indexOf(targetSheet);
555
+ try {
556
+ const groupedRules = /* @__PURE__ */ new Map();
557
+ for (const rule of flattenedRules) {
558
+ const key = `${rule.atRules?.join("|") ?? ""}||${rule.selector}||${rule.startingStyle ? "1" : "0"}`;
559
+ const existing = groupedRules.get(key);
560
+ if (existing) existing.declarations = existing.declarations ? `${existing.declarations} ${rule.declarations}` : rule.declarations;
561
+ else groupedRules.set(key, {
562
+ selector: rule.selector,
563
+ atRules: rule.atRules,
564
+ startingStyle: rule.startingStyle,
565
+ declarations: rule.declarations
566
+ });
567
+ }
568
+ const insertedRuleTexts = [];
569
+ const insertedIndices = [];
570
+ let currentRuleIndex = this.findAvailableRuleIndex(targetSheet);
571
+ let firstInsertedIndex = null;
572
+ let lastInsertedIndex = null;
573
+ const recordInsertion = (index) => {
574
+ targetSheet.ruleCount++;
575
+ insertedIndices.push(index);
576
+ if (firstInsertedIndex == null) firstInsertedIndex = index;
577
+ lastInsertedIndex = index;
578
+ currentRuleIndex = index + 1;
579
+ };
580
+ for (const rule of groupedRules.values()) {
581
+ const declarations = rule.declarations;
582
+ const innerContent = rule.startingStyle ? `@starting-style { ${declarations} }` : declarations;
583
+ const fullRule = wrapAtRules(`${rule.selector} { ${innerContent} }`, rule.atRules);
584
+ const styleElement = targetSheet.sheet;
585
+ const styleSheet = this.getCSSSheet(targetSheet);
586
+ if (!targetSheet.textMode && styleSheet) {
587
+ const maxIndex = styleSheet.cssRules.length;
588
+ const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
589
+ const safeIndex = Math.min(Math.max(0, atomicRuleIndex), maxIndex);
590
+ try {
591
+ styleSheet.insertRule(fullRule, safeIndex);
592
+ recordInsertion(safeIndex);
593
+ } catch (e) {
594
+ const selectors = splitSelectorsSafely(rule.selector);
595
+ if (selectors.length > 1) for (const sel of selectors) {
596
+ const singleRule = wrapAtRules(`${sel} { ${declarations} }`, rule.atRules);
597
+ try {
598
+ const maxIdx = styleSheet.cssRules.length;
599
+ const atomicIdx = this.findAvailableRuleIndex(targetSheet);
600
+ const idx = Math.min(Math.max(0, atomicIdx), maxIdx);
601
+ styleSheet.insertRule(singleRule, idx);
602
+ recordInsertion(idx);
603
+ } catch (singleErr) {
604
+ console.warn("[Tasty] Browser rejected CSS rule:", singleRule, singleErr);
605
+ }
606
+ }
607
+ else if (!(fullRule.startsWith("@property ") && !this.engineSupportsAtProperty(registry, styleSheet))) console.warn("[Tasty] Browser rejected CSS rule:", fullRule, e);
608
+ }
609
+ } else if (styleElement) {
610
+ const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
611
+ this.appendTextRule(targetSheet, atomicRuleIndex, fullRule);
612
+ recordInsertion(atomicRuleIndex);
613
+ }
614
+ if (styleElement && !styleElement.parentNode && !this.config.forceTextInjection) console.error("[Tasty] SheetManager: style element is not attached to the DOM; rules will not apply.", {
615
+ className,
616
+ ruleIndex: currentRuleIndex
617
+ });
618
+ if (this.config.devMode) {
619
+ insertedRuleTexts.push(fullRule);
620
+ try {
621
+ registry.ruleTextSet.add(fullRule);
622
+ } catch {}
623
+ }
624
+ }
625
+ if (insertedIndices.length === 0) return null;
626
+ return {
627
+ className,
628
+ ruleIndex: firstInsertedIndex ?? 0,
629
+ sheetIndex,
630
+ cssText: this.config.devMode ? insertedRuleTexts : void 0,
631
+ endRuleIndex: lastInsertedIndex ?? firstInsertedIndex ?? 0,
632
+ indices: insertedIndices
633
+ };
634
+ } catch (error) {
635
+ console.warn("[Tasty] Failed to insert CSS rules:", error, {
636
+ flattenedRules,
637
+ className
638
+ });
639
+ return null;
640
+ }
641
+ }
642
+ /**
643
+ * Insert global CSS rules
644
+ */
645
+ insertGlobalRule(registry, flattenedRules, globalKey, root) {
646
+ const ruleInfo = this.insertRule(registry, flattenedRules, globalKey, root);
647
+ if (ruleInfo) registry.globalRules.set(globalKey, ruleInfo);
648
+ return ruleInfo;
649
+ }
650
+ /**
651
+ * Delete a global CSS rule by key
652
+ */
653
+ deleteGlobalRule(registry, globalKey) {
654
+ const ruleInfo = registry.globalRules.get(globalKey);
655
+ if (!ruleInfo) return;
656
+ this.deleteRule(registry, ruleInfo);
657
+ registry.globalRules.delete(globalKey);
658
+ }
659
+ /**
660
+ * Adjust rule indices after deletion to account for shifting
661
+ */
662
+ adjustIndicesAfterDeletion(registry, sheetIndex, startIdx, endIdx, deleteCount, deletedRuleInfo, deletedIndices) {
663
+ try {
664
+ const sortedDeleted = deletedIndices && deletedIndices.length > 0 ? [...deletedIndices].sort((a, b) => a - b) : null;
665
+ const countDeletedBefore = (sorted, idx) => {
666
+ let shift = 0;
667
+ for (const delIdx of sorted) if (delIdx < idx) shift++;
668
+ else break;
669
+ return shift;
670
+ };
671
+ const adjustRuleInfo = (info) => {
672
+ if (info === deletedRuleInfo) return;
673
+ if (info.sheetIndex !== sheetIndex) return;
674
+ if (!info.indices || info.indices.length === 0) return;
675
+ if (sortedDeleted) info.indices = info.indices.map((idx) => {
676
+ return idx - countDeletedBefore(sortedDeleted, idx);
677
+ });
678
+ else info.indices = info.indices.map((idx) => idx > endIdx ? Math.max(0, idx - deleteCount) : idx);
679
+ if (info.indices.length > 0) {
680
+ info.ruleIndex = Math.min(...info.indices);
681
+ info.endRuleIndex = Math.max(...info.indices);
682
+ }
683
+ };
684
+ for (const info of registry.rules.values()) adjustRuleInfo(info);
685
+ for (const info of registry.globalRules.values()) adjustRuleInfo(info);
686
+ for (const entry of registry.keyframesCache.values()) {
687
+ const ki = entry.info;
688
+ if (ki.sheetIndex !== sheetIndex) continue;
689
+ if (sortedDeleted) {
690
+ const shift = countDeletedBefore(sortedDeleted, ki.ruleIndex);
691
+ if (shift > 0) ki.ruleIndex = Math.max(0, ki.ruleIndex - shift);
692
+ } else if (ki.ruleIndex > endIdx) ki.ruleIndex = Math.max(0, ki.ruleIndex - deleteCount);
693
+ }
694
+ } catch {}
695
+ }
696
+ /**
697
+ * Delete a CSS rule from the sheet
698
+ */
699
+ deleteRule(registry, ruleInfo) {
700
+ const sheet = registry.sheets[ruleInfo.sheetIndex];
701
+ if (!sheet) return;
702
+ try {
703
+ const texts = this.config.devMode && Array.isArray(ruleInfo.cssText) ? ruleInfo.cssText.slice() : [];
704
+ const styleSheet = this.getCSSSheet(sheet);
705
+ const indices = ruleInfo.indices;
706
+ if (sheet.textMode) {
707
+ let targetIndices;
708
+ if (indices?.length) targetIndices = indices;
709
+ else {
710
+ const startIdx = Math.max(0, ruleInfo.ruleIndex);
711
+ const endIdx = Math.min((sheet.textRules?.length ?? 0) - 1, Number.isFinite(ruleInfo.endRuleIndex) ? ruleInfo.endRuleIndex : startIdx);
712
+ targetIndices = [];
713
+ for (let idx = startIdx; idx <= endIdx; idx++) targetIndices.push(idx);
714
+ }
715
+ const deletedIndices = this.deleteTextRules(sheet, targetIndices);
716
+ if (deletedIndices.length > 0) {
717
+ sheet.ruleCount = Math.max(0, sheet.ruleCount - deletedIndices.length);
718
+ this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, Math.min(...deletedIndices), Math.max(...deletedIndices), deletedIndices.length, ruleInfo, deletedIndices);
719
+ }
720
+ } else if (styleSheet) {
721
+ const rules = styleSheet.cssRules;
722
+ if (indices?.length) {
723
+ const sortedIndices = [...indices].sort((a, b) => b - a);
724
+ const deletedIndices = [];
725
+ for (const idx of sortedIndices) if (idx >= 0 && idx < styleSheet.cssRules.length) try {
726
+ styleSheet.deleteRule(idx);
727
+ deletedIndices.push(idx);
728
+ } catch (e) {
729
+ console.warn(`[Tasty] Failed to delete rule at index ${idx}:`, e);
730
+ }
731
+ sheet.ruleCount = Math.max(0, sheet.ruleCount - deletedIndices.length);
732
+ if (deletedIndices.length > 0) this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, Math.min(...deletedIndices), Math.max(...deletedIndices), deletedIndices.length, ruleInfo, deletedIndices);
733
+ } else {
734
+ const startIdx = Math.max(0, ruleInfo.ruleIndex);
735
+ const endIdx = Math.min(rules.length - 1, Number.isFinite(ruleInfo.endRuleIndex) ? ruleInfo.endRuleIndex : startIdx);
736
+ if (Number.isFinite(startIdx) && endIdx >= startIdx) {
737
+ const deleteCount = endIdx - startIdx + 1;
738
+ for (let idx = endIdx; idx >= startIdx; idx--) {
739
+ if (idx < 0 || idx >= styleSheet.cssRules.length) continue;
740
+ styleSheet.deleteRule(idx);
741
+ }
742
+ sheet.ruleCount = Math.max(0, sheet.ruleCount - deleteCount);
743
+ this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, startIdx, endIdx, deleteCount, ruleInfo);
744
+ }
745
+ }
746
+ }
747
+ if (this.config.devMode && texts.length) try {
748
+ for (const text of texts) registry.ruleTextSet.delete(text);
749
+ } catch {}
750
+ } catch (error) {
751
+ console.warn("[Tasty] Failed to delete CSS rule:", error);
752
+ }
753
+ }
754
+ /**
755
+ * Find a sheet with available space or return null
756
+ */
757
+ findAvailableSheet(registry) {
758
+ const maxRules = this.config.maxRulesPerSheet;
759
+ if (!maxRules) return registry.sheets[registry.sheets.length - 1] || null;
760
+ for (const sheet of registry.sheets) if (sheet.ruleCount < maxRules) return sheet;
761
+ return null;
762
+ }
763
+ /**
764
+ * Find an available rule index in the sheet
765
+ */
766
+ findAvailableRuleIndex(sheet) {
767
+ return sheet.ruleCount;
768
+ }
769
+ /**
770
+ * Probe whether the underlying CSS engine supports `@property` at-rules.
771
+ * Result is cached per registry on `registry.atPropertySupported`.
772
+ *
773
+ * The probe inserts and immediately deletes a minimal known-valid rule
774
+ * (`@property --__tasty_probe__ { syntax: "*"; inherits: true; }`).
775
+ * Engines that lack `@property` support (jsdom, happy-dom) reject any
776
+ * `@property` rule including this one, so a probe failure is a reliable
777
+ * signal that further `@property` rejections are environmental noise and
778
+ * not user-authored bugs.
779
+ *
780
+ * The probe is intentionally a separate operation from the user's failing
781
+ * insertion: we don't want to leak `--__tasty_probe__` into the sheet, so
782
+ * on success we delete the probe rule immediately, leaving `ruleCount`
783
+ * and `cssRules.length` unchanged.
784
+ */
785
+ engineSupportsAtProperty(registry, styleSheet) {
786
+ if (registry.atPropertySupported !== void 0) return registry.atPropertySupported;
787
+ const probeRule = "@property --__tasty_probe__ { syntax: \"*\"; inherits: true; }";
788
+ try {
789
+ const probeIdx = styleSheet.cssRules.length;
790
+ styleSheet.insertRule(probeRule, probeIdx);
791
+ try {
792
+ styleSheet.deleteRule(probeIdx);
793
+ } catch {}
794
+ registry.atPropertySupported = true;
795
+ } catch {
796
+ registry.atPropertySupported = false;
797
+ }
798
+ return registry.atPropertySupported;
799
+ }
800
+ /**
801
+ * Delete the given classes: their rules leave the sheets and every registry
802
+ * entry pointing at them is dropped.
803
+ *
804
+ * Deciding *what* is unused belongs to `StyleInjector.gc()`, which owns the
805
+ * DOM scan and the capacity policy. This only re-checks that each class is
806
+ * still safe to delete, and reports how many were.
807
+ *
808
+ * @returns Number of classes actually deleted.
809
+ */
810
+ deleteClasses(registry, classNames) {
811
+ const cleanupStartTime = Date.now();
812
+ const selected = Array.from(classNames).map((className) => {
813
+ const ruleInfo = registry.rules.get(className);
814
+ return ruleInfo ? {
815
+ className,
816
+ ruleInfo
817
+ } : null;
818
+ }).filter((entry) => entry != null);
819
+ if (selected.length === 0) return 0;
820
+ const deleted = /* @__PURE__ */ new Set();
821
+ let totalCssSize = 0;
822
+ let totalRulesDeleted = 0;
823
+ const rulesBySheet = /* @__PURE__ */ new Map();
824
+ for (const { className, ruleInfo } of selected) {
825
+ const sheetIndex = ruleInfo.sheetIndex;
826
+ if (this.config.devMode && Array.isArray(ruleInfo.cssText)) {
827
+ const cssSize = ruleInfo.cssText.reduce((total, css) => total + css.length, 0);
828
+ totalCssSize += cssSize;
829
+ totalRulesDeleted += ruleInfo.cssText.length;
830
+ }
831
+ const rules = rulesBySheet.get(sheetIndex);
832
+ if (rules) rules.push({
833
+ className,
834
+ ruleInfo
835
+ });
836
+ else rulesBySheet.set(sheetIndex, [{
837
+ className,
838
+ ruleInfo
839
+ }]);
840
+ }
841
+ for (const rulesInSheet of rulesBySheet.values()) {
842
+ rulesInSheet.sort((a, b) => b.ruleInfo.ruleIndex - a.ruleInfo.ruleIndex);
843
+ for (const { className, ruleInfo } of rulesInSheet) {
844
+ if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
845
+ if (registry.rules.get(className) !== ruleInfo) continue;
846
+ const sheetInfo = registry.sheets[ruleInfo.sheetIndex];
847
+ if (!sheetInfo || !sheetInfo.sheet && !sheetInfo.constructableSheet) continue;
848
+ const styleSheet = this.getCSSSheet(sheetInfo);
849
+ if (!sheetInfo.textMode && !styleSheet) continue;
850
+ const maxRuleIndex = (sheetInfo.textMode ? sheetInfo.textRules?.length ?? 0 : styleSheet?.cssRules.length ?? 0) - 1;
851
+ const startIdx = ruleInfo.ruleIndex;
852
+ const endIdx = ruleInfo.endRuleIndex ?? ruleInfo.ruleIndex;
853
+ if (startIdx < 0 || endIdx > maxRuleIndex || startIdx > endIdx) continue;
854
+ this.deleteRule(registry, ruleInfo);
855
+ registry.rules.delete(className);
856
+ registry.pinCounts.delete(className);
857
+ registry.unusedSince.delete(className);
858
+ for (const [key, entry] of registry.localKeyframes) {
859
+ if (!entry.owners.delete(className)) continue;
860
+ if (entry.owners.size === 0) {
861
+ entry.dispose();
862
+ registry.localKeyframes.delete(key);
863
+ }
864
+ }
865
+ deleted.add(className);
866
+ }
867
+ }
868
+ if (deleted.size > 0) {
869
+ for (const [key, mappedClassName] of registry.cacheKeyToClassName) if (deleted.has(mappedClassName)) registry.cacheKeyToClassName.delete(key);
870
+ }
871
+ if (registry.metrics) {
872
+ registry.metrics.bulkCleanups++;
873
+ registry.metrics.stylesCleanedUp += deleted.size;
874
+ registry.metrics.cleanupHistory.push({
875
+ timestamp: cleanupStartTime,
876
+ classesDeleted: deleted.size,
877
+ cssSize: totalCssSize,
878
+ rulesDeleted: totalRulesDeleted
879
+ });
880
+ }
881
+ return deleted.size;
882
+ }
883
+ /**
884
+ * Get total number of rules across all sheets
885
+ */
886
+ getTotalRuleCount(registry) {
887
+ return registry.sheets.reduce((total, sheet) => total + sheet.ruleCount - sheet.holes.length, 0);
888
+ }
889
+ /**
890
+ * The CSS of one managed sheet, or `null` when it holds none this can read —
891
+ * an empty style element, or a sheet the engine refuses to hand over.
892
+ */
893
+ readSheetCSS(sheetInfo) {
894
+ try {
895
+ if (sheetInfo.sheet?.textContent) return sheetInfo.sheet.textContent;
896
+ const sheet = this.getCSSSheet(sheetInfo);
897
+ if (sheet) return rulesToCSS(sheet);
898
+ } catch (error) {
899
+ console.warn("[Tasty] Failed to read CSS from sheet:", error);
900
+ }
901
+ return null;
902
+ }
903
+ /**
904
+ * Get CSS text from all sheets (for SSR)
905
+ */
906
+ getCSSText(registry) {
907
+ const cssChunks = [];
908
+ for (const sheetInfo of registry.sheets) {
909
+ const css = this.readSheetCSS(sheetInfo);
910
+ if (css !== null) cssChunks.push(css);
911
+ }
912
+ return cssChunks.join("\n");
913
+ }
914
+ /**
915
+ * Get cache performance metrics
916
+ */
917
+ getMetrics(registry) {
918
+ if (!registry.metrics) return null;
919
+ return {
920
+ ...registry.metrics,
921
+ unusedHits: 0
922
+ };
923
+ }
924
+ /**
925
+ * Reset cache performance metrics
926
+ */
927
+ resetMetrics(registry) {
928
+ if (registry.metrics) registry.metrics = {
929
+ hits: 0,
930
+ misses: 0,
931
+ bulkCleanups: 0,
932
+ totalInsertions: 0,
933
+ totalUnused: 0,
934
+ stylesCleanedUp: 0,
935
+ cleanupHistory: [],
936
+ startTime: Date.now()
937
+ };
938
+ }
939
+ /**
940
+ * Convert keyframes steps to CSS string.
941
+ * Public so the SSR collector can format keyframes without DOM access.
942
+ * Returns both the CSS text and a combined declarations string for property type scanning.
943
+ */
944
+ stepsToCSS(steps) {
945
+ const rules = [];
946
+ const allDeclarations = [];
947
+ for (const [key, value] of Object.entries(steps)) {
948
+ if (typeof value === "string") {
949
+ rules.push(`${key} { ${value.trim()} }`);
950
+ allDeclarations.push(value.trim());
951
+ continue;
952
+ }
953
+ const styleMap = value || {};
954
+ const styleNames = Object.keys(styleMap).sort();
955
+ const handlerQueue = [];
956
+ const seenHandlers = /* @__PURE__ */ new Set();
957
+ styleNames.forEach((styleName) => {
958
+ let handlers = STYLE_HANDLER_MAP[styleName];
959
+ if (!handlers) handlers = STYLE_HANDLER_MAP[styleName] = [createStyle(styleName)];
960
+ handlers.forEach((handler) => {
961
+ if (!seenHandlers.has(handler)) {
962
+ seenHandlers.add(handler);
963
+ handlerQueue.push(handler);
964
+ }
965
+ });
966
+ });
967
+ const declarationPairs = [];
968
+ handlerQueue.forEach((handler) => {
969
+ const result = handler(handler.__lookupStyles.reduce((acc, name) => {
970
+ const v = styleMap[name];
971
+ if (v !== void 0) acc[name] = v;
972
+ return acc;
973
+ }, {}));
974
+ if (!result) return;
975
+ (Array.isArray(result) ? result : [result]).forEach((cssMap) => {
976
+ if (!cssMap || typeof cssMap !== "object") return;
977
+ const { $: _$, ...props } = cssMap;
978
+ Object.entries(props).forEach(([prop, val]) => {
979
+ if (val == null || val === "") return;
980
+ if (Array.isArray(val)) val.forEach((v) => {
981
+ if (v != null && v !== "") declarationPairs.push({
982
+ prop,
983
+ value: String(v)
984
+ });
985
+ });
986
+ else declarationPairs.push({
987
+ prop,
988
+ value: String(val)
989
+ });
990
+ });
991
+ });
992
+ });
993
+ const declarations = declarationPairs.map((d) => `${d.prop}: ${d.value}`).join("; ");
994
+ rules.push(`${key} { ${declarations.trim()} }`);
995
+ allDeclarations.push(declarations);
996
+ }
997
+ return {
998
+ css: rules.join(" "),
999
+ declarations: allDeclarations.join("; ")
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Insert keyframes rule.
1004
+ * Returns the KeyframesInfo and the raw declarations string for property type scanning.
1005
+ */
1006
+ insertKeyframes(registry, steps, name, root) {
1007
+ let targetSheet = this.findAvailableSheet(registry);
1008
+ if (!targetSheet) targetSheet = this.createSheet(registry, root);
1009
+ const ruleIndex = this.findAvailableRuleIndex(targetSheet);
1010
+ const sheetIndex = registry.sheets.indexOf(targetSheet);
1011
+ try {
1012
+ const { css: cssSteps, declarations } = this.stepsToCSS(steps);
1013
+ const fullRule = `@keyframes ${name} { ${cssSteps} }`;
1014
+ const styleSheet = this.getCSSSheet(targetSheet);
1015
+ if (!targetSheet.textMode && styleSheet) {
1016
+ const safeIndex = Math.min(Math.max(0, ruleIndex), styleSheet.cssRules.length);
1017
+ styleSheet.insertRule(fullRule, safeIndex);
1018
+ } else if (targetSheet.sheet) this.appendTextRule(targetSheet, ruleIndex, fullRule);
1019
+ targetSheet.ruleCount++;
1020
+ return {
1021
+ info: {
1022
+ name,
1023
+ ruleIndex,
1024
+ sheetIndex,
1025
+ cssText: this.config.devMode ? fullRule : void 0
1026
+ },
1027
+ declarations
1028
+ };
1029
+ } catch (error) {
1030
+ console.warn("[Tasty] Failed to insert keyframes:", error);
1031
+ return null;
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Delete keyframes rule
1036
+ */
1037
+ deleteKeyframes(registry, info) {
1038
+ const sheet = registry.sheets[info.sheetIndex];
1039
+ if (!sheet) return;
1040
+ try {
1041
+ const styleSheet = this.getCSSSheet(sheet);
1042
+ if (sheet.textMode) {
1043
+ if (!this.deleteTextRules(sheet, [info.ruleIndex]).length) return;
1044
+ } else {
1045
+ if (!styleSheet || info.ruleIndex < 0 || info.ruleIndex >= styleSheet.cssRules.length) return;
1046
+ styleSheet.deleteRule(info.ruleIndex);
1047
+ }
1048
+ sheet.ruleCount = Math.max(0, sheet.ruleCount - 1);
1049
+ this.adjustIndicesAfterDeletion(registry, info.sheetIndex, info.ruleIndex, info.ruleIndex, 1, null, [info.ruleIndex]);
1050
+ } catch (error) {
1051
+ console.warn("[Tasty] Failed to delete keyframes:", error);
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Clean up resources for a root
1056
+ */
1057
+ cleanup(root) {
1058
+ const registry = this.rootRegistries.get(root);
1059
+ if (!registry) return;
1060
+ if (registry.injectionMode === "adopted") {
1061
+ const shadowRoot = root;
1062
+ const ownedSheets = /* @__PURE__ */ new Set();
1063
+ for (const sheetInfo of registry.sheets) if (sheetInfo.constructableSheet) ownedSheets.add(sheetInfo.constructableSheet);
1064
+ const rawSheet = this.rawConstructableSheets.get(shadowRoot);
1065
+ if (rawSheet) {
1066
+ ownedSheets.add(rawSheet);
1067
+ this.rawConstructableSheets.delete(shadowRoot);
1068
+ }
1069
+ if (ownedSheets.size > 0) shadowRoot.adoptedStyleSheets = shadowRoot.adoptedStyleSheets.filter((s) => !ownedSheets.has(s));
1070
+ } else {
1071
+ for (const sheet of registry.sheets) try {
1072
+ const styleElement = sheet.sheet;
1073
+ if (styleElement?.parentNode) styleElement.parentNode.removeChild(styleElement);
1074
+ } catch (error) {
1075
+ console.warn("[Tasty] Failed to cleanup sheet:", error);
1076
+ }
1077
+ const rawStyleElement = this.rawStyleElements.get(root);
1078
+ if (rawStyleElement?.parentNode) rawStyleElement.parentNode.removeChild(rawStyleElement);
1079
+ this.rawStyleElements.delete(root);
1080
+ }
1081
+ this.rootRegistries.delete(root);
1082
+ this.activeRoots.delete(root);
1083
+ this.rawCSSBlocks.delete(root);
1084
+ }
1085
+ /**
1086
+ * Check if a root uses adopted injection mode.
1087
+ */
1088
+ isAdoptedMode(root) {
1089
+ const registry = this.rootRegistries.get(root);
1090
+ if (registry) return registry.injectionMode === "adopted";
1091
+ return this.detectInjectionMode(root) === "adopted";
1092
+ }
1093
+ /**
1094
+ * Get or create a constructable CSSStyleSheet for raw CSS in adopted mode.
1095
+ * The raw sheet is prepended to adoptedStyleSheets so it precedes tasty rules.
1096
+ */
1097
+ getOrCreateRawAdoptedSheet(root) {
1098
+ let sheet = this.rawConstructableSheets.get(root);
1099
+ if (!sheet) {
1100
+ sheet = new CSSStyleSheet();
1101
+ root.adoptedStyleSheets = [sheet, ...root.adoptedStyleSheets];
1102
+ this.rawConstructableSheets.set(root, sheet);
1103
+ if (!this.rawCSSBlocks.has(root)) this.rawCSSBlocks.set(root, /* @__PURE__ */ new Map());
1104
+ }
1105
+ return sheet;
1106
+ }
1107
+ /**
1108
+ * Get or create a dedicated style element for raw CSS
1109
+ * Raw CSS is kept separate from tasty-managed sheets to avoid index conflicts
1110
+ */
1111
+ getOrCreateRawStyleElement(root) {
1112
+ let styleElement = this.rawStyleElements.get(root);
1113
+ if (!styleElement) {
1114
+ styleElement = this.createStyleElement(root, "data-tasty-raw");
1115
+ this.rawStyleElements.set(root, styleElement);
1116
+ this.rawCSSBlocks.set(root, /* @__PURE__ */ new Map());
1117
+ }
1118
+ return styleElement;
1119
+ }
1120
+ /**
1121
+ * Inject raw CSS text directly without parsing
1122
+ * Returns a dispose function to remove the injected CSS
1123
+ */
1124
+ injectRawCSS(css, root) {
1125
+ if (!css.trim()) return { dispose: () => {} };
1126
+ const id = `raw_${this.rawCSSCounter++}`;
1127
+ if (this.isAdoptedMode(root)) {
1128
+ this.getOrCreateRawAdoptedSheet(root);
1129
+ const blocksMap = this.rawCSSBlocks.get(root);
1130
+ const info = {
1131
+ id,
1132
+ css,
1133
+ startOffset: 0,
1134
+ endOffset: css.length
1135
+ };
1136
+ blocksMap.set(id, info);
1137
+ this.rebuildRawAdoptedSheet(root);
1138
+ } else {
1139
+ const styleElement = this.getOrCreateRawStyleElement(root);
1140
+ const blocksMap = this.rawCSSBlocks.get(root);
1141
+ const currentContent = styleElement.textContent || "";
1142
+ const cssWithNewline = (currentContent ? "\n" : "") + css;
1143
+ styleElement.textContent = currentContent + cssWithNewline;
1144
+ blocksMap.set(id, {
1145
+ id,
1146
+ css,
1147
+ startOffset: currentContent.length,
1148
+ endOffset: currentContent.length + cssWithNewline.length
1149
+ });
1150
+ }
1151
+ return { dispose: () => {
1152
+ this.disposeRawCSS(id, root);
1153
+ } };
1154
+ }
1155
+ /**
1156
+ * Rebuild the raw CSS constructable sheet from all tracked blocks.
1157
+ */
1158
+ rebuildRawAdoptedSheet(root) {
1159
+ const sheet = this.rawConstructableSheets.get(root);
1160
+ const blocksMap = this.rawCSSBlocks.get(root);
1161
+ if (!sheet || !blocksMap) return;
1162
+ sheet.replaceSync(rawBlocksToCSS(blocksMap));
1163
+ }
1164
+ /**
1165
+ * Remove a raw CSS block by ID
1166
+ */
1167
+ disposeRawCSS(id, root) {
1168
+ const blocksMap = this.rawCSSBlocks.get(root);
1169
+ if (!blocksMap?.delete(id)) return;
1170
+ if (this.isAdoptedMode(root)) {
1171
+ this.rebuildRawAdoptedSheet(root);
1172
+ return;
1173
+ }
1174
+ const styleElement = this.rawStyleElements.get(root);
1175
+ if (!styleElement) return;
1176
+ const remainingBlocks = Array.from(blocksMap.values());
1177
+ if (remainingBlocks.length === 0) styleElement.textContent = "";
1178
+ else {
1179
+ remainingBlocks.sort((a, b) => a.startOffset - b.startOffset);
1180
+ styleElement.textContent = remainingBlocks.map((block) => block.css).join("\n");
1181
+ let offset = 0;
1182
+ for (const block of remainingBlocks) {
1183
+ block.startOffset = offset;
1184
+ block.endOffset = offset + block.css.length;
1185
+ offset = block.endOffset + 1;
1186
+ }
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Get the raw CSS content
1191
+ */
1192
+ /**
1193
+ * The CSS this injector owns in `root`, one entry per sheet, in the order the
1194
+ * engine applies them.
1195
+ *
1196
+ * `getCSSText()` walks the managed sheets only, and raw CSS has its own —
1197
+ * which sits before, after, or *between* them depending on how it got there:
1198
+ * prepended to `adoptedStyleSheets`, or wherever in `<head>` the first raw
1199
+ * injection happened to land, with every managed sheet opened afterwards
1200
+ * following it. Reporting a fixed order would describe the opposite winner
1201
+ * from the live page whenever two rules of equal specificity meet, so the raw
1202
+ * sheet is spliced in at the position the DOM actually gives it.
1203
+ */
1204
+ getOwnedCSSInOrder(registry, root) {
1205
+ const raw = this.getRawCSSText(root);
1206
+ const rawElement = this.isAdoptedMode(root) ? null : this.rawStyleElements.get(root);
1207
+ let rawIndex = raw && !rawElement ? 0 : -1;
1208
+ const chunks = [];
1209
+ for (const sheetInfo of registry.sheets) {
1210
+ const css = this.readSheetCSS(sheetInfo);
1211
+ if (css === null) continue;
1212
+ if (rawIndex < 0 && rawElement && sheetInfo.sheet && rawElement.compareDocumentPosition(sheetInfo.sheet) & Node.DOCUMENT_POSITION_FOLLOWING) rawIndex = chunks.length;
1213
+ chunks.push(css);
1214
+ }
1215
+ if (!raw) return chunks;
1216
+ chunks.splice(rawIndex < 0 ? chunks.length : rawIndex, 0, raw);
1217
+ return chunks;
1218
+ }
1219
+ /**
1220
+ * Top-level rules in the raw sheet.
1221
+ *
1222
+ * Read from the sheet the engine parsed, not from the text: a raw block is
1223
+ * one string but any number of rules, and one rule — `@keyframes`, `@media` —
1224
+ * can contain any number of blocks, so counting braces answers neither
1225
+ * question.
1226
+ */
1227
+ getRawRuleCount(root) {
1228
+ const sheet = this.isAdoptedMode(root) ? this.rawConstructableSheets.get(root) : this.rawStyleElements.get(root)?.sheet;
1229
+ try {
1230
+ return sheet?.cssRules.length ?? 0;
1231
+ } catch {
1232
+ return 0;
1233
+ }
1234
+ }
1235
+ getRawCSSText(root) {
1236
+ if (this.isAdoptedMode(root)) {
1237
+ const blocksMap = this.rawCSSBlocks.get(root);
1238
+ if (!blocksMap || blocksMap.size === 0) return "";
1239
+ return rawBlocksToCSS(blocksMap);
1240
+ }
1241
+ return this.rawStyleElements.get(root)?.textContent || "";
1242
+ }
1243
+ };
1244
+ //#endregion
1245
+ //#region src/injector/injector.ts
1246
+ /**
1247
+ * How long a class is left alone after it was last known to be wanted.
1248
+ * Long enough that a render cannot plausibly still be waiting to commit one.
1249
+ */
1250
+ const DEFAULT_GC_GRACE = 1e4;
1251
+ /** Dispose handle handed back for injections that took no pin. */
1252
+ const noop = () => {};
1253
+ /**
1254
+ * Placeholder `KeyframesInfo` for a cache entry reserved by a batched write and
1255
+ * not yet backed by a real rule. Only ever read through `entry.pending`, which
1256
+ * is checked before the info is used.
1257
+ */
1258
+ const EMPTY_KEYFRAMES_INFO = {
1259
+ name: "",
1260
+ ruleIndex: -3,
1261
+ sheetIndex: -3
1262
+ };
1263
+ /**
1264
+ * Extract class names from `<style data-tasty-rsc>` tags.
1265
+ * The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
1266
+ */
1267
+ function extractRSCClassNames(rscClassRegex) {
1268
+ if (typeof document === "undefined") return [];
1269
+ const styles = document.querySelectorAll("style[data-tasty-rsc]");
1270
+ if (styles.length === 0) return [];
1271
+ const classSet = /* @__PURE__ */ new Set();
1272
+ for (const style of styles) {
1273
+ const text = style.textContent;
1274
+ if (!text) continue;
1275
+ let match;
1276
+ rscClassRegex.lastIndex = 0;
1277
+ while ((match = rscClassRegex.exec(text)) !== null) classSet.add(match[1]);
1278
+ }
1279
+ return Array.from(classSet);
1280
+ }
1281
+ /**
1282
+ * Lazily sync server-rendered class names into the client registry.
1283
+ *
1284
+ * Sources:
1285
+ * 1. `window.__TASTY__` — pushed by SSR/RSC streaming scripts
1286
+ * 2. `<style data-tasty-rsc>` tags — inline CSS emitted by RSC components
1287
+ *
1288
+ * Called inside `inject()` / `allocateClassName()` to pick up
1289
+ * class names rendered on the server (including during SPA navigation).
1290
+ */
1291
+ function syncServerClasses(registry, rscClassRegex) {
1292
+ if (typeof window === "undefined") return;
1293
+ const classes = window.__TASTY__;
1294
+ if (classes && classes.length > registry.serverClassSyncIndex) {
1295
+ for (let i = registry.serverClassSyncIndex; i < classes.length; i++) registerHydratedClass(registry, classes[i]);
1296
+ registry.serverClassSyncIndex = classes.length;
1297
+ }
1298
+ if (!registry.rscStylesScanned) {
1299
+ registry.rscStylesScanned = true;
1300
+ for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
1301
+ }
1302
+ }
1303
+ function registerHydratedClass(registry, className) {
1304
+ if (registry.rules.has(className)) return;
1305
+ registry.rules.set(className, {
1306
+ className,
1307
+ ruleIndex: -2,
1308
+ sheetIndex: -2
1309
+ });
1310
+ registry.pinCounts.set(className, 0);
1311
+ }
1312
+ var StyleInjector = class {
1313
+ sheetManager;
1314
+ config;
1315
+ globalRuleCounter = 0;
1316
+ /** Cancels the scheduled sweep, whichever timer scheduled it. */
1317
+ cancelPendingGC = null;
1318
+ namePrefix;
1319
+ classRegex;
1320
+ rscClassRegex;
1321
+ /** @internal — exposed for debug utilities only */
1322
+ get _sheetManager() {
1323
+ return this.sheetManager;
1324
+ }
1325
+ /** Register inferable custom properties used by a set of declarations. */
1326
+ inferProperties(registry, rules, root) {
1327
+ if (this.config.autoPropertyTypes === false) return;
1328
+ const resolver = registry.propertyTypeResolver;
1329
+ const defined = registry.injectedProperties;
1330
+ for (const rule of rules) {
1331
+ if (!rule.declarations) continue;
1332
+ resolver.scanDeclarations(rule.declarations, (name) => defined.has(name), (name, syntax, initialValue) => {
1333
+ this.property(name, {
1334
+ syntax,
1335
+ inherits: true,
1336
+ initialValue,
1337
+ root
1338
+ });
1339
+ });
1340
+ }
1341
+ }
1342
+ /**
1343
+ * Whether sheet writes should be queued instead of applied immediately.
1344
+ *
1345
+ * Only ever true on the client: SSR collects CSS as text and the RSC path
1346
+ * returns it as strings, so neither has a live sheet to batch writes against.
1347
+ *
1348
+ * In the default `true` mode a write is only queued inside an open batch
1349
+ * window — a commit in which `<TastyBatchProvider>` rendered and will
1350
+ * therefore flush in its insertion effect, before any layout effect can
1351
+ * measure. Everything else falls through to a synchronous write, so enabling
1352
+ * the flag cannot make a `useLayoutEffect` read an unstyled element.
1353
+ * `'always'` drops the gate and accepts that trade for wider coverage.
1354
+ */
1355
+ get batching() {
1356
+ const mode = this.config.batchInjection;
1357
+ if (!mode || typeof document === "undefined") return false;
1358
+ if (mode === "always") return true;
1359
+ if (isBatchWindowOpen()) return true;
1360
+ warnBatchProviderMissing();
1361
+ return false;
1362
+ }
1363
+ /**
1364
+ * Apply a sheet write now, or queue it when batching is on.
1365
+ * Returns the queue handle when deferred, so the caller can cancel it if it
1366
+ * disposes before the flush.
1367
+ */
1368
+ writeSheet(task) {
1369
+ if (this.batching) return enqueueStyleWrite(task);
1370
+ task();
1371
+ return null;
1372
+ }
1373
+ /**
1374
+ * Insert a global (non-class) rule, honouring batching.
1375
+ *
1376
+ * `onApplied` records the caller's dedupe bookkeeping. When the write is
1377
+ * queued it runs eagerly, so a repeat call before the flush bails out instead
1378
+ * of queueing the same rule twice — the same reasoning as
1379
+ * `insertPropertyRule`'s eager marking. When written synchronously it runs
1380
+ * only on success, preserving the existing retry-on-failure behaviour.
1381
+ */
1382
+ writeGlobalRule(registry, rules, key, root, onApplied) {
1383
+ if (this.batching) {
1384
+ onApplied?.();
1385
+ enqueueStyleWrite(() => {
1386
+ this.sheetManager.insertGlobalRule(registry, rules, key, root);
1387
+ });
1388
+ return;
1389
+ }
1390
+ if (this.sheetManager.insertGlobalRule(registry, rules, key, root)) onApplied?.();
1391
+ }
1392
+ constructor(config = {}) {
1393
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
1394
+ this.config = config;
1395
+ this.sheetManager = new SheetManager(config);
1396
+ this.namePrefix = config.namePrefix ?? "t";
1397
+ this.classRegex = tastyClassRegex(this.namePrefix);
1398
+ this.rscClassRegex = rscClassRegexGlobal(this.namePrefix);
1399
+ }
1400
+ /**
1401
+ * Generate a deterministic class name from a cache key using content hash.
1402
+ * The same cache key always produces the same class name across environments
1403
+ * with the same `namePrefix`.
1404
+ */
1405
+ generateClassName(cacheKey) {
1406
+ return makeClassName(this.namePrefix, hashString(cacheKey));
1407
+ }
1408
+ /**
1409
+ * Check if `className` was hydrated from server-rendered styles and,
1410
+ * if so, wire the cacheKey mapping. Returns true on hit.
1411
+ */
1412
+ tryHydratedHit(registry, cacheKey, className) {
1413
+ syncServerClasses(registry, this.rscClassRegex);
1414
+ const rule = registry.rules.get(className);
1415
+ if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
1416
+ registry.cacheKeyToClassName.set(cacheKey, className);
1417
+ return true;
1418
+ }
1419
+ return false;
1420
+ }
1421
+ /**
1422
+ * Allocate a className for a cacheKey without injecting styles yet.
1423
+ * This allows separating className allocation (render phase) from style injection (insertion phase).
1424
+ */
1425
+ allocateClassName(cacheKey, options) {
1426
+ const root = options?.root || document;
1427
+ const registry = this.sheetManager.getRegistry(root);
1428
+ if (registry.cacheKeyToClassName.has(cacheKey)) return {
1429
+ className: registry.cacheKeyToClassName.get(cacheKey),
1430
+ isNewAllocation: false
1431
+ };
1432
+ const className = this.generateClassName(cacheKey);
1433
+ if (this.tryHydratedHit(registry, cacheKey, className)) return {
1434
+ className,
1435
+ isNewAllocation: false
1436
+ };
1437
+ if (registry.rules.get(className)) {
1438
+ if (isDevEnv()) console.warn(`[Tasty] Hash collision: cache keys produce the same class "${className}". Styles may be incorrect.`);
1439
+ registry.cacheKeyToClassName.set(cacheKey, className);
1440
+ return {
1441
+ className,
1442
+ isNewAllocation: false
1443
+ };
1444
+ }
1445
+ const placeholderRuleInfo = {
1446
+ className,
1447
+ ruleIndex: -1,
1448
+ sheetIndex: -1
1449
+ };
1450
+ registry.rules.set(className, placeholderRuleInfo);
1451
+ registry.cacheKeyToClassName.set(cacheKey, className);
1452
+ return {
1453
+ className,
1454
+ isNewAllocation: true
1455
+ };
1456
+ }
1457
+ /**
1458
+ * Inject styles from StyleResult objects
1459
+ */
1460
+ inject(rules, options) {
1461
+ const root = options?.root || document;
1462
+ const registry = this.sheetManager.getRegistry(root);
1463
+ const pin = options?.pin ?? true;
1464
+ if (rules.length === 0) return {
1465
+ className: "",
1466
+ dispose: () => {}
1467
+ };
1468
+ const cacheKey = options?.cacheKey;
1469
+ let className;
1470
+ if (cacheKey && registry.cacheKeyToClassName.has(cacheKey)) {
1471
+ className = registry.cacheKeyToClassName.get(cacheKey);
1472
+ const existingRuleInfo = registry.rules.get(className);
1473
+ if (existingRuleInfo.ruleIndex === -3) {
1474
+ registry.unusedSince.delete(className);
1475
+ if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
1476
+ if (registry.metrics) registry.metrics.hits++;
1477
+ return {
1478
+ className,
1479
+ dispose: pin ? () => this.unpin(className, registry) : noop
1480
+ };
1481
+ }
1482
+ if (!(existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1)) {
1483
+ registry.unusedSince.delete(className);
1484
+ if (pin) {
1485
+ const pins = registry.pinCounts.get(className) || 0;
1486
+ registry.pinCounts.set(className, pins + 1);
1487
+ }
1488
+ if (registry.metrics) registry.metrics.hits++;
1489
+ return {
1490
+ className,
1491
+ dispose: pin ? () => this.unpin(className, registry) : noop
1492
+ };
1493
+ }
1494
+ } else if (cacheKey) {
1495
+ className = this.generateClassName(cacheKey);
1496
+ if (this.tryHydratedHit(registry, cacheKey, className)) {
1497
+ if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
1498
+ if (registry.metrics) registry.metrics.hits++;
1499
+ return {
1500
+ className,
1501
+ dispose: pin ? () => this.unpin(className, registry) : noop
1502
+ };
1503
+ }
1504
+ } else {
1505
+ const parts = rules.map((r) => `${r.selector}\0${r.declarations}`);
1506
+ className = makeClassName(this.namePrefix, hashString(parts.join("\n")));
1507
+ }
1508
+ const rulesToInsert = rules.map((rule) => {
1509
+ let newSelector = rule.selector;
1510
+ if (rule.needsClassName) {
1511
+ const selectorParts = newSelector ? newSelector.split("|||") : [""];
1512
+ const classPrefix = `.${className}.${className}`;
1513
+ newSelector = selectorParts.map((part) => {
1514
+ const classSelector = part ? `${classPrefix}${part}` : classPrefix;
1515
+ if (rule.rootPrefix) return `${rule.rootPrefix} ${classSelector}`;
1516
+ return classSelector;
1517
+ }).join(", ");
1518
+ }
1519
+ return {
1520
+ ...rule,
1521
+ selector: newSelector,
1522
+ needsClassName: void 0,
1523
+ rootPrefix: void 0
1524
+ };
1525
+ });
1526
+ const applySheetWrite = () => {
1527
+ this.inferProperties(registry, rulesToInsert, root);
1528
+ const ruleInfo = this.sheetManager.insertRule(registry, rulesToInsert, className, root);
1529
+ if (!ruleInfo) {
1530
+ if (registry.metrics) registry.metrics.misses++;
1531
+ return null;
1532
+ }
1533
+ registry.rules.set(className, ruleInfo);
1534
+ if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
1535
+ if (registry.metrics) {
1536
+ registry.metrics.totalInsertions++;
1537
+ registry.metrics.misses++;
1538
+ }
1539
+ return ruleInfo;
1540
+ };
1541
+ if (this.batching) {
1542
+ registry.rules.set(className, {
1543
+ className,
1544
+ ruleIndex: -3,
1545
+ sheetIndex: -3
1546
+ });
1547
+ if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
1548
+ if (pin) registry.pinCounts.set(className, 1);
1549
+ registry.unusedSince.set(className, Date.now());
1550
+ const queued = enqueueStyleWrite(() => {
1551
+ if (applySheetWrite()) return;
1552
+ registry.rules.set(className, {
1553
+ className,
1554
+ ruleIndex: -1,
1555
+ sheetIndex: -1
1556
+ });
1557
+ });
1558
+ return {
1559
+ className,
1560
+ dispose: pin ? () => {
1561
+ if (!queued.done && registry.pinCounts.get(className) === 1 && registry.rules.get(className)?.ruleIndex === -3) {
1562
+ queued.cancelled = true;
1563
+ registry.rules.delete(className);
1564
+ if (cacheKey) registry.cacheKeyToClassName.delete(cacheKey);
1565
+ registry.pinCounts.set(className, 0);
1566
+ return;
1567
+ }
1568
+ this.unpin(className, registry);
1569
+ } : noop
1570
+ };
1571
+ }
1572
+ if (!applySheetWrite()) return {
1573
+ className,
1574
+ dispose: () => {}
1575
+ };
1576
+ if (pin) registry.pinCounts.set(className, 1);
1577
+ registry.unusedSince.set(className, Date.now());
1578
+ return {
1579
+ className,
1580
+ dispose: pin ? () => this.unpin(className, registry) : noop
1581
+ };
1582
+ }
1583
+ /**
1584
+ * Inject global styles (rules without a generated tasty class selector)
1585
+ * This ensures we don't reserve a tasty class name (t{number}) for global rules,
1586
+ * which could otherwise collide with element-level styles and break lookups.
1587
+ */
1588
+ injectGlobal(rules, options) {
1589
+ const root = options?.root || document;
1590
+ const registry = this.sheetManager.getRegistry(root);
1591
+ if (!rules || rules.length === 0) return { dispose: () => {} };
1592
+ const key = `global:${this.globalRuleCounter++}`;
1593
+ const applyWrite = () => {
1594
+ this.inferProperties(registry, rules, root);
1595
+ const inserted = this.sheetManager.insertGlobalRule(registry, rules, key, root);
1596
+ if (registry.metrics) registry.metrics.totalInsertions++;
1597
+ return inserted;
1598
+ };
1599
+ if (this.batching) {
1600
+ let info = null;
1601
+ const queued = enqueueStyleWrite(() => {
1602
+ info = applyWrite();
1603
+ });
1604
+ return { dispose: () => {
1605
+ if (!queued.done) {
1606
+ queued.cancelled = true;
1607
+ return;
1608
+ }
1609
+ if (info) this.sheetManager.deleteGlobalRule(registry, key);
1610
+ } };
1611
+ }
1612
+ const info = applyWrite();
1613
+ return { dispose: () => {
1614
+ if (info) this.sheetManager.deleteGlobalRule(registry, key);
1615
+ } };
1616
+ }
1617
+ /**
1618
+ * Inject raw CSS text directly without parsing
1619
+ * This is a low-overhead alternative to createGlobalStyle for raw CSS
1620
+ * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking
1621
+ */
1622
+ injectRawCSS(css, options) {
1623
+ const root = options?.root || document;
1624
+ if (!this.batching) return this.sheetManager.injectRawCSS(css, root);
1625
+ let result = null;
1626
+ const queued = enqueueStyleWrite(() => {
1627
+ result = this.sheetManager.injectRawCSS(css, root);
1628
+ });
1629
+ return { dispose: () => {
1630
+ if (!queued.done) {
1631
+ queued.cancelled = true;
1632
+ return;
1633
+ }
1634
+ result?.dispose();
1635
+ } };
1636
+ }
1637
+ /**
1638
+ * Get raw CSS text for SSR
1639
+ */
1640
+ getRawCSSText(options) {
1641
+ flushStyles();
1642
+ const root = options?.root || document;
1643
+ return this.sheetManager.getRawCSSText(root);
1644
+ }
1645
+ /**
1646
+ * Take a reference on local `@keyframes`, under the deterministic names the
1647
+ * caller resolved.
1648
+ *
1649
+ * One reference per distinct set of steps, shared by every class that ends up
1650
+ * animating it, so a repeat render takes nothing further. Ownership is
1651
+ * assigned by `ownKeyframes()` once the rules exist to be inspected.
1652
+ */
1653
+ holdKeyframes(steps, names, options) {
1654
+ const root = options?.root || document;
1655
+ const registry = this.sheetManager.getRegistry(root);
1656
+ const keys = /* @__PURE__ */ new Map();
1657
+ for (const [authored, definition] of Object.entries(steps)) {
1658
+ const resolved = names.get(authored) ?? authored;
1659
+ let entry = registry.localKeyframes.get(resolved);
1660
+ if (!entry) {
1661
+ const injected = this.keyframes(definition, {
1662
+ name: resolved,
1663
+ root,
1664
+ distinctByName: true
1665
+ });
1666
+ entry = {
1667
+ name: injected.toString(),
1668
+ dispose: injected.dispose,
1669
+ owners: /* @__PURE__ */ new Set()
1670
+ };
1671
+ registry.localKeyframes.set(resolved, entry);
1672
+ }
1673
+ keys.set(authored, resolved);
1674
+ }
1675
+ return keys;
1676
+ }
1677
+ /**
1678
+ * Record that `className` animates these keyframes, so the reference is
1679
+ * released when the last such class is collected.
1680
+ *
1681
+ * Only classes whose rules actually reference the animation should be here:
1682
+ * a class that merely rendered alongside one would otherwise keep the
1683
+ * keyframes alive for as long as it lives.
1684
+ */
1685
+ ownKeyframes(key, className, options) {
1686
+ this.sheetManager.getRegistry(options?.root || document).localKeyframes.get(key)?.owners.add(className);
1687
+ }
1688
+ /**
1689
+ * Pin an already-injected cacheKey and return the handle that releases it.
1690
+ * For callers that skipped the pipeline on a cache hit but still need the
1691
+ * class held. Returns null if the cacheKey is not found.
1692
+ */
1693
+ trackRef(cacheKey, options) {
1694
+ const root = options?.root || document;
1695
+ const registry = this.sheetManager.getRegistry(root);
1696
+ if (!registry.cacheKeyToClassName.has(cacheKey)) return null;
1697
+ const className = registry.cacheKeyToClassName.get(cacheKey);
1698
+ const pins = registry.pinCounts.get(className) || 0;
1699
+ registry.pinCounts.set(className, pins + 1);
1700
+ if (registry.metrics) registry.metrics.hits++;
1701
+ return {
1702
+ className,
1703
+ dispose: () => this.unpin(className, registry)
1704
+ };
1705
+ }
1706
+ /**
1707
+ * Release one pin on a className. At zero pins the class is not deleted — it
1708
+ * stays cached and collectible, and `gc()` decides when it actually goes.
1709
+ */
1710
+ unpin(className, registry) {
1711
+ const pins = registry.pinCounts.get(className);
1712
+ if (pins == null || pins <= 0) return;
1713
+ const remaining = pins - 1;
1714
+ registry.pinCounts.set(className, remaining);
1715
+ if (remaining === 0 && registry.metrics) registry.metrics.totalUnused++;
1716
+ }
1717
+ /**
1718
+ * Remove every style that is neither in the DOM nor referenced by an
1719
+ * outstanding `inject()` handle, ignoring the GC capacity threshold.
1720
+ */
1721
+ cleanup(root) {
1722
+ this.gc({
1723
+ root,
1724
+ force: true
1725
+ });
1726
+ }
1727
+ /**
1728
+ * Get CSS text from all sheets (for SSR)
1729
+ */
1730
+ getCSSText(options) {
1731
+ flushStyles();
1732
+ const root = options?.root || document;
1733
+ const registry = this.sheetManager.getRegistry(root);
1734
+ return this.sheetManager.getCSSText(registry);
1735
+ }
1736
+ /**
1737
+ * Get CSS only for the provided tasty classNames (e.g., ["t0","t3"])
1738
+ */
1739
+ getCSSTextForClasses(classNames, options) {
1740
+ flushStyles();
1741
+ const root = options?.root || document;
1742
+ const registry = this.sheetManager.getRegistry(root);
1743
+ const cssChunks = [];
1744
+ for (const cls of classNames) {
1745
+ const info = registry.rules.get(cls);
1746
+ if (info) {
1747
+ const sheet = registry.sheets[info.sheetIndex];
1748
+ const styleSheet = sheet ? this.sheetManager.getCSSSheet(sheet) : null;
1749
+ if (styleSheet) {
1750
+ const start = Math.max(0, info.ruleIndex);
1751
+ const end = Math.min(styleSheet.cssRules.length - 1, info.endRuleIndex ?? info.ruleIndex);
1752
+ if (start >= 0 && end >= start && start < styleSheet.cssRules.length) for (let i = start; i <= end; i++) {
1753
+ const rule = styleSheet.cssRules[i];
1754
+ if (rule) cssChunks.push(rule.cssText);
1755
+ }
1756
+ } else if (info.cssText && info.cssText.length) cssChunks.push(...info.cssText);
1757
+ }
1758
+ }
1759
+ return cssChunks.join("\n");
1760
+ }
1761
+ /**
1762
+ * Get cache performance metrics
1763
+ */
1764
+ getMetrics(options) {
1765
+ flushStyles();
1766
+ const root = options?.root || document;
1767
+ const registry = this.sheetManager.getRegistry(root);
1768
+ const metrics = this.sheetManager.getMetrics(registry);
1769
+ if (metrics && typeof document !== "undefined") metrics.unusedHits = this.collectUnused(registry, root).length;
1770
+ return metrics;
1771
+ }
1772
+ /**
1773
+ * Reset cache performance metrics
1774
+ */
1775
+ resetMetrics(options) {
1776
+ const root = options?.root || document;
1777
+ const registry = this.sheetManager.getRegistry(root);
1778
+ this.sheetManager.resetMetrics(registry);
1779
+ }
1780
+ /**
1781
+ * Define a CSS @property custom property.
1782
+ *
1783
+ * Accepts tasty token syntax for the property name:
1784
+ * - `$name` → defines `--name`
1785
+ * - `#name` → defines `--name-color` (auto-sets syntax: '<color>', defaults initialValue: 'transparent')
1786
+ * - `--name` → defines `--name` (legacy format)
1787
+ *
1788
+ * Example:
1789
+ * @property --rotation { syntax: "<angle>"; inherits: false; initial-value: 45deg; }
1790
+ *
1791
+ * Note: No caching or dispose — this defines a global property.
1792
+ *
1793
+ * If the same property is registered with different options, a warning is emitted
1794
+ * but the original definition is preserved (CSS @property cannot be redefined).
1795
+ */
1796
+ property(name, options) {
1797
+ const root = options?.root || document;
1798
+ const registry = this.sheetManager.getRegistry(root);
1799
+ const effectiveResult = getEffectiveDefinition(name, {
1800
+ syntax: options?.syntax,
1801
+ inherits: options?.inherits,
1802
+ initialValue: options?.initialValue
1803
+ });
1804
+ if (!effectiveResult.isValid) {
1805
+ if (isDevEnv()) console.warn(`[Tasty] property(): ${effectiveResult.error}. Got: "${name}"`);
1806
+ return;
1807
+ }
1808
+ const cssName = effectiveResult.cssName;
1809
+ const definition = effectiveResult.definition;
1810
+ this.insertPropertyRule(registry, root, cssName, definition, name);
1811
+ }
1812
+ /**
1813
+ * Build and insert a single `@property` rule into the given registry.
1814
+ * No-op if the property was already injected.
1815
+ */
1816
+ insertPropertyRule(registry, root, cssName, definition, cacheKey) {
1817
+ if (registry.injectedProperties.has(cssName)) return;
1818
+ const parts = [];
1819
+ if (definition.syntax != null) {
1820
+ let syntax = String(definition.syntax).trim();
1821
+ if (!/^['"]/u.test(syntax)) syntax = `"${syntax}"`;
1822
+ parts.push(`syntax: ${syntax};`);
1823
+ }
1824
+ const inherits = definition.inherits ?? true;
1825
+ parts.push(`inherits: ${inherits ? "true" : "false"};`);
1826
+ if (definition.initialValue != null) {
1827
+ let initialValueStr;
1828
+ if (typeof definition.initialValue === "number") initialValueStr = String(definition.initialValue);
1829
+ else initialValueStr = parseStyle(definition.initialValue).output;
1830
+ parts.push(`initial-value: ${initialValueStr};`);
1831
+ }
1832
+ const declarations = parts.join(" ").trim();
1833
+ const rule = {
1834
+ selector: `@property ${cssName}`,
1835
+ declarations
1836
+ };
1837
+ registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
1838
+ this.writeGlobalRule(registry, [rule], `property:${cacheKey}`, root);
1839
+ }
1840
+ /**
1841
+ * Check whether a given @property name was already injected by this injector.
1842
+ *
1843
+ * Accepts tasty token syntax:
1844
+ * - `$name` → checks `--name`
1845
+ * - `#name` → checks `--name-color`
1846
+ * - `--name` → checks `--name` (legacy format)
1847
+ */
1848
+ isPropertyDefined(name, options) {
1849
+ flushStyles();
1850
+ const root = options?.root || document;
1851
+ const registry = this.sheetManager.getRegistry(root);
1852
+ const effectiveResult = getEffectiveDefinition(name, {});
1853
+ if (!effectiveResult.isValid) return false;
1854
+ return registry.injectedProperties.has(effectiveResult.cssName);
1855
+ }
1856
+ /**
1857
+ * Inject a CSS @font-face rule.
1858
+ *
1859
+ * Permanent and global — no dispose or ref-counting.
1860
+ * Deduplicates by content hash (family + descriptors).
1861
+ */
1862
+ fontFace(family, descriptors, options) {
1863
+ const root = options?.root || document;
1864
+ const registry = this.sheetManager.getRegistry(root);
1865
+ const hash = fontFaceContentHash(family, descriptors);
1866
+ if (registry.injectedFontFaces.has(hash)) return;
1867
+ const rule = {
1868
+ selector: "@font-face",
1869
+ declarations: formatFontFaceDeclarations(family, descriptors)
1870
+ };
1871
+ this.writeGlobalRule(registry, [rule], `fontface:${hash}`, root, () => {
1872
+ registry.injectedFontFaces.add(hash);
1873
+ });
1874
+ }
1875
+ /**
1876
+ * Inject a CSS @counter-style rule.
1877
+ *
1878
+ * Permanent and global — no dispose or ref-counting. Deduplicates by name.
1879
+ * By default a definition overrides a previously injected one of the same
1880
+ * name. Pass `weak: true` for global `configure()` definitions, which must
1881
+ * never clobber an existing rule (so component-local definitions win
1882
+ * regardless of injection order).
1883
+ */
1884
+ counterStyle(name, descriptors, options) {
1885
+ const root = options?.root || document;
1886
+ const registry = this.sheetManager.getRegistry(root);
1887
+ const isWeak = options?.weak === true;
1888
+ const existingIsStrong = registry.injectedCounterStyles.get(name);
1889
+ if (existingIsStrong !== void 0) {
1890
+ if (isWeak || existingIsStrong === true) return;
1891
+ this.writeSheet(() => {
1892
+ this.sheetManager.deleteGlobalRule(registry, `counterstyle:${name}`);
1893
+ });
1894
+ }
1895
+ const rule = {
1896
+ selector: `@counter-style ${name}`,
1897
+ declarations: formatCounterStyleDeclarations(descriptors)
1898
+ };
1899
+ this.writeGlobalRule(registry, [rule], `counterstyle:${name}`, root, () => {
1900
+ registry.injectedCounterStyles.set(name, !isWeak);
1901
+ });
1902
+ }
1903
+ /**
1904
+ * Inject a CSS @function rule (custom function).
1905
+ *
1906
+ * Permanent and global — no dispose or ref-counting. Deduplicates by function
1907
+ * name. By default a definition overrides a previously injected one of the
1908
+ * same name. Pass `weak: true` for global `configure()` definitions, which
1909
+ * must never clobber an existing rule (so component-local definitions win
1910
+ * regardless of injection order).
1911
+ */
1912
+ func(name, definition, options) {
1913
+ const root = options?.root || document;
1914
+ const registry = this.sheetManager.getRegistry(root);
1915
+ const isWeak = options?.weak === true;
1916
+ const cssName = parseFunctionName(name);
1917
+ const existingIsStrong = registry.injectedFunctions.get(cssName);
1918
+ if (existingIsStrong !== void 0) {
1919
+ if (isWeak || existingIsStrong === true) return;
1920
+ this.writeSheet(() => {
1921
+ this.sheetManager.deleteGlobalRule(registry, `function:${cssName}`);
1922
+ });
1923
+ }
1924
+ const rule = {
1925
+ selector: formatFunctionPrelude(name, definition.args, definition.returns),
1926
+ declarations: formatFunctionDeclarations(definition)
1927
+ };
1928
+ this.writeGlobalRule(registry, [rule], `function:${cssName}`, root, () => {
1929
+ registry.injectedFunctions.set(cssName, !isWeak);
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Inject keyframes and return object with toString() and dispose()
1934
+ *
1935
+ * Keyframes are cached by content (steps). If the same content is injected
1936
+ * multiple times with different provided names, the first injected name is reused.
1937
+ *
1938
+ * If the same name is provided with different content (collision), a unique
1939
+ * name is generated to avoid overwriting the existing keyframes.
1940
+ */
1941
+ keyframes(steps, nameOrOptions) {
1942
+ const isStringName = typeof nameOrOptions === "string";
1943
+ const providedName = isStringName ? nameOrOptions : nameOrOptions?.name;
1944
+ const distinctByName = isStringName ? false : nameOrOptions?.distinctByName ?? false;
1945
+ const root = isStringName ? document : nameOrOptions?.root || document;
1946
+ const registry = this.sheetManager.getRegistry(root);
1947
+ if (Object.keys(steps).length === 0) return {
1948
+ toString: () => "",
1949
+ dispose: () => {}
1950
+ };
1951
+ const contentHash = distinctByName && providedName ? `${providedName}\u0000${JSON.stringify(steps)}` : JSON.stringify(steps);
1952
+ const existing = registry.keyframesCache.get(contentHash);
1953
+ if (existing) {
1954
+ existing.refCount++;
1955
+ return {
1956
+ toString: () => existing.name,
1957
+ dispose: () => this.disposeKeyframes(contentHash, registry)
1958
+ };
1959
+ }
1960
+ let actualName;
1961
+ if (providedName) {
1962
+ const existingContentForName = registry.keyframesNameToContent.get(providedName);
1963
+ if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
1964
+ else {
1965
+ actualName = providedName;
1966
+ registry.keyframesNameToContent.set(providedName, contentHash);
1967
+ }
1968
+ } else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
1969
+ const applyWrite = () => {
1970
+ const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
1971
+ if (!result) return false;
1972
+ const { info, declarations } = result;
1973
+ if (this.config.autoPropertyTypes !== false && declarations) registry.propertyTypeResolver.scanDeclarations(declarations, (name) => registry.injectedProperties.has(name), (name, syntax, initialValue) => {
1974
+ this.property(name, {
1975
+ syntax,
1976
+ inherits: true,
1977
+ initialValue,
1978
+ root
1979
+ });
1980
+ });
1981
+ const entry = registry.keyframesCache.get(contentHash);
1982
+ if (entry) {
1983
+ entry.info = info;
1984
+ entry.pending = void 0;
1985
+ } else registry.keyframesCache.set(contentHash, {
1986
+ name: actualName,
1987
+ refCount: 1,
1988
+ info
1989
+ });
1990
+ if (registry.metrics) {
1991
+ registry.metrics.totalInsertions++;
1992
+ registry.metrics.misses++;
1993
+ }
1994
+ return true;
1995
+ };
1996
+ if (this.batching) {
1997
+ const entry = {
1998
+ name: actualName,
1999
+ refCount: 1,
2000
+ info: EMPTY_KEYFRAMES_INFO
2001
+ };
2002
+ registry.keyframesCache.set(contentHash, entry);
2003
+ entry.pending = enqueueStyleWrite(() => {
2004
+ if (applyWrite()) return;
2005
+ if (registry.keyframesCache.get(contentHash) === entry) registry.keyframesCache.delete(contentHash);
2006
+ });
2007
+ return {
2008
+ toString: () => actualName,
2009
+ dispose: () => this.disposeKeyframes(contentHash, registry)
2010
+ };
2011
+ }
2012
+ if (!applyWrite()) return {
2013
+ toString: () => "",
2014
+ dispose: () => {}
2015
+ };
2016
+ return {
2017
+ toString: () => actualName,
2018
+ dispose: () => this.disposeKeyframes(contentHash, registry)
2019
+ };
2020
+ }
2021
+ /**
2022
+ * Dispose keyframes
2023
+ */
2024
+ disposeKeyframes(contentHash, registry) {
2025
+ const entry = registry.keyframesCache.get(contentHash);
2026
+ if (!entry) return;
2027
+ entry.refCount--;
2028
+ if (entry.refCount <= 0) {
2029
+ if (entry.pending && !entry.pending.done) entry.pending.cancelled = true;
2030
+ else this.sheetManager.deleteKeyframes(registry, entry.info);
2031
+ registry.keyframesCache.delete(contentHash);
2032
+ for (const [name, hash] of registry.keyframesNameToContent.entries()) if (hash === contentHash) {
2033
+ registry.keyframesNameToContent.delete(name);
2034
+ break;
2035
+ }
2036
+ if (registry.metrics) {
2037
+ registry.metrics.totalUnused++;
2038
+ registry.metrics.stylesCleanedUp++;
2039
+ }
2040
+ }
2041
+ }
2042
+ /**
2043
+ * Count a render, and schedule a collection pass every `touchInterval` of
2044
+ * them. Nothing else: what a class is worth keeping is decided by the sweep's
2045
+ * own DOM scan, so rendering does not track usage at all.
2046
+ *
2047
+ * @deprecated The class name is ignored — pass anything, or stop calling it.
2048
+ * Collection no longer records per-class usage, and scheduling does not need
2049
+ * to know which class was rendered. A class handed back by `inject()` is
2050
+ * marked wanted there, which is what reuse actually goes through.
2051
+ */
2052
+ touch(_className, options) {
2053
+ if (typeof document === "undefined") return;
2054
+ if (!this.config.gc) return;
2055
+ const registry = this.sheetManager.getRegistry(options?.root || document);
2056
+ if (++registry.touchCount >= (this.config.gc.touchInterval ?? 1e3)) {
2057
+ registry.touchCount = 0;
2058
+ this.scheduleGC();
2059
+ }
2060
+ }
2061
+ /**
2062
+ * Schedule a GC in idle time. Runs GC on all active roots, and avoids
2063
+ * double-scheduling.
2064
+ *
2065
+ * Idle only. Without `requestIdleCallback` nothing is collected
2066
+ * automatically: running the sweep inline here would put it inside the render
2067
+ * that touched the class, and collection is never urgent enough for that.
2068
+ */
2069
+ scheduleGC() {
2070
+ if (this.cancelPendingGC) return;
2071
+ const runGC = () => {
2072
+ this.cancelPendingGC = null;
2073
+ this.sheetManager.pruneDisconnectedRoots();
2074
+ for (const root of this.sheetManager.getActiveRoots()) this.gc({ root });
2075
+ };
2076
+ if (typeof requestIdleCallback !== "undefined") {
2077
+ const handle = requestIdleCallback(() => runGC());
2078
+ this.cancelPendingGC = () => cancelIdleCallback(handle);
2079
+ }
2080
+ }
2081
+ /**
2082
+ * The one definition of "unused": this injector owns the class's rules, no
2083
+ * element in `root` carries it, and nobody pinned it.
2084
+ *
2085
+ * `gc()` evicts from this set, `getMetrics()` counts it, and `tastyDebug`
2086
+ * reports it — all through here, so the three can never disagree about what
2087
+ * "unused" means. Entries carry their last `touch()` so callers can drop the
2088
+ * oldest first; a class that was never touched sorts as oldest.
2089
+ */
2090
+ /**
2091
+ * Everything the injector holds falls into one of five bands, and only the
2092
+ * last of them is ever deleted:
2093
+ *
2094
+ * 1. rendered — some element carries the class right now
2095
+ * 2. not ours — queued for a batched write, pre-allocated, or server-rendered
2096
+ * 3. hot — nothing carries it, but that was noticed less than `grace` ago
2097
+ * 4. cached — cold, but within `capacity` when ordered by when it went cold
2098
+ * 5. the rest
2099
+ *
2100
+ * This returns bands 4 and 5 together, most recently cold first; `gc()` draws
2101
+ * the capacity line through them. Band 3 is what makes collection safe
2102
+ * without a commit signal: a render can resolve a class and commit it a
2103
+ * little later, and during that gap nothing on the page carries it.
2104
+ */
2105
+ collectUnused(registry, root) {
2106
+ const now = Date.now();
2107
+ const grace = this.config.gc?.grace ?? DEFAULT_GC_GRACE;
2108
+ const liveClasses = /* @__PURE__ */ new Set();
2109
+ for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
2110
+ const unused = [];
2111
+ for (const [className, ruleInfo] of registry.rules) {
2112
+ if (ruleInfo.sheetIndex < 0) continue;
2113
+ if (liveClasses.has(className)) {
2114
+ registry.unusedSince.delete(className);
2115
+ continue;
2116
+ }
2117
+ if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
2118
+ let since = registry.unusedSince.get(className);
2119
+ if (since === void 0) {
2120
+ since = now;
2121
+ registry.unusedSince.set(className, now);
2122
+ }
2123
+ if (now - since < grace) continue;
2124
+ unused.push(className);
2125
+ }
2126
+ unused.sort((a, b) => (registry.unusedSince.get(b) ?? 0) - (registry.unusedSince.get(a) ?? 0));
2127
+ return unused;
2128
+ }
2129
+ /**
2130
+ * Class names this injector holds CSS for that nothing renders and nobody
2131
+ * pinned — exactly what `gc({ force: true })` would delete. Unordered.
2132
+ */
2133
+ getUnusedClasses(options) {
2134
+ flushStyles();
2135
+ if (typeof document === "undefined") return [];
2136
+ const root = options?.root || document;
2137
+ const registry = this.sheetManager.getRegistry(root);
2138
+ return this.collectUnused(registry, root);
2139
+ }
2140
+ /**
2141
+ * Synchronous garbage collection.
2142
+ *
2143
+ * 1. Quick upper-bound check: skip if the registry is smaller than capacity.
2144
+ * 2. Scans the DOM for live tasty classNames — the DOM, not a ref count, is
2145
+ * what says a class rendered by a component is still in use.
2146
+ * 3. With `force: true`: deletes every unused class.
2147
+ * Without `force`: keeps the `capacity` most recently touched and deletes
2148
+ * the rest, oldest first.
2149
+ *
2150
+ * @returns Number of styles evicted.
2151
+ */
2152
+ gc(options) {
2153
+ flushStyles();
2154
+ if (typeof document === "undefined") return 0;
2155
+ this.cancelPendingGC?.();
2156
+ this.cancelPendingGC = null;
2157
+ const root = options?.root || document;
2158
+ const force = options?.force;
2159
+ const registry = this.sheetManager.getRegistry(root);
2160
+ const capacity = this.config.gc?.capacity ?? 1e3;
2161
+ if (!force && registry.rules.size <= capacity) return 0;
2162
+ const unused = this.collectUnused(registry, root);
2163
+ let doomed;
2164
+ if (force) doomed = unused;
2165
+ else if (unused.length > capacity) doomed = unused.slice(capacity);
2166
+ else return 0;
2167
+ if (doomed.length === 0) return 0;
2168
+ return this.sheetManager.deleteClasses(registry, doomed);
2169
+ }
2170
+ /**
2171
+ * Destroy all resources for a root
2172
+ */
2173
+ destroy(root) {
2174
+ flushStyles();
2175
+ const targetRoot = root || document;
2176
+ this.sheetManager.cleanup(targetRoot);
2177
+ if (this.cancelPendingGC && !this.sheetManager.hasActiveRoots()) {
2178
+ this.cancelPendingGC();
2179
+ this.cancelPendingGC = null;
2180
+ }
2181
+ }
2182
+ };
2183
+ //#endregion
2184
+ //#region src/config.ts
2185
+ /**
2186
+ * Tasty Configuration Module
2187
+ *
2188
+ * Centralizes all tasty configuration, including:
2189
+ * - Style injector settings (nonce, cleanup thresholds, etc.)
2190
+ * - Global predefined states for advanced state mapping
2191
+ * - stylesGenerated flag that locks configuration after first style generation
2192
+ *
2193
+ * Configuration must be done BEFORE any styles are generated.
2194
+ * After the first `inject()` call, configuration is locked and attempts to
2195
+ * reconfigure will emit a warning and be ignored.
2196
+ */
2197
+ const emittedWarnings = /* @__PURE__ */ new Set();
2198
+ const devMode = isDevEnv();
2199
+ /**
2200
+ * Emit a warning only once
2201
+ */
2202
+ function warnOnce(key, message) {
2203
+ if (devMode && !emittedWarnings.has(key)) {
2204
+ emittedWarnings.add(key);
2205
+ console.warn(message);
2206
+ }
2207
+ }
2208
+ let currentConfig = null;
2209
+ let globalKeyframes = null;
2210
+ let globalRecipes = null;
2211
+ const GLOBAL_INJECTOR_KEY = "__TASTY_GLOBAL_INJECTOR__";
2212
+ /**
2213
+ * Detect if we're running in a test environment
2214
+ */
2215
+ function isTestEnvironment() {
2216
+ if (typeof global !== "undefined") {
2217
+ const g = global;
2218
+ if (g.vi || g.jest || g.expect || g.describe || g.it) return true;
2219
+ }
2220
+ if (typeof window !== "undefined") {
2221
+ const ua = window.navigator?.userAgent;
2222
+ if (ua?.includes("jsdom") || ua?.includes("HappyDOM")) return true;
2223
+ }
2224
+ if (typeof globalThis !== "undefined") {
2225
+ const gt = globalThis;
2226
+ if (gt.vitest || gt.mocha) return true;
2227
+ }
2228
+ return false;
2229
+ }
2230
+ /**
2231
+ * Create default configuration with optional test environment detection
2232
+ */
2233
+ function createDefaultConfig(isTest) {
2234
+ return {
2235
+ maxRulesPerSheet: 8192,
2236
+ forceTextInjection: isTest ?? false,
2237
+ devMode: isDevEnv(),
2238
+ namePrefix: "t"
2239
+ };
2240
+ }
2241
+ /**
2242
+ * Mark that styles have been generated (called by injector on first inject)
2243
+ * This locks the configuration - no further changes allowed.
2244
+ * Also injects internal and global properties.
2245
+ */
2246
+ function markStylesGenerated() {
2247
+ if (!markStylesGeneratedState()) return;
2248
+ if (typeof document !== "undefined" && document.querySelector("[data-tasty-ssr]")) {
2249
+ warnOnce("ssr-globals-skip", "[Tasty] SSR styles detected — skipping client-side global CSS injection to avoid duplicates.");
2250
+ return;
2251
+ }
2252
+ const injector = getGlobalInjector();
2253
+ const globalFontFaces = getGlobalFontFaces();
2254
+ const globalCounterStyles = getGlobalCounterStyles();
2255
+ const globalFunctions = getGlobalFunctions();
2256
+ const globalTokens = getGlobalConfigTokens();
2257
+ const configuredGlobalStyles = getGlobalStyles();
2258
+ for (const [token, definition] of Object.entries(getEffectiveProperties())) injector.property(token, definition);
2259
+ if (globalFontFaces && Object.keys(globalFontFaces).length > 0) for (const [family, input] of Object.entries(globalFontFaces)) {
2260
+ const descriptors = Array.isArray(input) ? input : [input];
2261
+ for (const desc of descriptors) injector.fontFace(family, desc);
2262
+ }
2263
+ if (globalCounterStyles && Object.keys(globalCounterStyles).length > 0) for (const [name, descriptors] of Object.entries(globalCounterStyles)) injector.counterStyle(name, descriptors, { weak: true });
2264
+ if (globalFunctions && Object.keys(globalFunctions).length > 0) for (const [name, definition] of Object.entries(globalFunctions)) injector.func(name, definition, { weak: true });
2265
+ if (globalTokens && Object.keys(globalTokens).length > 0) {
2266
+ const tokenRules = renderStyles(globalTokens, ":root");
2267
+ if (tokenRules.length > 0) injector.injectGlobal(tokenRules);
2268
+ }
2269
+ if (configuredGlobalStyles) {
2270
+ for (const [selector, styles] of Object.entries(configuredGlobalStyles)) if (Object.keys(styles).length > 0) {
2271
+ const rules = renderStyles(styles, selector);
2272
+ if (rules.length > 0) injector.injectGlobal(rules);
2273
+ }
2274
+ }
2275
+ }
2276
+ let _hasGlobalKeyframes = false;
2277
+ /**
2278
+ * Check if any global keyframes are configured.
2279
+ * Uses a pre-computed flag to avoid Object.keys() allocation on every call.
2280
+ */
2281
+ function hasGlobalKeyframes() {
2282
+ return _hasGlobalKeyframes;
2283
+ }
2284
+ /**
2285
+ * Get global keyframes configuration.
2286
+ * Returns null if no keyframes configured (fast path for zero-overhead).
2287
+ */
2288
+ function getGlobalKeyframes() {
2289
+ return globalKeyframes;
2290
+ }
2291
+ /**
2292
+ * Set global keyframes (called from configure).
2293
+ * Internal use only.
2294
+ */
2295
+ function setGlobalKeyframes(keyframes) {
2296
+ if (hasStylesGenerated()) {
2297
+ warnOnce("keyframes-after-styles", "[Tasty] Cannot update keyframes after styles have been generated.\nThe new keyframes will be ignored.");
2298
+ return;
2299
+ }
2300
+ globalKeyframes = {
2301
+ ...globalKeyframes ?? {},
2302
+ ...keyframes
2303
+ };
2304
+ _hasGlobalKeyframes = Object.keys(globalKeyframes).length > 0;
2305
+ }
2306
+ /**
2307
+ * Set global properties (called from configure).
2308
+ * Internal use only.
2309
+ */
2310
+ function setGlobalProperties(properties) {
2311
+ if (hasStylesGenerated()) {
2312
+ warnOnce("properties-after-styles", "[Tasty] Cannot update properties after styles have been generated.\nThe new properties will be ignored.");
2313
+ return;
2314
+ }
2315
+ mergeGlobalProperties(properties);
2316
+ }
2317
+ /**
2318
+ * Set global font faces (called from configure).
2319
+ * Internal use only.
2320
+ */
2321
+ function setGlobalFontFace(fontFace) {
2322
+ if (hasStylesGenerated()) {
2323
+ warnOnce("fontface-after-styles", "[Tasty] Cannot update fontFaces after styles have been generated.\nThe new font faces will be ignored.");
2324
+ return;
2325
+ }
2326
+ mergeGlobalFontFaces(fontFace);
2327
+ }
2328
+ /**
2329
+ * Set global counter styles (called from configure).
2330
+ * Internal use only.
2331
+ */
2332
+ function setGlobalCounterStyle(counterStyle) {
2333
+ if (hasStylesGenerated()) {
2334
+ warnOnce("counterstyle-after-styles", "[Tasty] Cannot update counterStyles after styles have been generated.\nThe new counter styles will be ignored.");
2335
+ return;
2336
+ }
2337
+ mergeGlobalCounterStyles(counterStyle);
2338
+ }
2339
+ /**
2340
+ * Set global functions (called from configure).
2341
+ * Internal use only.
2342
+ */
2343
+ function setGlobalFunction(functions) {
2344
+ if (hasStylesGenerated()) {
2345
+ warnOnce("function-after-styles", "[Tasty] Cannot update functions after styles have been generated.\nThe new functions will be ignored.");
2346
+ return;
2347
+ }
2348
+ mergeGlobalFunctions(functions);
2349
+ }
2350
+ /**
2351
+ * Check if any global recipes are configured.
2352
+ * Fast path: returns false if no recipes were ever set.
2353
+ */
2354
+ function hasGlobalRecipes() {
2355
+ return globalRecipes !== null && Object.keys(globalRecipes).length > 0;
2356
+ }
2357
+ /**
2358
+ * Get global recipes configuration.
2359
+ * Returns null if no recipes configured (fast path for zero-overhead).
2360
+ */
2361
+ function getGlobalRecipes() {
2362
+ return globalRecipes;
2363
+ }
2364
+ /**
2365
+ * Set global recipes (called from configure).
2366
+ * Internal use only.
2367
+ */
2368
+ function setGlobalRecipes(recipes) {
2369
+ if (hasStylesGenerated()) {
2370
+ warnOnce("recipes-after-styles", "[Tasty] Cannot update recipes after styles have been generated.\nThe new recipes will be ignored.");
2371
+ return;
2372
+ }
2373
+ if (devMode) for (const [name, recipeStyles] of Object.entries(recipes)) {
2374
+ if (name === "none") warnOnce("recipe-reserved-none", "[Tasty] Recipe name \"none\" is reserved. It is used as a keyword meaning \"no base recipes\" (e.g. recipe: 'none / post-recipe'). Choose a different name for your recipe.");
2375
+ for (const key of Object.keys(recipeStyles)) {
2376
+ if (isSelector(key)) warnOnce(`recipe-selector-${name}-${key}`, `[Tasty] Recipe "${name}" contains sub-element key "${key}". Recipes must be flat styles without sub-element keys. Remove the sub-element key from the recipe definition.`);
2377
+ if (key === "recipe") warnOnce(`recipe-recursive-${name}`, `[Tasty] Recipe "${name}" contains a "recipe" key. Recipes cannot reference other recipes. Use space-separated names for composition: recipe: 'base elevated'.`);
2378
+ }
2379
+ }
2380
+ globalRecipes = {
2381
+ ...globalRecipes ?? {},
2382
+ ...recipes
2383
+ };
2384
+ }
2385
+ /**
2386
+ * Set global token styles (called from configure).
2387
+ * Internal use only.
2388
+ */
2389
+ function setGlobalConfigTokens(styles) {
2390
+ if (hasStylesGenerated()) {
2391
+ warnOnce("tokens-after-styles", "[Tasty] Cannot update tokens after styles have been generated.\nThe new tokens will be ignored.");
2392
+ return;
2393
+ }
2394
+ mergeGlobalConfigTokens(styles);
2395
+ }
2396
+ /**
2397
+ * Set configured global styles (called from configure).
2398
+ * Internal use only.
2399
+ */
2400
+ function setGlobalStyles(styles) {
2401
+ if (hasStylesGenerated()) {
2402
+ warnOnce("globalStyles-after-styles", "[Tasty] Cannot update globalStyles after styles have been generated.\nThe new global styles will be ignored.");
2403
+ return;
2404
+ }
2405
+ mergeGlobalStyles(styles);
2406
+ }
2407
+ /**
2408
+ * Check if configuration is locked (styles have been generated)
2409
+ */
2410
+ function isConfigLocked() {
2411
+ return hasStylesGenerated();
2412
+ }
2413
+ /**
2414
+ * Configure the Tasty style system.
2415
+ *
2416
+ * Must be called BEFORE any styles are generated (before first render that uses tasty).
2417
+ * After styles are generated, configuration is locked and calls to configure() will
2418
+ * emit a warning and be ignored.
2419
+ *
2420
+ * @example
2421
+ * ```ts
2422
+ * import { configure } from '@tenphi/tasty';
2423
+ *
2424
+ * // Configure before app renders
2425
+ * configure({
2426
+ * nonce: 'abc123',
2427
+ * states: {
2428
+ * '@mobile': '@media(w < 768px)',
2429
+ * '@dark': '@root(theme=dark)',
2430
+ * },
2431
+ * });
2432
+ * ```
2433
+ */
2434
+ function configure(config = {}) {
2435
+ if (hasStylesGenerated()) {
2436
+ warnOnce("configure-after-styles", "[Tasty] Cannot call configure() after styles have been generated.\nConfiguration must be done before the first render. The configuration will be ignored.");
2437
+ return;
2438
+ }
2439
+ if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
2440
+ const normalized = normalizeConfig(config);
2441
+ const { propHandlers: mergedPropHandlers, propHandlerSources, baseStyleProps: mergedBaseStyleProps, properties: mergedProperties, keyframes: mergedKeyframes, fontFaces: mergedFontFaces, counterStyles: mergedCounterStyles, tokens: mergedConfigTokens, recipes: mergedRecipes, globalStyles: mergedGlobalStyles } = normalized;
2442
+ const functionDefs = applyStyleConfig(config, normalized, warnOnce);
2443
+ if (!isFunctionsPolyfillEnabled() && Object.keys(functionDefs).length > 0) setGlobalFunction(functionDefs);
2444
+ if (Object.keys(mergedKeyframes).length > 0) setGlobalKeyframes(mergedKeyframes);
2445
+ if (Object.keys(mergedProperties).length > 0) setGlobalProperties(mergedProperties);
2446
+ if (Object.keys(mergedFontFaces).length > 0) setGlobalFontFace(mergedFontFaces);
2447
+ if (Object.keys(mergedCounterStyles).length > 0) setGlobalCounterStyle(mergedCounterStyles);
2448
+ if (Object.keys(mergedPropHandlers).length > 0) for (const [name, definition] of Object.entries(mergedPropHandlers)) registerPropHandler(name, definition, { source: propHandlerSources.get(name) });
2449
+ if (mergedBaseStyleProps.length > 0) registerBaseStyleProps(mergedBaseStyleProps);
2450
+ if (Object.keys(mergedConfigTokens).length > 0) setGlobalConfigTokens(mergedConfigTokens);
2451
+ if (Object.keys(mergedRecipes).length > 0) setGlobalRecipes(mergedRecipes);
2452
+ if (Object.keys(mergedGlobalStyles).length > 0) setGlobalStyles(mergedGlobalStyles);
2453
+ const { states: _states, parserCacheSize: _parserCacheSize, units: _units, functions: _functions, polyfills: _polyfills, plugins: _plugins, keyframes: _keyframes, properties: _properties, fontFaces: _fontFaces, counterStyles: _counterStyles, handlers: _handlers, propHandlers: _propHandlers, baseStyleProps: _baseStyleProps, tokens: _tokens, replaceTokens: _replaceTokens, recipes: _recipes, colorSpace: _colorSpace, presets: _presets, globalStyles: _globalStyles, ...injectorConfig } = config;
2454
+ const fullConfig = {
2455
+ ...createDefaultConfig(),
2456
+ ...currentConfig,
2457
+ ...injectorConfig
2458
+ };
2459
+ currentConfig = fullConfig;
2460
+ setRuntimeConfigState(fullConfig);
2461
+ const storage = typeof window !== "undefined" ? window : globalThis;
2462
+ storage[GLOBAL_INJECTOR_KEY] = new StyleInjector(fullConfig);
2463
+ }
2464
+ /**
2465
+ * Get the current configuration.
2466
+ * If not configured, returns default configuration.
2467
+ */
2468
+ function getConfig() {
2469
+ if (currentConfig) return currentConfig;
2470
+ const defaultConfig = createDefaultConfig(isTestEnvironment());
2471
+ currentConfig = defaultConfig;
2472
+ setRuntimeConfigState(defaultConfig);
2473
+ return defaultConfig;
2474
+ }
2475
+ /**
2476
+ * Get the configured prefix used for every generated identifier
2477
+ * (class names, keyframe names, counter-style names).
2478
+ *
2479
+ * Falls back to the default prefix (`'t'`) when `configure()` has not
2480
+ * been called yet — this matches the auto-configuration behavior used
2481
+ * by the rest of the system.
2482
+ */
2483
+ function getNamePrefix() {
2484
+ return currentConfig?.namePrefix ?? "t";
2485
+ }
2486
+ /**
2487
+ * Get the global injector instance.
2488
+ * Auto-configures with defaults if not already configured.
2489
+ */
2490
+ function getGlobalInjector() {
2491
+ const storage = typeof window !== "undefined" ? window : globalThis;
2492
+ if (!storage[GLOBAL_INJECTOR_KEY]) configure();
2493
+ return storage[GLOBAL_INJECTOR_KEY];
2494
+ }
2495
+ /**
2496
+ * Reset configuration (for testing only).
2497
+ * Clears the global injector and allows reconfiguration.
2498
+ */
2499
+ function resetConfig() {
2500
+ resetStylesGeneratedState();
2501
+ currentConfig = null;
2502
+ globalKeyframes = null;
2503
+ _hasGlobalKeyframes = false;
2504
+ resetGlobalPolyfillsState();
2505
+ globalRecipes = null;
2506
+ resetConfigResources();
2507
+ resetGlobalPredefinedTokens();
2508
+ resetGlobalParseFunctions();
2509
+ resetFunctionPolyfills();
2510
+ resetHandlers();
2511
+ resetStyleChunks();
2512
+ resetPropHandlers();
2513
+ resetBaseStyleProps();
2514
+ clearPipelineCache();
2515
+ emittedWarnings.clear();
2516
+ resetStyleWarnings();
2517
+ const storage = typeof window !== "undefined" ? window : globalThis;
2518
+ delete storage[GLOBAL_INJECTOR_KEY];
2519
+ }
2520
+ //#endregion
2521
+ //#region src/injector/chunk-sheet-registry.ts
2522
+ /**
2523
+ * Global registry mapping CSS content hashes to shared constructable
2524
+ * CSSStyleSheet objects with reference counting.
2525
+ *
2526
+ * Multiple shadow roots adopting the same chunk share a single underlying
2527
+ * stylesheet object — parse once, adopt everywhere.
2528
+ */
2529
+ var ChunkSheetRegistry = class {
2530
+ sheets = /* @__PURE__ */ new Map();
2531
+ sheetToHash = /* @__PURE__ */ new WeakMap();
2532
+ /**
2533
+ * Get or create a CSSStyleSheet for the given CSS text.
2534
+ * Increments refCount. Uses content hash as the dedup key.
2535
+ */
2536
+ acquire(cssText) {
2537
+ const hash = hashString(cssText);
2538
+ const existing = this.sheets.get(hash);
2539
+ if (existing) {
2540
+ existing.refCount++;
2541
+ return existing.sheet;
2542
+ }
2543
+ const sheet = new CSSStyleSheet();
2544
+ sheet.replaceSync(cssText);
2545
+ const entry = {
2546
+ sheet,
2547
+ cssText,
2548
+ refCount: 1
2549
+ };
2550
+ this.sheets.set(hash, entry);
2551
+ this.sheetToHash.set(sheet, hash);
2552
+ return sheet;
2553
+ }
2554
+ /**
2555
+ * Decrement refCount for a sheet. When refCount reaches 0,
2556
+ * the sheet is removed from the registry.
2557
+ */
2558
+ release(sheet) {
2559
+ const hash = this.sheetToHash.get(sheet);
2560
+ if (!hash) return;
2561
+ const entry = this.sheets.get(hash);
2562
+ if (!entry) return;
2563
+ entry.refCount--;
2564
+ if (entry.refCount <= 0) {
2565
+ this.sheets.delete(hash);
2566
+ this.sheetToHash.delete(sheet);
2567
+ }
2568
+ }
2569
+ /**
2570
+ * Bulk acquire — returns an array of CSSStyleSheet in the same order.
2571
+ */
2572
+ acquireAll(cssTexts) {
2573
+ return cssTexts.map((text) => this.acquire(text));
2574
+ }
2575
+ /**
2576
+ * Bulk release — decrements refCount for each sheet.
2577
+ */
2578
+ releaseAll(sheets) {
2579
+ for (const sheet of sheets) this.release(sheet);
2580
+ }
2581
+ /** Number of unique sheets currently held. */
2582
+ get size() {
2583
+ return this.sheets.size;
2584
+ }
2585
+ };
2586
+ /** Module-level singleton shared across the entire application. */
2587
+ const chunkSheetRegistry = new ChunkSheetRegistry();
2588
+ //#endregion
2589
+ //#region src/injector/index.ts
2590
+ /**
2591
+ * Inject styles and return className with dispose function
2592
+ */
2593
+ function inject(rules, options) {
2594
+ const injector = getGlobalInjector();
2595
+ markStylesGenerated();
2596
+ return injector.inject(rules, options);
2597
+ }
2598
+ /**
2599
+ * Inject global rules that should not reserve tasty class names
2600
+ */
2601
+ function injectGlobal(rules, options) {
2602
+ return getGlobalInjector().injectGlobal(rules, options);
2603
+ }
2604
+ /**
2605
+ * Inject raw CSS text directly without parsing
2606
+ * This is a low-overhead method for injecting raw CSS that doesn't need tasty processing.
2607
+ * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking.
2608
+ *
2609
+ * @example
2610
+ * ```tsx
2611
+ * // Inject raw CSS
2612
+ * const { dispose } = injectRawCSS(`
2613
+ * body { margin: 0; padding: 0; }
2614
+ * .my-class { color: red; }
2615
+ * `);
2616
+ *
2617
+ * // Later, remove the injected CSS
2618
+ * dispose();
2619
+ * ```
2620
+ */
2621
+ function injectRawCSS(css, options) {
2622
+ return getGlobalInjector().injectRawCSS(css, options);
2623
+ }
2624
+ /**
2625
+ * Get raw CSS text for SSR
2626
+ */
2627
+ function getRawCSSText(options) {
2628
+ return getGlobalInjector().getRawCSSText(options);
2629
+ }
2630
+ /**
2631
+ * Inject keyframes and return object with toString() and dispose()
2632
+ */
2633
+ function keyframes(steps, nameOrOptions) {
2634
+ return getGlobalInjector().keyframes(steps, nameOrOptions);
2635
+ }
2636
+ /**
2637
+ * Define a CSS @property custom property.
2638
+ * This enables advanced features like animating custom properties.
2639
+ *
2640
+ * Note: @property rules are global and persistent once defined.
2641
+ * Re-registering the same property name is a no-op.
2642
+ *
2643
+ * @param name - The custom property name (must start with --)
2644
+ * @param options - Property configuration
2645
+ *
2646
+ * @example
2647
+ * ```ts
2648
+ * // Define a color property that can be animated
2649
+ * property('--my-color', {
2650
+ * syntax: '<color>',
2651
+ * initialValue: 'red',
2652
+ * });
2653
+ *
2654
+ * // Define an angle property
2655
+ * property('--rotation', {
2656
+ * syntax: '<angle>',
2657
+ * inherits: false,
2658
+ * initialValue: '0deg',
2659
+ * });
2660
+ * ```
2661
+ */
2662
+ function property(name, options) {
2663
+ return getGlobalInjector().property(name, options);
2664
+ }
2665
+ /**
2666
+ * Check if a CSS @property has already been defined
2667
+ *
2668
+ * @param name - The custom property name to check
2669
+ * @param options - Options including root
2670
+ */
2671
+ function isPropertyDefined(name, options) {
2672
+ return getGlobalInjector().isPropertyDefined(name, options);
2673
+ }
2674
+ /**
2675
+ * Inject a CSS @font-face rule.
2676
+ *
2677
+ * Permanent and global — no dispose or ref-counting.
2678
+ * Deduplicates by content hash (family + descriptors).
2679
+ */
2680
+ function fontFace(family, descriptors, options) {
2681
+ return getGlobalInjector().fontFace(family, descriptors, options);
2682
+ }
2683
+ /**
2684
+ * Inject a CSS @counter-style rule.
2685
+ *
2686
+ * Permanent and global — no dispose or ref-counting. Deduplicates by name and
2687
+ * overrides an existing rule by default. Pass `weak: true` for global
2688
+ * `configure()` definitions, which never clobber an existing rule.
2689
+ */
2690
+ function counterStyle(name, descriptors, options) {
2691
+ return getGlobalInjector().counterStyle(name, descriptors, options);
2692
+ }
2693
+ /**
2694
+ * Inject a CSS @function rule (custom function).
2695
+ *
2696
+ * Permanent and global — no dispose or ref-counting. Deduplicates by function
2697
+ * name and overrides an existing rule by default. Pass `weak: true` for global
2698
+ * `configure()` definitions, which never clobber an existing rule.
2699
+ *
2700
+ * @param name - The function name (`$$name`, `$name`, or `--name`)
2701
+ * @param definition - Function definition (args, returns, result, local vars)
2702
+ *
2703
+ * @example
2704
+ * ```ts
2705
+ * func('$$negative', { args: ['$value'], result: '(-1 * $value)' });
2706
+ * ```
2707
+ */
2708
+ function func(name, definition, options) {
2709
+ return getGlobalInjector().func(name, definition, options);
2710
+ }
2711
+ /**
2712
+ * Get CSS text from all sheets (for SSR)
2713
+ */
2714
+ function getCSSText(options) {
2715
+ return getGlobalInjector().getCSSText(options);
2716
+ }
2717
+ /**
2718
+ * Collect only CSS used by a rendered subtree (like jest-styled-components).
2719
+ * Pass the container returned by render(...).
2720
+ */
2721
+ function getCSSTextForNode(node, options) {
2722
+ const classRegex = tastyClassRegex(getNamePrefix());
2723
+ const classSet = /* @__PURE__ */ new Set();
2724
+ const readClasses = (el) => {
2725
+ const cls = el.getAttribute("class");
2726
+ if (!cls) return;
2727
+ for (const token of cls.split(/\s+/)) if (classRegex.test(token)) classSet.add(token);
2728
+ };
2729
+ if (node.getAttribute) readClasses(node);
2730
+ const elements = node.querySelectorAll ? node.querySelectorAll("[class]") : [];
2731
+ if (elements) elements.forEach(readClasses);
2732
+ return getGlobalInjector().getCSSTextForClasses(classSet, options);
2733
+ }
2734
+ /**
2735
+ * Take a reference on local `@keyframes` under the deterministic names the
2736
+ * caller resolved, and report which entry holds each. Pair with
2737
+ * `ownKeyframes()` once the rules exist to inspect.
2738
+ */
2739
+ function holdKeyframes(steps, names, options) {
2740
+ return getGlobalInjector().holdKeyframes(steps, names, options);
2741
+ }
2742
+ /**
2743
+ * Record that a class animates these keyframes, so the reference is released
2744
+ * when the last such class is collected.
2745
+ */
2746
+ function ownKeyframes(key, className, options) {
2747
+ getGlobalInjector().ownKeyframes(key, className, options);
2748
+ }
2749
+ /**
2750
+ * Remove every injected rule that is neither in the DOM nor held by an
2751
+ * outstanding `inject()` handle. Equivalent to `gc({ force: true })`.
2752
+ */
2753
+ function cleanup(root) {
2754
+ return getGlobalInjector().cleanup(root);
2755
+ }
2756
+ /**
2757
+ * Count a render, so that a collection pass is scheduled every
2758
+ * `gc.touchInterval` of them. Used internally by computeStyles and tasty().
2759
+ *
2760
+ * @deprecated The class name is ignored — collection asks the DOM what is
2761
+ * rendered rather than recording usage per class. The argument is kept so
2762
+ * existing calls still compile.
2763
+ */
2764
+ function touch(className, options) {
2765
+ if (!getConfig().gc) return;
2766
+ getGlobalInjector().touch(className, options);
2767
+ }
2768
+ /**
2769
+ * Synchronous garbage collection of unused styles — those no element carries
2770
+ * and no `inject()` handle references. Evicts the oldest ones once they exceed
2771
+ * capacity. With `{ force: true }`, removes all of them regardless of capacity.
2772
+ *
2773
+ * @returns Number of styles evicted.
2774
+ */
2775
+ function gc(options) {
2776
+ return getGlobalInjector().gc(options);
2777
+ }
2778
+ /**
2779
+ * Get the global injector instance for debugging
2780
+ */
2781
+ const injector = { get instance() {
2782
+ return getGlobalInjector();
2783
+ } };
2784
+ /**
2785
+ * Destroy all resources and clean up
2786
+ */
2787
+ function destroy(root) {
2788
+ return getGlobalInjector().destroy(root);
2789
+ }
2790
+ /**
2791
+ * Create a new isolated injector instance
2792
+ */
2793
+ function createInjector(config = {}) {
2794
+ return new StyleInjector({
2795
+ ...getConfig(),
2796
+ forceTextInjection: config.forceTextInjection ?? isTestEnvironment(),
2797
+ ...config
2798
+ });
2799
+ }
2800
+ //#endregion
2801
+ //#region src/rsc-cache.ts
2802
+ /**
2803
+ * Shared RSC (React Server Components) inline style cache.
2804
+ *
2805
+ * Uses React.cache for per-request memoization in Server Components.
2806
+ * Both computeStyles() and standalone style functions (useGlobalStyles,
2807
+ * useRawCSS, useKeyframes, useProperty, useFontFace, useCounterStyle)
2808
+ * share this cache so that CSS accumulated by standalone functions is
2809
+ * flushed into inline <style> tags by the next tasty() component.
2810
+ */
2811
+ /**
2812
+ * Per-request RSC style cache using React.cache.
2813
+ * React.cache provides per-request memoization in Server Components,
2814
+ * so each request gets its own isolated cache.
2815
+ */
2816
+ const getRSCCache = cache(() => ({
2817
+ cacheKeyToClassName: /* @__PURE__ */ new Map(),
2818
+ emittedKeys: /* @__PURE__ */ new Set(),
2819
+ internalsEmitted: false,
2820
+ pendingCSS: [],
2821
+ keyToIndex: /* @__PURE__ */ new Map(),
2822
+ generatedNames: /* @__PURE__ */ new Map()
2823
+ }));
2824
+ function rscAllocateClassName(rscCache, cacheKey) {
2825
+ const existing = rscCache.cacheKeyToClassName.get(cacheKey);
2826
+ if (existing) return {
2827
+ className: existing,
2828
+ isNew: false
2829
+ };
2830
+ const className = makeClassName(getNamePrefix(), hashString(cacheKey));
2831
+ rscCache.cacheKeyToClassName.set(cacheKey, className);
2832
+ return {
2833
+ className,
2834
+ isNew: true
2835
+ };
2836
+ }
2837
+ /**
2838
+ * Flush any pending CSS accumulated by standalone functions.
2839
+ * Returns the CSS string and clears the buffer.
2840
+ */
2841
+ function flushPendingCSS(rscCache) {
2842
+ if (rscCache.pendingCSS.length === 0) return "";
2843
+ const css = rscCache.pendingCSS.join("\n");
2844
+ rscCache.pendingCSS.length = 0;
2845
+ rscCache.keyToIndex.clear();
2846
+ return css;
2847
+ }
2848
+ /**
2849
+ * Push CSS into the RSC pending buffer with dedup via emittedKeys.
2850
+ * Returns true if the CSS was added, false if it was already emitted.
2851
+ *
2852
+ * Pass `replace` for slot-keyed entries (an explicit `id`), where the last write
2853
+ * must win to match the client's update-tracking behavior. If the slot has
2854
+ * already been flushed into a `<style>` tag the new CSS is appended instead,
2855
+ * which still wins by cascade order.
2856
+ */
2857
+ function pushRSCCSS(rscCache, key, css, replace) {
2858
+ if (replace) {
2859
+ const pendingIndex = rscCache.keyToIndex.get(key);
2860
+ if (pendingIndex !== void 0) {
2861
+ rscCache.pendingCSS[pendingIndex] = css;
2862
+ return true;
2863
+ }
2864
+ } else if (rscCache.emittedKeys.has(key)) return false;
2865
+ rscCache.emittedKeys.add(key);
2866
+ rscCache.keyToIndex.set(key, rscCache.pendingCSS.length);
2867
+ rscCache.pendingCSS.push(css);
2868
+ return true;
2869
+ }
2870
+ /**
2871
+ * Determine the current style injection target.
2872
+ * Centralizes the three-way detection (SSR collector / RSC cache / client DOM)
2873
+ * used by all style functions.
2874
+ */
2875
+ function getStyleTarget() {
2876
+ const collector = getRegisteredSSRCollector();
2877
+ if (collector) return {
2878
+ mode: "ssr",
2879
+ collector
2880
+ };
2881
+ if (typeof document === "undefined") return {
2882
+ mode: "rsc",
2883
+ cache: getRSCCache()
2884
+ };
2885
+ return { mode: "client" };
2886
+ }
2887
+ //#endregion
2888
+ //#region src/ssr/collect-auto-properties.ts
2889
+ /**
2890
+ * Scan rendered rules for auto-inferable custom properties and emit
2891
+ * @property CSS via the provided callback.
2892
+ */
2893
+ function scanAndEmitAutoProperties(rules, styles, emit) {
2894
+ const registered = /* @__PURE__ */ new Set();
2895
+ if (styles) {
2896
+ const localProps = styles["@property"];
2897
+ if (localProps && typeof localProps === "object") for (const token of Object.keys(localProps)) {
2898
+ const parsed = parsePropertyToken(token);
2899
+ if (parsed.isValid) registered.add(parsed.cssName);
2900
+ }
2901
+ }
2902
+ const resolver = new PropertyTypeResolver();
2903
+ for (const rule of rules) {
2904
+ if (!rule.declarations) continue;
2905
+ resolver.scanDeclarations(rule.declarations, (name) => registered.has(name), (name, syntax, initialValue) => {
2906
+ registered.add(name);
2907
+ const css = formatPropertyCSS(name, {
2908
+ syntax,
2909
+ inherits: true,
2910
+ initialValue
2911
+ });
2912
+ if (css) emit(name, css);
2913
+ });
2914
+ }
2915
+ }
2916
+ /**
2917
+ * Scan rendered rules for custom property declarations and collect
2918
+ * auto-inferred @property rules via the SSR collector.
2919
+ *
2920
+ * @param rules - Rendered style rules containing CSS declarations
2921
+ * @param collector - SSR collector to emit @property CSS into
2922
+ * @param styles - Original styles object (used to skip explicit @property)
2923
+ */
2924
+ function collectAutoInferredProperties(rules, collector, styles) {
2925
+ scanAndEmitAutoProperties(rules, styles, (name, css) => {
2926
+ collector.collectProperty(`__auto:${name}`, css);
2927
+ });
2928
+ }
2929
+ /**
2930
+ * RSC variant: scan rendered rules and push auto-inferred @property CSS
2931
+ * into the RSC pending buffer.
2932
+ */
2933
+ function collectAutoInferredPropertiesRSC(rules, rscCache, styles) {
2934
+ scanAndEmitAutoProperties(rules, styles, (name, css) => {
2935
+ pushRSCCSS(rscCache, `__auto:${name}`, css);
2936
+ });
2937
+ }
2938
+ //#endregion
2939
+ //#region src/utils/has-keys.ts
2940
+ /**
2941
+ * Check if an object has any own enumerable keys.
2942
+ * Avoids the array allocation of Object.keys(obj).length > 0.
2943
+ */
2944
+ function hasKeys(obj) {
2945
+ for (const _ in obj) return true;
2946
+ return false;
2947
+ }
2948
+ //#endregion
2949
+ //#region src/utils/resolve-recipes.ts
2950
+ /** Resolve recipe references against the globally configured recipe map. */
2951
+ function resolveRecipes(styles) {
2952
+ return resolveRecipesWith(styles, getGlobalRecipes());
2953
+ }
2954
+ //#endregion
2955
+ //#region src/compute-styles.ts
2956
+ /**
2957
+ * Hook-free, synchronous style computation.
2958
+ *
2959
+ * Extracts the core logic from useStyles() into a plain function that can
2960
+ * be called during React render without any hooks. Three code paths:
2961
+ *
2962
+ * 1. SSR collector — styles collected via ServerStyleCollector
2963
+ * 2. Client inject — styles injected synchronously into the DOM
2964
+ * 3. RSC inline — styles returned as CSS strings for inline <style> emission
2965
+ *
2966
+ * This enables tasty() components to work as React Server Components.
2967
+ */
2968
+ const EMPTY_RESULT = { className: "" };
2969
+ /**
2970
+ * Collect per-component ancillary CSS (keyframes, @property, font-face,
2971
+ * counter-style) for RSC mode.
2972
+ */
2973
+ function collectAncillaryRSC(rscCache, styles, usedKf, keyframeNames) {
2974
+ const parts = [];
2975
+ if (usedKf && keyframeNames) for (const [authored, steps] of Object.entries(usedKf)) {
2976
+ const name = keyframeNames.get(authored);
2977
+ const key = `__kf:${name}`;
2978
+ if (!rscCache.emittedKeys.has(key)) {
2979
+ rscCache.emittedKeys.add(key);
2980
+ parts.push(formatKeyframesCSS(name, steps));
2981
+ }
2982
+ }
2983
+ const localProperties = extractLocalProperties(styles);
2984
+ if (localProperties) for (const [token, definition] of Object.entries(localProperties)) {
2985
+ const key = `__prop:${token}`;
2986
+ if (!rscCache.emittedKeys.has(key)) {
2987
+ rscCache.emittedKeys.add(key);
2988
+ const css = formatPropertyCSS(token, definition);
2989
+ if (css) parts.push(css);
2990
+ }
2991
+ }
2992
+ const localFontFace = extractLocalFontFace(styles);
2993
+ if (localFontFace) for (const [family, input] of Object.entries(localFontFace)) {
2994
+ const descriptors = Array.isArray(input) ? input : [input];
2995
+ for (const desc of descriptors) {
2996
+ const key = `__ff:${fontFaceContentHash(family, desc)}`;
2997
+ if (!rscCache.emittedKeys.has(key)) {
2998
+ rscCache.emittedKeys.add(key);
2999
+ parts.push(formatFontFaceRule(family, desc));
3000
+ }
3001
+ }
3002
+ }
3003
+ const localCounterStyle = extractLocalCounterStyle(styles);
3004
+ if (localCounterStyle) for (const [name, descriptors] of Object.entries(localCounterStyle)) {
3005
+ const key = `__cs:${name}:${JSON.stringify(descriptors)}`;
3006
+ if (!rscCache.emittedKeys.has(key)) {
3007
+ rscCache.emittedKeys.add(key);
3008
+ parts.push(formatCounterStyleRule(name, descriptors));
3009
+ }
3010
+ }
3011
+ if (!isFunctionsPolyfillEnabled()) {
3012
+ const localFunctions = extractLocalFunctions(styles);
3013
+ if (localFunctions) for (const [name, definition] of Object.entries(localFunctions)) {
3014
+ const key = `__func:${parseFunctionName(name)}`;
3015
+ if (!rscCache.emittedKeys.has(key)) {
3016
+ rscCache.emittedKeys.add(key);
3017
+ parts.push(formatFunctionRule(name, definition));
3018
+ }
3019
+ }
3020
+ }
3021
+ return parts.join("\n");
3022
+ }
3023
+ /**
3024
+ * Process all chunks in RSC mode: render CSS to strings, allocate classNames,
3025
+ * and return combined { className, css }.
3026
+ */
3027
+ function computeStylesRSC(styles, chunkMap, stableStyles) {
3028
+ const rscCache = getRSCCache();
3029
+ const cssParts = [];
3030
+ const classNames = [];
3031
+ const rscUsedKf = getUsedKeyframes(styles);
3032
+ const rscKeyframeNames = rscUsedKf ? resolveKeyframesNames(rscUsedKf) : null;
3033
+ const pendingCSS = flushPendingCSS(rscCache);
3034
+ if (pendingCSS) cssParts.push(pendingCSS);
3035
+ rscCache.internalsEmitted = true;
3036
+ for (const [chunkName, chunkStyleKeys] of chunkMap) {
3037
+ if (chunkStyleKeys.length === 0) continue;
3038
+ const baseKey = generateChunkCacheKey(styles, chunkName, chunkStyleKeys, stableStyles);
3039
+ const renderResult = renderStylesForChunk(styles, chunkName, chunkStyleKeys);
3040
+ if (renderResult.rules.length === 0) continue;
3041
+ const { rules, cacheKey } = applyKeyframeNames(renderResult.rules, baseKey, rscKeyframeNames);
3042
+ const { className, isNew } = rscAllocateClassName(rscCache, cacheKey);
3043
+ classNames.push(className);
3044
+ if (isNew) {
3045
+ const css = formatRules(rules, className);
3046
+ if (css) cssParts.push(css);
3047
+ }
3048
+ }
3049
+ const ancillaryCSS = collectAncillaryRSC(rscCache, styles, rscUsedKf, rscKeyframeNames);
3050
+ if (ancillaryCSS) cssParts.push(ancillaryCSS);
3051
+ if (classNames.length === 0) return EMPTY_RESULT;
3052
+ const css = cssParts.join("\n");
3053
+ return {
3054
+ className: classNames.join(" "),
3055
+ css: css || void 0
3056
+ };
3057
+ }
3058
+ /**
3059
+ * Get keyframes that are actually used in styles.
3060
+ * Returns null if no keyframes are used (fast path for zero overhead).
3061
+ */
3062
+ function getUsedKeyframes(styles) {
3063
+ const hasLocal = hasLocalKeyframes(styles);
3064
+ const hasGlobal = hasGlobalKeyframes();
3065
+ if (!hasLocal && !hasGlobal) return null;
3066
+ const usedNames = extractAnimationNamesFromStyles(styles);
3067
+ if (usedNames.size === 0) return null;
3068
+ return filterUsedKeyframes(mergeKeyframes(hasLocal ? extractLocalKeyframes(styles) : null, hasGlobal ? getGlobalKeyframes() : null), usedNames);
3069
+ }
3070
+ /**
3071
+ * Process a chunk on the SSR path: allocate via collector, render, collect CSS.
3072
+ */
3073
+ function processChunkSSR(collector, styles, chunkName, styleKeys, stableStyles, keyframeNames) {
3074
+ if (styleKeys.length === 0) return null;
3075
+ const baseKey = generateChunkCacheKey(styles, chunkName, styleKeys, stableStyles);
3076
+ const renderResult = renderStylesForChunk(styles, chunkName, styleKeys);
3077
+ if (renderResult.rules.length === 0) return null;
3078
+ const { rules, cacheKey } = applyKeyframeNames(renderResult.rules, baseKey, keyframeNames);
3079
+ const { className, isNewAllocation } = collector.allocateClassName(cacheKey);
3080
+ if (isNewAllocation) {
3081
+ collector.collectChunk(cacheKey, className, rules);
3082
+ return {
3083
+ className,
3084
+ rules
3085
+ };
3086
+ }
3087
+ return {
3088
+ className,
3089
+ rules: []
3090
+ };
3091
+ }
3092
+ /**
3093
+ * Point a chunk's rules at the resolved keyframe names, and fold those names
3094
+ * into its cache key.
3095
+ *
3096
+ * Both halves matter, and both have to happen before the rules are written
3097
+ * anywhere: the declarations have to name the animation that will exist, and
3098
+ * the key has to distinguish two components that authored the same shorthand
3099
+ * over different definitions. Shared by the client and the server so they
3100
+ * cannot disagree about either.
3101
+ */
3102
+ function applyKeyframeNames(ruleset, baseKey, keyframeNames) {
3103
+ if (!keyframeNames || keyframeNames.size === 0) return {
3104
+ animations: [],
3105
+ rules: ruleset,
3106
+ cacheKey: baseKey
3107
+ };
3108
+ const usedNames = /* @__PURE__ */ new Set();
3109
+ let rules;
3110
+ for (let i = 0; i < ruleset.length; i++) {
3111
+ const rule = ruleset[i];
3112
+ const declarations = replaceAnimationNames(rule.declarations, keyframeNames, usedNames);
3113
+ if (declarations !== rule.declarations) {
3114
+ rules ??= ruleset.slice();
3115
+ rules[i] = {
3116
+ ...rule,
3117
+ declarations
3118
+ };
3119
+ }
3120
+ }
3121
+ const animations = [...keyframeNames.keys()].filter((authored) => usedNames.has(authored));
3122
+ if (animations.length === 0) return {
3123
+ animations,
3124
+ rules: ruleset,
3125
+ cacheKey: baseKey
3126
+ };
3127
+ const cacheKey = `${baseKey}\u0000kf:${animations.map((authored) => `${authored}=${keyframeNames.get(authored)}`).sort().join(",")}`;
3128
+ return {
3129
+ animations,
3130
+ rules: rules ?? ruleset,
3131
+ cacheKey
3132
+ };
3133
+ }
3134
+ /**
3135
+ * Process a chunk on the client: render, allocate className, and inject
3136
+ * CSS synchronously. The injector's cache makes this idempotent.
3137
+ */
3138
+ function processChunkSync(styles, chunkName, styleKeys, stableStyles, root, keyframeNames) {
3139
+ if (styleKeys.length === 0) return null;
3140
+ const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys, stableStyles);
3141
+ const renderResult = renderStylesForChunk(styles, chunkName, styleKeys, cacheKey);
3142
+ if (renderResult.rules.length === 0) return null;
3143
+ const { animations, rules, cacheKey: injectKey } = applyKeyframeNames(renderResult.rules, cacheKey, keyframeNames);
3144
+ const { className } = inject(rules, {
3145
+ cacheKey: injectKey,
3146
+ root,
3147
+ pin: false
3148
+ });
3149
+ return {
3150
+ className,
3151
+ animations
3152
+ };
3153
+ }
3154
+ /**
3155
+ * Inject all ancillary rules (properties, font-faces, counter-styles) synchronously.
3156
+ */
3157
+ function injectAncillarySync(styles, root) {
3158
+ const localProperties = extractLocalProperties(styles);
3159
+ if (localProperties) for (const [token, definition] of Object.entries(localProperties)) property(token, {
3160
+ ...definition,
3161
+ root
3162
+ });
3163
+ const localFontFace = extractLocalFontFace(styles);
3164
+ if (localFontFace) for (const [family, input] of Object.entries(localFontFace)) {
3165
+ const descriptors = Array.isArray(input) ? input : [input];
3166
+ for (const desc of descriptors) fontFace(family, desc, { root });
3167
+ }
3168
+ const localCounterStyle = extractLocalCounterStyle(styles);
3169
+ if (localCounterStyle) for (const [name, descriptors] of Object.entries(localCounterStyle)) counterStyle(name, descriptors, { root });
3170
+ if (!isFunctionsPolyfillEnabled()) {
3171
+ const localFunctions = extractLocalFunctions(styles);
3172
+ if (localFunctions) for (const [name, definition] of Object.entries(localFunctions)) func(name, definition, { root });
3173
+ }
3174
+ }
3175
+ /**
3176
+ * Collect all ancillary rules into the SSR collector.
3177
+ */
3178
+ function collectAncillarySSR(collector, styles, chunks, usedKf, keyframeNames) {
3179
+ if (usedKf && keyframeNames) for (const [authored, steps] of Object.entries(usedKf)) {
3180
+ const name = keyframeNames.get(authored);
3181
+ collector.collectKeyframes(name, formatKeyframesCSS(name, steps));
3182
+ }
3183
+ const localProperties = extractLocalProperties(styles);
3184
+ if (localProperties) for (const [token, definition] of Object.entries(localProperties)) {
3185
+ const css = formatPropertyCSS(token, definition);
3186
+ if (css) collector.collectProperty(token, css);
3187
+ }
3188
+ const localFontFace = extractLocalFontFace(styles);
3189
+ if (localFontFace) for (const [family, input] of Object.entries(localFontFace)) {
3190
+ const descriptors = Array.isArray(input) ? input : [input];
3191
+ for (const desc of descriptors) {
3192
+ const hash = fontFaceContentHash(family, desc);
3193
+ const css = formatFontFaceRule(family, desc);
3194
+ collector.collectFontFace(hash, css);
3195
+ }
3196
+ }
3197
+ const localCounterStyle = extractLocalCounterStyle(styles);
3198
+ if (localCounterStyle) for (const [name, descriptors] of Object.entries(localCounterStyle)) {
3199
+ const css = formatCounterStyleRule(name, descriptors);
3200
+ collector.collectCounterStyle(name, css);
3201
+ }
3202
+ if (!isFunctionsPolyfillEnabled()) {
3203
+ const localFunctions = extractLocalFunctions(styles);
3204
+ if (localFunctions) for (const [name, definition] of Object.entries(localFunctions)) {
3205
+ const css = formatFunctionRule(name, definition);
3206
+ collector.collectFunction(parseFunctionName(name), css);
3207
+ }
3208
+ }
3209
+ if (getConfig().autoPropertyTypes !== false) {
3210
+ const allRules = chunks.flatMap((chunk) => chunk.rules ?? []);
3211
+ if (allRules.length > 0) collectAutoInferredProperties(allRules, collector, styles);
3212
+ }
3213
+ }
3214
+ /**
3215
+ * Synchronous, hook-free style computation.
3216
+ *
3217
+ * Resolves recipes, categorizes style keys into chunks, renders CSS rules,
3218
+ * allocates class names, and injects / collects / returns the CSS.
3219
+ *
3220
+ * Three code paths:
3221
+ * 1. SSR collector — discovered via ALS or passed explicitly; CSS collected
3222
+ * 2. RSC inline — no collector and no `document`; CSS returned as `result.css`
3223
+ * for the caller to emit as an inline `<style>` tag
3224
+ * 3. Client inject — CSS injected synchronously into the DOM (idempotent)
3225
+ *
3226
+ * @param styles - Tasty styles object (or undefined for no styles)
3227
+ * @param options - Optional SSR collector override
3228
+ */
3229
+ function computeStyles(styles, options) {
3230
+ if (!styles || !hasKeys(styles)) return EMPTY_RESULT;
3231
+ const resolved = resolveRecipes(styles);
3232
+ const stableStyles = options?.stableStyles === true && resolved === styles;
3233
+ if (isFunctionsPolyfillEnabled()) registerLocalFunctionPolyfills(resolved);
3234
+ const chunkMap = categorizeStyleKeys(resolved);
3235
+ const collector = options?.ssrCollector !== void 0 ? options.ssrCollector : getRegisteredSSRCollector();
3236
+ const chunks = [];
3237
+ if (collector) {
3238
+ collector.collectInternals();
3239
+ const ssrKf = getUsedKeyframes(resolved);
3240
+ const ssrKeyframeNames = ssrKf ? resolveKeyframesNames(ssrKf) : null;
3241
+ for (const [chunkName, chunkStyleKeys] of chunkMap) {
3242
+ const chunk = processChunkSSR(collector, resolved, chunkName, chunkStyleKeys, stableStyles, ssrKeyframeNames);
3243
+ if (chunk) chunks.push(chunk);
3244
+ }
3245
+ collectAncillarySSR(collector, resolved, chunks, ssrKf, ssrKeyframeNames);
3246
+ } else if (typeof document === "undefined") return computeStylesRSC(resolved, chunkMap, stableStyles);
3247
+ else {
3248
+ const root = options?.root;
3249
+ injectAncillarySync(resolved, root);
3250
+ const usedKf = getUsedKeyframes(resolved);
3251
+ const keyframeNames = usedKf ? resolveKeyframesNames(usedKf) : null;
3252
+ const keyframeKeys = usedKf && keyframeNames ? holdKeyframes(usedKf, keyframeNames, { root }) : null;
3253
+ for (const [chunkName, chunkStyleKeys] of chunkMap) {
3254
+ const chunk = processChunkSync(resolved, chunkName, chunkStyleKeys, stableStyles, root, keyframeNames);
3255
+ if (chunk) chunks.push(chunk);
3256
+ }
3257
+ if (keyframeKeys) for (const chunk of chunks) for (const authored of chunk.animations ?? []) {
3258
+ const key = keyframeKeys.get(authored);
3259
+ if (key) ownKeyframes(key, chunk.className, { root });
3260
+ }
3261
+ for (const chunk of chunks) touch(chunk.className, { root });
3262
+ }
3263
+ if (chunks.length === 0) return EMPTY_RESULT;
3264
+ if (chunks.length === 1) return { className: chunks[0].className };
3265
+ return { className: chunks.map((c) => c.className).join(" ") };
3266
+ }
3267
+ //#endregion
3268
+ //#region src/utils/filter-base-props.ts
3269
+ const BasePropNames = new Set([
3270
+ "role",
3271
+ "as",
3272
+ "element",
3273
+ "css",
3274
+ "qa",
3275
+ "mods",
3276
+ "qaVal",
3277
+ "hidden",
3278
+ "isHidden",
3279
+ "disabled",
3280
+ "isDisabled",
3281
+ "children",
3282
+ "style",
3283
+ "className",
3284
+ "href",
3285
+ "target",
3286
+ "tabIndex"
3287
+ ]);
3288
+ const eventRe = /^on[A-Z].+$/;
3289
+ const ignoredEventProps = new Set([
3290
+ "onPress",
3291
+ "onHoverStart",
3292
+ "onHoverEnd",
3293
+ "onPressStart",
3294
+ "onPressEnd"
3295
+ ]);
3296
+ /**
3297
+ * Filters out all props that aren't valid DOM props or defined via override prop obj.
3298
+ * @param props - The component props to be filtered.
3299
+ * @param opts - Props to override.
3300
+ */
3301
+ function filterBaseProps(props, opts = {}) {
3302
+ const { propNames, eventProps } = opts;
3303
+ const filteredProps = {};
3304
+ for (const prop of Object.keys(props)) if (prop === "id" || BasePropNames.has(prop) || prop.startsWith("aria-") || eventProps && eventRe.test(prop) && !ignoredEventProps.has(prop) || propNames?.has(prop) || prop.startsWith("data-")) filteredProps[prop] = props[prop];
3305
+ return filteredProps;
3306
+ }
3307
+ //#endregion
3308
+ //#region src/utils/mod-attrs.ts
3309
+ const cache$1 = new Lru();
3310
+ function modAttrs(map) {
3311
+ if (!map) return null;
3312
+ const cacheKey = JSON.stringify(map);
3313
+ const cached = cache$1.get(cacheKey);
3314
+ if (cached) return cached;
3315
+ const attrs = {};
3316
+ for (const key of Object.keys(map)) {
3317
+ const value = map[key];
3318
+ if (value == null || value === false) continue;
3319
+ const attrName = `data-${camelToKebab(key)}`;
3320
+ if (value === true) attrs[attrName] = "";
3321
+ else if (typeof value === "string") attrs[attrName] = value;
3322
+ else if (typeof value === "number") attrs[attrName] = String(value);
3323
+ else console.warn(`[Tasty] Invalid mod value for "${key}". Expected boolean, string, or number, got ${typeof value}`);
3324
+ }
3325
+ cache$1.set(cacheKey, attrs);
3326
+ return attrs;
3327
+ }
3328
+ //#endregion
3329
+ //#region src/utils/process-tokens.ts
3330
+ /**
3331
+ * Check if a value is a valid token value (string, number, or boolean - not object).
3332
+ * Returns false for `false` values (they mean "skip this token").
3333
+ */
3334
+ function isValidTokenValue(value) {
3335
+ if (value === void 0 || value === null || value === false) return false;
3336
+ if (typeof value === "object") {
3337
+ console.warn("[Tasty] Object values are not allowed in tokens prop. Tokens do not support state-based styling. Use a primitive value instead.");
3338
+ return false;
3339
+ }
3340
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
3341
+ }
3342
+ /**
3343
+ * Process a single token value through the tasty parser.
3344
+ * Numbers are converted to strings; 0 stays as "0".
3345
+ */
3346
+ function processTokenValue(value) {
3347
+ if (typeof value === "number") {
3348
+ if (value === 0) return "0";
3349
+ return parseStyle(String(value)).output;
3350
+ }
3351
+ return parseStyle(value).output;
3352
+ }
3353
+ /**
3354
+ * Resolve one `$name` / `#name` entry to the custom property it declares and
3355
+ * the value to declare, or `null` when the entry declares nothing.
3356
+ *
3357
+ * The two sigils differ only in the property name they build and in how `true`
3358
+ * is read: for a custom property it is the empty value, for a color it is
3359
+ * `transparent`.
3360
+ */
3361
+ function resolveTokenEntry(key, value) {
3362
+ const name = `--${normalizeDslName(key.slice(1))}`;
3363
+ if (key[0] === "$") return [name, processTokenValue(value === true ? "" : value)];
3364
+ const colorValue = normalizeColorTokenValue(value);
3365
+ if (colorValue === null) return null;
3366
+ return [`${name}-color`, processTokenValue(colorValue)];
3367
+ }
3368
+ /**
3369
+ * Process tokens object into inline style properties.
3370
+ * - `$name` -> `--name` with the parsed value
3371
+ * - `#name` -> `--name-color` with the parsed value
3372
+ *
3373
+ * @param tokens - The tokens object to process
3374
+ * @returns CSSProperties object or undefined if no tokens to process
3375
+ */
3376
+ function processTokens(tokens) {
3377
+ if (!tokens) return;
3378
+ const keys = Object.keys(tokens);
3379
+ if (keys.length === 0) return;
3380
+ let result;
3381
+ for (const key of keys) {
3382
+ const value = tokens[key];
3383
+ if (!isValidTokenValue(value)) continue;
3384
+ if (key[0] !== "$" && key[0] !== "#") continue;
3385
+ const entry = resolveTokenEntry(key, value);
3386
+ if (!entry) continue;
3387
+ if (!result) result = {};
3388
+ result[entry[0]] = entry[1];
3389
+ }
3390
+ return result;
3391
+ }
3392
+ //#endregion
3393
+ //#region src/utils/client-state.ts
3394
+ /**
3395
+ * Build a per-(injector, root) client state cache for the standalone style
3396
+ * functions (`useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useCounterStyle`).
3397
+ *
3398
+ * Two levels, both weak:
3399
+ *
3400
+ * - **injector** — `configure()` replaces the global injector, and every dispose
3401
+ * handle and generated name we cache belongs to the one that produced it.
3402
+ * Keying by injector makes stale state fall away with it, instead of letting
3403
+ * change-detection keys suppress re-injection into the new sheets.
3404
+ * - **root** — the same selector or slot name can be used in several shadow
3405
+ * roots, and each holds its own injection.
3406
+ */
3407
+ function createClientState(create) {
3408
+ const byInjector = /* @__PURE__ */ new WeakMap();
3409
+ return (root) => {
3410
+ const injector = getGlobalInjector();
3411
+ let byRoot = byInjector.get(injector);
3412
+ if (!byRoot) {
3413
+ byRoot = /* @__PURE__ */ new WeakMap();
3414
+ byInjector.set(injector, byRoot);
3415
+ }
3416
+ let state = byRoot.get(root);
3417
+ if (!state) {
3418
+ state = create();
3419
+ byRoot.set(root, state);
3420
+ }
3421
+ return state;
3422
+ };
3423
+ }
3424
+ //#endregion
3425
+ //#region src/utils/deps-equal.ts
3426
+ /**
3427
+ * Shallow comparison of two dependency arrays using Object.is semantics.
3428
+ * Returns true when both arrays have the same length and every element
3429
+ * at the same index is identical.
3430
+ */
3431
+ function depsEqual(a, b) {
3432
+ if (a.length !== b.length) return false;
3433
+ for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
3434
+ return true;
3435
+ }
3436
+ //#endregion
3437
+ export { propHandlerRegistry as $, ownKeyframes as A, getNamePrefix as B, holdKeyframes as C, injector as D, injectRawCSS as E, configure as F, resetConfig as G, hasGlobalRecipes as H, getConfig as I, closeBatchWindow as J, StyleInjector as K, getGlobalInjector as L, touch as M, ChunkSheetRegistry as N, isPropertyDefined as O, chunkSheetRegistry as P, resetStyleBatch as Q, getGlobalKeyframes as R, getRawCSSText as S, injectGlobal as T, isConfigLocked as U, hasGlobalKeyframes as V, isTestEnvironment as W, hasPendingStyleWrites as X, flushStyles as Y, openBatchWindow as Z, fontFace as _, filterBaseProps as a, getCSSText as b, hasKeys as c, getStyleTarget as d, pushRSCCSS as f, destroy as g, createInjector as h, modAttrs as i, property as j, keyframes as k, collectAutoInferredProperties as l, counterStyle as m, createClientState as n, computeStyles as o, cleanup as p, SheetManager as q, processTokens as r, resolveRecipes as s, depsEqual as t, collectAutoInferredPropertiesRSC as u, func as v, inject as w, getCSSTextForNode as x, gc as y, getGlobalRecipes as z };
3438
+
3439
+ //# sourceMappingURL=runtime-engine-D2WOLvd6.js.map