@tenphi/tasty 0.0.0-snapshot.d656d18 → 0.0.0-snapshot.d7129ca

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 (92) hide show
  1. package/README.md +6 -6
  2. package/dist/{babel-_RVkw0iE.d.ts → babel-DD7kdEFT.d.ts} +2 -2
  3. package/dist/chunks/async-storage-CG5HnUT5.js +20 -0
  4. package/dist/chunks/{async-storage-DKK-wTD4.js.map → async-storage-CG5HnUT5.js.map} +1 -1
  5. package/dist/chunks/build-config-CUQVwdWF.js +42 -0
  6. package/dist/chunks/build-config-CUQVwdWF.js.map +1 -0
  7. package/dist/chunks/{collector-DBGcmUEq.js → collector-B0LTHw6Z.js} +6 -124
  8. package/dist/chunks/collector-B0LTHw6Z.js.map +1 -0
  9. package/dist/chunks/config-engine-C3pF9EK9.js +267 -0
  10. package/dist/chunks/config-engine-C3pF9EK9.js.map +1 -0
  11. package/dist/chunks/css-definitions-D_it4XZH.js +875 -0
  12. package/dist/chunks/css-definitions-D_it4XZH.js.map +1 -0
  13. package/dist/chunks/{css-resources-Cyl_axbI.js → css-resources-BN8apgxt.js} +1 -8
  14. package/dist/chunks/{css-resources-Cyl_axbI.js.map → css-resources-BN8apgxt.js.map} +1 -1
  15. package/dist/chunks/{debug-BUwqo6vC.js → debug-CiiHDgmR.js} +69 -150
  16. package/dist/chunks/debug-CiiHDgmR.js.map +1 -0
  17. package/dist/chunks/{dsl-CXvoaLnj.js → dsl-C5yuAn41.js} +62 -786
  18. package/dist/chunks/dsl-C5yuAn41.js.map +1 -0
  19. package/dist/chunks/hydration-TRcz5vnk.js +41 -0
  20. package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
  21. package/dist/chunks/react-runtime-sH6r6Ajr.js +557 -0
  22. package/dist/chunks/react-runtime-sH6r6Ajr.js.map +1 -0
  23. package/dist/chunks/runtime-engine-BrRcOZwD.js +2438 -0
  24. package/dist/chunks/runtime-engine-BrRcOZwD.js.map +1 -0
  25. package/dist/chunks/runtime-state-BTDqSYBk.js +127 -0
  26. package/dist/chunks/runtime-state-BTDqSYBk.js.map +1 -0
  27. package/dist/chunks/{shared-utils-OIg0N_dp.js → shared-utils-DWA4LjIa.js} +24 -62
  28. package/dist/chunks/shared-utils-DWA4LjIa.js.map +1 -0
  29. package/dist/chunks/{style-engine-CqSmIlud.js → style-engine-C1ECBZxR.js} +671 -2736
  30. package/dist/chunks/style-engine-C1ECBZxR.js.map +1 -0
  31. package/dist/chunks/{css-writer-PFzrSPeu.js → zero-engine-B6EnYcXY.js} +3 -120
  32. package/dist/chunks/zero-engine-B6EnYcXY.js.map +1 -0
  33. package/dist/{collector-CVdVIy7s.d.ts → collector-YpsMjV7T.d.ts} +6 -4
  34. package/dist/{config-BI0y21JV.d.ts → config-BnZqyYG4.d.ts} +16 -25
  35. package/dist/core/index.d.ts +5 -5
  36. package/dist/core/index.js +9 -7
  37. package/dist/{index-CrT0bsiI.d.ts → index-BLJLo9cl.d.ts} +17 -50
  38. package/dist/index.d.ts +5 -5
  39. package/dist/index.js +10 -7
  40. package/dist/{merge-styles-BFktNP8J.d.ts → merge-styles-CTDLIrO5.d.ts} +2 -2
  41. package/dist/ssr/astro-client.js +1 -14
  42. package/dist/ssr/astro-client.js.map +1 -1
  43. package/dist/ssr/astro-middleware-static.js +0 -11
  44. package/dist/ssr/astro-middleware-static.js.map +1 -1
  45. package/dist/ssr/astro-middleware.js +0 -12
  46. package/dist/ssr/astro-middleware.js.map +1 -1
  47. package/dist/ssr/astro.js +5 -85
  48. package/dist/ssr/astro.js.map +1 -1
  49. package/dist/ssr/index.d.ts +6 -6
  50. package/dist/ssr/index.js +4 -4
  51. package/dist/ssr/next-config.d.ts +1 -1
  52. package/dist/ssr/next-config.js +29 -20
  53. package/dist/ssr/next-config.js.map +1 -1
  54. package/dist/ssr/next.d.ts +1 -1
  55. package/dist/ssr/next.js +5 -36
  56. package/dist/ssr/next.js.map +1 -1
  57. package/dist/static/index.d.ts +2 -2
  58. package/dist/static/index.js +1 -19
  59. package/dist/static/index.js.map +1 -1
  60. package/dist/{index-BzAC3p06.d.ts → types-vfRQhzRd.d.ts} +811 -811
  61. package/dist/zero/babel.d.ts +1 -1
  62. package/dist/zero/babel.js +25 -95
  63. package/dist/zero/babel.js.map +1 -1
  64. package/dist/zero/index.d.ts +1 -1
  65. package/dist/zero/index.js +1 -1
  66. package/dist/zero/next.d.ts +1 -1
  67. package/dist/zero/next.js +0 -31
  68. package/dist/zero/next.js.map +1 -1
  69. package/docs/ai-agents.md +5 -1
  70. package/docs/dsl.md +2 -2
  71. package/docs/getting-started.md +12 -11
  72. package/docs/methodology.md +2 -2
  73. package/docs/pipeline.md +3 -0
  74. package/docs/plugins.md +1 -1
  75. package/docs/runtime-benchmarks.md +105 -68
  76. package/docs/ssr.md +1 -1
  77. package/docs/styles.md +5 -5
  78. package/docs/tasty-static.md +1 -1
  79. package/package.json +29 -10
  80. package/dist/chunks/async-storage-DKK-wTD4.js +0 -44
  81. package/dist/chunks/collector-DBGcmUEq.js.map +0 -1
  82. package/dist/chunks/css-writer-PFzrSPeu.js.map +0 -1
  83. package/dist/chunks/debug-BUwqo6vC.js.map +0 -1
  84. package/dist/chunks/dsl-CXvoaLnj.js.map +0 -1
  85. package/dist/chunks/hydrate-6YoHZP6A.js +0 -37
  86. package/dist/chunks/hydrate-6YoHZP6A.js.map +0 -1
  87. package/dist/chunks/react-runtime-B3CmvDEQ.js +0 -1525
  88. package/dist/chunks/react-runtime-B3CmvDEQ.js.map +0 -1
  89. package/dist/chunks/runtime-engine-Y00VWjrS.js +0 -4719
  90. package/dist/chunks/runtime-engine-Y00VWjrS.js.map +0 -1
  91. package/dist/chunks/shared-utils-OIg0N_dp.js.map +0 -1
  92. package/dist/chunks/style-engine-CqSmIlud.js.map +0 -1
@@ -1,4719 +0,0 @@
1
- import { F as RE_NUMBER, I as RE_RAW_UNIT, T as Lru, _ as setGlobalPredefinedTokens, c as getGlobalParseFunctions, d as normalizeColorTokenValue, h as resetGlobalPredefinedTokens, i as CUSTOM_UNITS, l as getGlobalParser, m as resetGlobalParseFunctions, o as customFunction, p as parseStyle } from "./dsl-CXvoaLnj.js";
2
- import { i as warn, n as isSelector, o as isDevEnv, r as resetStyleWarnings, t as mergeStyles } from "./shared-utils-OIg0N_dp.js";
3
- import { A as resetHandlers, E as STYLE_HANDLER_MAP, H as hasStylesGenerated, M as createStyle, O as normalizeHandlerDefinition, T as setGlobalPredefinedStates, U as markStylesGeneratedState, V as resetStyleChunks, W as resetStylesGeneratedState, a as registerBaseStyleProps, b as renderStyles, k as registerHandler, o as resetBaseStyleProps, y as clearPipelineCache } from "./style-engine-CqSmIlud.js";
4
- //#region src/functions/index.ts
5
- const FUNCTION_KEY = "@function";
6
- /** Descriptor keys that are not local-variable declarations. */
7
- const RESERVED_KEYS = new Set([
8
- "args",
9
- "returns",
10
- "result"
11
- ]);
12
- /**
13
- * Check if styles object has a local @function definition.
14
- */
15
- function hasLocalFunctions(styles) {
16
- return FUNCTION_KEY in styles;
17
- }
18
- /**
19
- * Extract local @function definitions from a styles object.
20
- * Returns null if none (fast path).
21
- */
22
- function extractLocalFunctions(styles) {
23
- const functions = styles[FUNCTION_KEY];
24
- if (!functions || typeof functions !== "object") return null;
25
- return functions;
26
- }
27
- /**
28
- * Parse a function name token into its CSS custom name.
29
- * Accepts `$$name`, `$name`, `--name`, or bare `name` → `--name`.
30
- */
31
- function parseFunctionName(token) {
32
- if (token.startsWith("$$")) return `--${token.slice(2)}`;
33
- if (token.startsWith("$")) return `--${token.slice(1)}`;
34
- if (token.startsWith("--")) return token;
35
- return `--${token}`;
36
- }
37
- /**
38
- * Parse a parameter / local-variable name token into its CSS custom name.
39
- * Accepts `$name`, `--name`, or bare `name` → `--name`.
40
- */
41
- function parseParamName(token) {
42
- if (token.startsWith("$")) return `--${token.slice(1)}`;
43
- if (token.startsWith("--")) return token;
44
- return `--${token}`;
45
- }
46
- /**
47
- * Format a single parameter (`--name <type>: default`).
48
- */
49
- function formatParameter(token, param) {
50
- const name = parseParamName(token);
51
- if (param === true) return name;
52
- if (typeof param === "string") return param ? `${name} ${param}` : name;
53
- let out = name;
54
- if (param.syntax) out += ` ${param.syntax}`;
55
- if (param.default !== void 0) out += `: ${parseStyle(param.default).output}`;
56
- return out;
57
- }
58
- /**
59
- * Format the function prelude: `@function --name(params) returns <type>`.
60
- */
61
- function formatFunctionPrelude(name, args, returns) {
62
- const cssName = parseFunctionName(name);
63
- let params = "";
64
- if (Array.isArray(args)) params = args.map((token) => formatParameter(token, true)).join(", ");
65
- else if (args) params = Object.entries(args).map(([token, param]) => formatParameter(token, param)).join(", ");
66
- const returnsPart = returns ? ` returns ${returns}` : "";
67
- return `@function ${cssName}(${params})${returnsPart}`;
68
- }
69
- /**
70
- * Format the inner declarations of a @function rule (local variables + result).
71
- */
72
- function formatFunctionDeclarations(def) {
73
- const parts = [];
74
- for (const key of Object.keys(def)) {
75
- if (RESERVED_KEYS.has(key) || !key.startsWith("$")) continue;
76
- const value = def[key];
77
- if (value === void 0) continue;
78
- parts.push(`${parseParamName(key)}: ${parseStyle(value).output};`);
79
- }
80
- parts.push(`result: ${parseStyle(def.result).output};`);
81
- return parts.join(" ");
82
- }
83
- /**
84
- * Format a complete @function rule as CSS.
85
- */
86
- function formatFunctionRule(name, def) {
87
- return `${formatFunctionPrelude(name, def.args, def.returns)} { ${formatFunctionDeclarations(def)} }`;
88
- }
89
- /** A key is a CSS-function key when it carries the `$`/`--` prefix. */
90
- function isCssFunctionKey(key) {
91
- return key.startsWith("$") || key.startsWith("--");
92
- }
93
- /** Discriminate a unified `functions` entry by its value type. */
94
- function isParseFunction(value) {
95
- return typeof value === "function";
96
- }
97
- /**
98
- * Split the unified `functions` map into the two internal registries:
99
- * - `parseFuncs`: JS parse functions (bare keys).
100
- * - `functionDefs`: declarative CSS `@function` definitions (`$$` keys).
101
- *
102
- * Entries whose key prefix does not match the value type are reported via
103
- * `onMismatch` and skipped.
104
- */
105
- function splitFunctions(map, onMismatch) {
106
- const parseFuncs = {};
107
- const functionDefs = {};
108
- for (const [key, value] of Object.entries(map)) {
109
- const cssKey = isCssFunctionKey(key);
110
- if (isParseFunction(value)) {
111
- if (cssKey) {
112
- onMismatch?.(key, "expected-definition");
113
- continue;
114
- }
115
- parseFuncs[key] = value;
116
- } else {
117
- if (!cssKey) {
118
- onMismatch?.(key, "expected-parse-function");
119
- continue;
120
- }
121
- functionDefs[key] = value;
122
- }
123
- }
124
- return {
125
- parseFuncs,
126
- functionDefs
127
- };
128
- }
129
- function escapeRegExp(str) {
130
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
131
- }
132
- const paramBareName = (token) => parseParamName(token).slice(2);
133
- /**
134
- * Rewrite `var(--name)` references for declared parameter/local names to a
135
- * collision-resistant, function-scoped prefix. This guarantees that later
136
- * value substitution only ever targets the function's own variables and can
137
- * never accidentally match a real element-level `var(--name)` introduced by an
138
- * argument value (e.g. the `--gap` used by Tasty units). Blanket prefix — no
139
- * exact-collision detection.
140
- */
141
- function prefixDeclaredVars(template, declared, prefixOf) {
142
- let out = template;
143
- for (const bare of declared) {
144
- const re = new RegExp(`var\\(\\s*--${escapeRegExp(bare)}(?![\\w-])`, "g");
145
- out = out.replace(re, `var(--${prefixOf(bare)}`);
146
- }
147
- return out;
148
- }
149
- /**
150
- * Replace the prefixed `var(--<prefixed>)` / `var(--<prefixed>, fallback)`
151
- * references with their mapped values, iterating so that local variables that
152
- * reference parameters (or other locals) resolve fully.
153
- */
154
- function substituteVars(template, valueMap) {
155
- let out = template;
156
- for (let iter = 0; iter < 30; iter++) {
157
- let changed = false;
158
- for (const [name, value] of valueMap) {
159
- const re = new RegExp(`var\\(\\s*--${escapeRegExp(name)}\\s*(?:,[^)]*)?\\)`, "g");
160
- out = out.replace(re, () => {
161
- changed = true;
162
- return value;
163
- });
164
- }
165
- if (!changed) break;
166
- }
167
- return out;
168
- }
169
- /** Function names currently being expanded, to guard against recursion cycles. */
170
- const inProgress = /* @__PURE__ */ new Set();
171
- /**
172
- * Compile a declarative `@function` definition into a parse-function closure
173
- * that fully inlines the call into plain CSS (the `@function` polyfill).
174
- *
175
- * Parameters and local variables are substituted directly with their values,
176
- * so no function-internal custom properties are emitted (collisions are
177
- * structurally impossible). Self/mutually-recursive functions are not expanded:
178
- * the cycle guard bails and the call is left untouched.
179
- */
180
- function compileFunctionClosure(name, def) {
181
- const cssName = parseFunctionName(name);
182
- const prefixOf = (bare) => `tf-${cssName.slice(2)}-${bare}`;
183
- const params = [];
184
- if (Array.isArray(def.args)) for (const token of def.args) params.push({ prefixed: prefixOf(paramBareName(token)) });
185
- else if (def.args) for (const [token, param] of Object.entries(def.args)) {
186
- let dflt;
187
- if (param && typeof param === "object" && param.default !== void 0) dflt = parseStyle(param.default).output;
188
- params.push({
189
- prefixed: prefixOf(paramBareName(token)),
190
- default: dflt
191
- });
192
- }
193
- const localKeys = Object.keys(def).filter((key) => !RESERVED_KEYS.has(key) && key.startsWith("$"));
194
- const declared = [...Array.isArray(def.args) ? def.args.map(paramBareName) : def.args ? Object.keys(def.args).map(paramBareName) : [], ...localKeys.map(paramBareName)];
195
- let built = false;
196
- let resultTemplate = "";
197
- const localTemplates = [];
198
- const build = () => {
199
- built = true;
200
- resultTemplate = prefixDeclaredVars(parseStyle(def.result).output, declared, prefixOf);
201
- for (const key of localKeys) {
202
- const value = def[key];
203
- if (value === void 0) continue;
204
- localTemplates.push({
205
- prefixed: prefixOf(paramBareName(key)),
206
- value: prefixDeclaredVars(parseStyle(value).output, declared, prefixOf)
207
- });
208
- }
209
- };
210
- return (groups) => {
211
- if (inProgress.has(cssName)) return "";
212
- inProgress.add(cssName);
213
- try {
214
- if (!built) build();
215
- const valueMap = /* @__PURE__ */ new Map();
216
- params.forEach((param, index) => {
217
- const arg = groups[index]?.output?.trim();
218
- valueMap.set(param.prefixed, arg || param.default || "");
219
- });
220
- for (const local of localTemplates) valueMap.set(local.prefixed, local.value);
221
- return substituteVars(resultTemplate, valueMap);
222
- } finally {
223
- inProgress.delete(cssName);
224
- }
225
- };
226
- }
227
- /** CSS function name -> serialized definition, to dedupe polyfill registration. */
228
- const registeredFunctionDefs = /* @__PURE__ */ new Map();
229
- /**
230
- * Register a declarative `@function` as a parse-function closure so the parser
231
- * inlines its call sites. Deduplicated by name + definition; a later, different
232
- * definition of the same name overrides the previous one (local wins).
233
- */
234
- function registerFunctionPolyfill(name, definition) {
235
- const cssName = parseFunctionName(name);
236
- const serialized = JSON.stringify(definition);
237
- if (registeredFunctionDefs.get(cssName) === serialized) return;
238
- registeredFunctionDefs.set(cssName, serialized);
239
- customFunction(cssName, compileFunctionClosure(name, definition));
240
- }
241
- /** Register all local `@function` definitions from a styles object (polyfill). */
242
- function registerLocalFunctionPolyfills(styles) {
243
- const local = extractLocalFunctions(styles);
244
- if (!local) return;
245
- for (const [name, definition] of Object.entries(local)) registerFunctionPolyfill(name, definition);
246
- }
247
- /** Clear polyfill registration bookkeeping (used by resetConfig in tests). */
248
- function resetFunctionPolyfills() {
249
- registeredFunctionDefs.clear();
250
- }
251
- //#endregion
252
- //#region src/prop-handlers.ts
253
- /**
254
- * Props middleware for tasty components.
255
- *
256
- * A prop handler receives a component's props and returns them, changed or not.
257
- * That makes it the extension point for props that are *not* style properties:
258
- * it can read a custom prop, strip it so it never reaches the DOM, and fold its
259
- * meaning into `styles`, `mods`, `tokens`, `variant`, or `as`.
260
- *
261
- * ```ts
262
- * configure({
263
- * propHandlers: {
264
- * glaze: (props) => {
265
- * const { glaze, ...rest } = props;
266
- * if (!glaze) return rest;
267
- * return { ...rest, styles: mergeStyles(glazeStyles(glaze), rest.styles) };
268
- * },
269
- * },
270
- * });
271
- * ```
272
- *
273
- * Handlers run on every render of every tasty component, so the registry is a
274
- * pre-composed chain that is `null` while nothing is registered — the cost when
275
- * unused is one property load and one branch.
276
- */
277
- const GTKEY = "__tasty_prop_handlers__";
278
- const globalStore = globalThis;
279
- const propHandlerRegistry = globalStore[GTKEY] ?? (globalStore[GTKEY] = {
280
- apply: null,
281
- list: []
282
- });
283
- function normalize(key, definition, options) {
284
- const source = options?.source ?? "configure()";
285
- if (typeof definition === "function") return {
286
- key,
287
- triggers: [key],
288
- fn: definition,
289
- source
290
- };
291
- if (Array.isArray(definition)) {
292
- const [first, fn] = definition;
293
- 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].`);
294
- if (first === "*") return {
295
- key,
296
- triggers: null,
297
- fn,
298
- source
299
- };
300
- if (typeof first === "string") return {
301
- key,
302
- triggers: [first],
303
- fn,
304
- source
305
- };
306
- if (Array.isArray(first)) return {
307
- key,
308
- triggers: first.includes("*") ? null : first,
309
- fn,
310
- source
311
- };
312
- throw new Error(`[Tasty] Invalid prop handler definition for "${key}". First element must be a string or string array.`);
313
- }
314
- throw new Error(`[Tasty] Invalid prop handler definition for "${key}". Expected function, [string, function], or [string[], function].`);
315
- }
316
- /**
317
- * Run one handler, treating a nothing/non-object return as "unchanged".
318
- *
319
- * `isDevEnv()` is called lazily rather than captured at module load so the
320
- * warnings are assertable in tests; it is only reached on the misuse path.
321
- */
322
- function runOne(handler, props) {
323
- const next = handler.fn(props);
324
- if (next != null && typeof next === "object" && !Array.isArray(next)) return next;
325
- 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.`);
326
- return props;
327
- }
328
- function compose(list) {
329
- if (list.length === 0) return null;
330
- if (list.length === 1) {
331
- const only = list[0];
332
- if (only.triggers === null) return (props) => runOne(only, props);
333
- if (only.triggers.length === 1) {
334
- const trigger = only.triggers[0];
335
- return (props) => trigger in props ? runOne(only, props) : props;
336
- }
337
- }
338
- return (props) => {
339
- for (const handler of list) {
340
- const triggers = handler.triggers;
341
- if (triggers !== null) {
342
- let triggered = false;
343
- for (const trigger of triggers) if (trigger in props) {
344
- triggered = true;
345
- break;
346
- }
347
- if (!triggered) continue;
348
- }
349
- props = runOne(handler, props);
350
- }
351
- return props;
352
- };
353
- }
354
- /**
355
- * Register a props middleware under `key`, replacing any handler already
356
- * registered under the same key while keeping its position in the chain.
357
- */
358
- function registerPropHandler(key, definition, options) {
359
- const normalized = normalize(key, definition, options);
360
- const { list } = propHandlerRegistry;
361
- const existing = list.findIndex((handler) => handler.key === key);
362
- if (existing === -1) list.push(normalized);
363
- else list[existing] = normalized;
364
- propHandlerRegistry.apply = compose(list);
365
- }
366
- /** Drop every registered props middleware. Called by `resetConfig()`. */
367
- function resetPropHandlers() {
368
- if (propHandlerRegistry.list.length === 0) return;
369
- propHandlerRegistry.list.length = 0;
370
- propHandlerRegistry.apply = null;
371
- }
372
- //#endregion
373
- //#region src/properties/index.ts
374
- const PROPERTIES_KEY = "@property";
375
- /**
376
- * Valid CSS custom property name pattern (after the -- prefix).
377
- * Must start with a letter or underscore, followed by letters, digits, hyphens, or underscores.
378
- */
379
- const VALID_PROPERTY_NAME_PATTERN = /^[a-z_][a-z0-9-_]*$/i;
380
- /**
381
- * Validate a CSS custom property name (the part after --).
382
- * Returns true if the name is valid for use as a CSS custom property.
383
- */
384
- function isValidPropertyName(name) {
385
- return VALID_PROPERTY_NAME_PATTERN.test(name);
386
- }
387
- /**
388
- * Check if styles object has local @property definition.
389
- * Fast path: single property lookup.
390
- */
391
- function hasLocalProperties(styles) {
392
- return PROPERTIES_KEY in styles;
393
- }
394
- /**
395
- * Extract local @property from styles object.
396
- * Returns null if no local properties (fast path).
397
- */
398
- function extractLocalProperties(styles) {
399
- const properties = styles[PROPERTIES_KEY];
400
- if (!properties || typeof properties !== "object") return null;
401
- return properties;
402
- }
403
- /**
404
- * Parse a property token name and return the CSS property name and whether it's a color.
405
- * Supports tasty token syntax and validates the property name.
406
- *
407
- * Token formats:
408
- * - `$name` → { cssName: '--name', isColor: false }
409
- * - `#name` → { cssName: '--name-color', isColor: true }
410
- * - `--name` → { cssName: '--name', isColor: false } (legacy, auto-detect color by suffix)
411
- * - `name` → { cssName: '--name', isColor: false } (legacy)
412
- *
413
- * @param token - The property token to parse
414
- * @returns Parsed result with cssName, isColor, isValid, and optional error
415
- */
416
- function parsePropertyToken(token) {
417
- if (!token || typeof token !== "string") return {
418
- cssName: "",
419
- isColor: false,
420
- isValid: false,
421
- error: "Property token must be a non-empty string"
422
- };
423
- let name;
424
- let isColor;
425
- if (token.startsWith("$")) {
426
- name = token.slice(1);
427
- isColor = false;
428
- } else if (token.startsWith("#")) {
429
- name = token.slice(1);
430
- isColor = true;
431
- } else if (token.startsWith("--")) {
432
- name = token.slice(2);
433
- isColor = token.endsWith("-color");
434
- } else {
435
- name = token;
436
- isColor = token.endsWith("-color");
437
- }
438
- if (!name) return {
439
- cssName: "",
440
- isColor,
441
- isValid: false,
442
- error: "Property name cannot be empty"
443
- };
444
- if (!isValidPropertyName(name)) return {
445
- cssName: "",
446
- isColor,
447
- isValid: false,
448
- error: `Invalid property name "${name}". Must start with a letter or underscore, followed by letters, digits, hyphens, or underscores.`
449
- };
450
- let cssName;
451
- if (token.startsWith("#")) cssName = `--${name}-color`;
452
- else cssName = `--${name}`;
453
- return {
454
- cssName,
455
- isColor,
456
- isValid: true
457
- };
458
- }
459
- /**
460
- * Normalize a property definition to a consistent string representation.
461
- * Used for comparing definitions to detect type conflicts.
462
- *
463
- * Only `syntax` and `inherits` are compared — `initialValue` is intentionally
464
- * excluded because different components may set different defaults for the
465
- * same typed property (e.g. auto-inferred `0px` vs explicit `6px`).
466
- *
467
- * Keys are sorted alphabetically to ensure consistent comparison:
468
- * { inherits: true, syntax: '<color>' } === { syntax: '<color>', inherits: true }
469
- */
470
- function normalizePropertyDefinition(def) {
471
- const normalized = {};
472
- if (def.inherits !== void 0) normalized.inherits = def.inherits;
473
- if (def.syntax !== void 0) normalized.syntax = def.syntax;
474
- return JSON.stringify(normalized);
475
- }
476
- /**
477
- * Get the effective property definition for a token.
478
- * For color tokens (#name), auto-sets syntax to '<color>' and defaults initialValue to 'transparent'.
479
- *
480
- * @param token - Property token ($name, #name, --name, or plain name)
481
- * @param userDefinition - User-provided definition options
482
- * @returns Effective definition with cssName, definition, isValid, and optional error
483
- */
484
- function getEffectiveDefinition(token, userDefinition) {
485
- const parsed = parsePropertyToken(token);
486
- if (!parsed.isValid) return {
487
- cssName: "",
488
- definition: userDefinition,
489
- isValid: false,
490
- error: parsed.error
491
- };
492
- if (parsed.isColor) return {
493
- cssName: parsed.cssName,
494
- definition: {
495
- syntax: "<color>",
496
- inherits: userDefinition.inherits,
497
- initialValue: userDefinition.initialValue ?? "transparent"
498
- },
499
- isValid: true
500
- };
501
- return {
502
- cssName: parsed.cssName,
503
- definition: userDefinition,
504
- isValid: true
505
- };
506
- }
507
- const UNIT_TO_SYNTAX = {};
508
- const LENGTH_UNITS = [
509
- "px",
510
- "em",
511
- "rem",
512
- "vw",
513
- "vh",
514
- "vmin",
515
- "vmax",
516
- "ch",
517
- "ex",
518
- "cap",
519
- "ic",
520
- "lh",
521
- "rlh",
522
- "svw",
523
- "svh",
524
- "lvw",
525
- "lvh",
526
- "dvw",
527
- "dvh",
528
- "cqw",
529
- "cqh",
530
- "cqi",
531
- "cqb",
532
- "cqmin",
533
- "cqmax"
534
- ];
535
- const ANGLE_UNITS = [
536
- "deg",
537
- "rad",
538
- "grad",
539
- "turn"
540
- ];
541
- const TIME_UNITS = ["ms", "s"];
542
- for (const u of LENGTH_UNITS) UNIT_TO_SYNTAX[u] = {
543
- syntax: "<length-percentage>",
544
- initialValue: "0px"
545
- };
546
- UNIT_TO_SYNTAX["%"] = {
547
- syntax: "<length-percentage>",
548
- initialValue: "0px"
549
- };
550
- for (const u of ANGLE_UNITS) UNIT_TO_SYNTAX[u] = {
551
- syntax: "<angle>",
552
- initialValue: "0deg"
553
- };
554
- for (const u of TIME_UNITS) UNIT_TO_SYNTAX[u] = {
555
- syntax: "<time>",
556
- initialValue: "0s"
557
- };
558
- /**
559
- * Infer CSS @property syntax from a concrete value.
560
- * Detects numeric types: \<number\>, \<length-percentage\>, \<angle\>, \<time\>.
561
- * Length and percentage values both map to \<length-percentage\> for maximum flexibility.
562
- * Color properties are handled separately via the #name token convention
563
- * (--name-color gets \<color\> syntax automatically in getEffectiveDefinition).
564
- *
565
- * @param value - The CSS value to infer from (e.g. '10px', '1', '45deg')
566
- * @returns Inferred syntax and initial value, or null if not inferable
567
- */
568
- function inferSyntaxFromValue(value) {
569
- if (!value || typeof value !== "string") return null;
570
- const trimmed = value.trim();
571
- if (!trimmed) return null;
572
- if (RE_NUMBER.test(trimmed)) {
573
- if (parseFloat(trimmed) === 0) return null;
574
- return {
575
- syntax: "<number>",
576
- initialValue: "0"
577
- };
578
- }
579
- const unitMatch = trimmed.match(RE_RAW_UNIT);
580
- if (unitMatch) {
581
- const mapping = UNIT_TO_SYNTAX[unitMatch[2]];
582
- if (mapping) return mapping;
583
- }
584
- return null;
585
- }
586
- //#endregion
587
- //#region src/utils/hash.ts
588
- /**
589
- * Fast string hash (djb2) for deduplication keys.
590
- * Not cryptographic — just collision-resistant enough for cache keys.
591
- */
592
- function hashString(str) {
593
- let hash = 5381;
594
- for (let i = 0; i < str.length; i++) hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
595
- return (hash >>> 0).toString(36);
596
- }
597
- //#endregion
598
- //#region src/utils/name-prefix.ts
599
- /**
600
- * Name prefix utilities for generated identifiers.
601
- *
602
- * Tasty generates three kinds of identifiers from content hashes:
603
- * - class names (used in DOM `class` attribute)
604
- * - keyframe names (used in CSS `animation`)
605
- * - counter-style names (used in CSS `list-style-type`)
606
- *
607
- * All three derive from a single configurable prefix so that an app
608
- * can namespace every identifier under one string. Discriminator letters
609
- * (`k`, `c`) keep the three kinds visually distinct in devtools — they
610
- * are not required for correctness (CSS keeps these in separate
611
- * namespaces), only for readability.
612
- *
613
- * The runtime / SSR / RSC paths must agree on the prefix; otherwise the
614
- * client-side hash for a given style will not match the server-rendered
615
- * class and hydration breaks. The zero-runtime build path uses a
616
- * different default (`'ts'`) so its classes can't collide with runtime
617
- * (`'t'`) classes when both are loaded on the same page.
618
- */
619
- /** Default prefix used by the runtime / SSR / RSC paths. */
620
- const DEFAULT_NAME_PREFIX = "t";
621
- /** Default prefix used by the zero-runtime (`tastyStatic`) build path. */
622
- const DEFAULT_ZERO_NAME_PREFIX = "ts";
623
- /**
624
- * Allowed shape: starts with a letter or underscore, then letters/
625
- * digits/underscore/hyphen. Length capped at 32 to keep generated
626
- * names sane. Matches the CSS identifier rules for the common case
627
- * while keeping the surface conservative.
628
- */
629
- const NAME_PREFIX_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$/;
630
- /**
631
- * Validate a `namePrefix` value.
632
- * Throws a TypeError with a descriptive message on invalid input so
633
- * misconfiguration fails loudly at `configure()` time rather than
634
- * surfacing later as broken hydration.
635
- */
636
- function validateNamePrefix(prefix) {
637
- if (typeof prefix !== "string") throw new TypeError(`[Tasty] namePrefix must be a string, got ${typeof prefix}.`);
638
- if (!NAME_PREFIX_PATTERN.test(prefix)) throw new TypeError(`[Tasty] namePrefix "${prefix}" is invalid. It must start with a letter (a-z, A-Z) or "_", contain only letters, digits, "_" or "-", and be 1-32 characters long. Examples: "t", "ts", "myapp-", "_foo".`);
639
- }
640
- /**
641
- * Build a class name: `${prefix}${hash}`.
642
- * The hash is appended verbatim — supply a separator inside the prefix
643
- * itself if you want one (e.g. `'myapp-'`).
644
- */
645
- function makeClassName(prefix, hash) {
646
- return `${prefix}${hash}`;
647
- }
648
- /**
649
- * Build a keyframe name: `${prefix}k${suffix}`.
650
- * The `k` discriminator keeps keyframe names visually distinct from
651
- * class names sharing the same prefix. `suffix` is typically a content
652
- * hash but may be a counter for ad-hoc allocation.
653
- */
654
- function makeKeyframeName(prefix, suffix) {
655
- return `${prefix}k${suffix}`;
656
- }
657
- /**
658
- * Build a counter-style name: `${prefix}c${suffix}`.
659
- * The `c` discriminator keeps counter-style names visually distinct
660
- * from class names sharing the same prefix.
661
- */
662
- function makeCounterStyleName(prefix, suffix) {
663
- return `${prefix}c${suffix}`;
664
- }
665
- /** Escape a string for safe inclusion in a regex literal. */
666
- function escapeRegex(str) {
667
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
668
- }
669
- /**
670
- * Regex matching any tasty class for the given prefix.
671
- * Used by the runtime GC's DOM scan and class-allocation bookkeeping.
672
- */
673
- function tastyClassRegex(prefix) {
674
- return new RegExp(`^${escapeRegex(prefix)}[a-z0-9]+$`);
675
- }
676
- /**
677
- * Global regex extracting tasty class names from RSC-inlined CSS.
678
- * Looks for the doubled-specificity pattern `.cls.cls` that
679
- * `formatRules()` always emits, which makes extraction reliable.
680
- */
681
- function rscClassRegexGlobal(prefix) {
682
- return new RegExp(`\\.(${escapeRegex(prefix)}[a-z0-9]+)\\.\\1`, "g");
683
- }
684
- //#endregion
685
- //#region src/injector/batch.ts
686
- /**
687
- * Deferred CSSOM writes ("batched injection").
688
- *
689
- * Every `insertRule()` on a live stylesheet invalidates style for the sheet's
690
- * scope, so Blink recalculates style the next time anything reads layout or
691
- * computed style. When components inject during React's render phase and other
692
- * components read layout in the same pass, the two interleave:
693
- *
694
- * inject -> read (forced recalc) -> inject -> read (forced recalc) -> ...
695
- *
696
- * Batching moves every sheet write out from between those reads. Writes are
697
- * queued in FIFO order and drained in one go, so the tree is invalidated once
698
- * per flush instead of once per component.
699
- *
700
- * ## Ordering
701
- *
702
- * A single queue holds *all* writes — component rules, global rules, keyframes,
703
- * `@property`, `@font-face`, `@counter-style`, `@function` and raw CSS. Draining
704
- * it in insertion order keeps the sheet byte-identical to unbatched output,
705
- * which matters because equal-specificity rules resolve by document order.
706
- *
707
- * ## Batch windows — why queuing is safe
708
- *
709
- * Deferring a write past React's layout phase would let a `useLayoutEffect`
710
- * measure an element whose rules are not in the sheet yet, reading the unstyled
711
- * box. `<TastyBatchProvider>` closes that hole by opening a *window* during its
712
- * render and closing it — flushing — in its `useInsertionEffect`, which React
713
- * runs in the mutation phase, before any layout effect:
714
- *
715
- * provider renders -> window OPEN
716
- * children render -> injections queued
717
- * provider insertionEffect -> FLUSH, window CLOSED
718
- * layout effects run -> rules are in the sheet
719
- *
720
- * So in the default (`batchInjection: true`) mode a write is only ever queued
721
- * inside a commit whose flush is already guaranteed by that same commit. Every
722
- * injection outside a window — a deep update the provider did not re-render
723
- * for, a `useLayoutEffect` that injects, an event handler, an async callback —
724
- * is written straight through, exactly as with batching off.
725
- *
726
- * `batchInjection: 'always'` opts out of the gate and queues unconditionally.
727
- * That wins on more commits, at the cost of the measurement hazard above.
728
- *
729
- * ## Flush points (earliest wins)
730
- *
731
- * 1. `<TastyBatchProvider>`'s `useInsertionEffect` — closes the window.
732
- * 2. A microtask — a backstop for a render that was aborted or suspended and
733
- * therefore never reached its insertion effect. An aborted render mounts
734
- * nothing, so nothing can measure what it queued. Microtasks also always
735
- * drain before paint, so styles are never visually missing.
736
- * 3. `flushStyles()` — explicit, and called internally by every injector read
737
- * API (`getCSSText`, `cleanup`, `gc`, `destroy`, ...).
738
- */
739
- /**
740
- * FIFO of pending writes. Drained with a moving `head` index rather than
741
- * `shift()` so a large batch stays O(n) instead of O(n²).
742
- */
743
- let queue = [];
744
- let head = 0;
745
- let microtaskScheduled = false;
746
- let flushing = false;
747
- /**
748
- * Whether a `<TastyBatchProvider>` has rendered in the current commit and will
749
- * therefore flush in its insertion effect, making it safe to queue a write.
750
- *
751
- * A flag rather than a depth count, because renders and insertion effects do
752
- * not pair up one-to-one. StrictMode double-invokes render but runs the
753
- * insertion effect once, so a counter ends the commit stuck above zero — and a
754
- * window that stays open past its commit turns the next provider-less commit
755
- * into `'always'` mode behind the user's back, which is exactly the measurement
756
- * hazard `true` exists to avoid.
757
- *
758
- * Clearing on the first close is safe with nested or sibling providers: React
759
- * finishes every render in a commit before running any insertion effect, so once
760
- * one closes, no further render-phase injection can arrive in that commit.
761
- */
762
- let windowOpen = false;
763
- /** Whether any window has ever been opened, i.e. a provider is in the tree. */
764
- let everOpened = false;
765
- /** Dev-only: warn at most once that batching is on with no provider mounted. */
766
- let warnedNoProvider = false;
767
- /**
768
- * Queue a sheet write.
769
- *
770
- * Returns a handle whose `cancel()` drops the write if the caller disposes
771
- * before the flush. The handle is intentionally tiny — one is allocated per
772
- * injected class, on the cache-miss path only.
773
- */
774
- function enqueueStyleWrite(run) {
775
- if (flushing) {
776
- run();
777
- return {
778
- run,
779
- cancelled: false,
780
- done: true
781
- };
782
- }
783
- const entry = {
784
- run,
785
- cancelled: false,
786
- done: false
787
- };
788
- queue.push(entry);
789
- scheduleMicrotaskFlush();
790
- return entry;
791
- }
792
- /** Whether any write is still waiting to hit a stylesheet. */
793
- function hasPendingStyleWrites() {
794
- return head < queue.length;
795
- }
796
- /**
797
- * Open a batch window. Called from `<TastyBatchProvider>`'s render, so every
798
- * descendant injection in this commit is covered by the provider's insertion
799
- * effect.
800
- *
801
- * Called during render on purpose: the window must be open before children
802
- * render. It is idempotent — so StrictMode's double render costs nothing — and
803
- * carries no other side effect. A render that is thrown away is recovered by the
804
- * microtask backstop.
805
- *
806
- * A no-op without a `document`. On the server there is no sheet to batch
807
- * against, and `useInsertionEffect` never runs, so nothing would ever close a
808
- * window this opened. Skipping it keeps the provider inert during SSR and RSC
809
- * instead of merely harmless.
810
- */
811
- function openBatchWindow() {
812
- if (typeof document === "undefined") return;
813
- windowOpen = true;
814
- everOpened = true;
815
- }
816
- /**
817
- * Close a batch window and flush. Called from the provider's
818
- * `useInsertionEffect`, i.e. after every render in the commit and before any
819
- * layout effect.
820
- */
821
- function closeBatchWindow() {
822
- windowOpen = false;
823
- flushStyles();
824
- }
825
- /** Whether a batch window is currently open. */
826
- function isBatchWindowOpen() {
827
- return windowOpen;
828
- }
829
- function scheduleMicrotaskFlush() {
830
- if (microtaskScheduled || flushing) return;
831
- microtaskScheduled = true;
832
- queueMicrotask(() => {
833
- microtaskScheduled = false;
834
- windowOpen = false;
835
- flushStyles();
836
- });
837
- }
838
- /**
839
- * Drain every pending sheet write, in insertion order.
840
- *
841
- * Safe to call when the queue is empty (the common case, so the guard comes
842
- * first) and safe to call re-entrantly: a nested `flushStyles()` is a no-op,
843
- * and work a draining write triggers is written in place by
844
- * `enqueueStyleWrite` rather than queued behind the rest of the batch.
845
- */
846
- function flushStyles() {
847
- if (head >= queue.length) return;
848
- if (flushing) return;
849
- flushing = true;
850
- try {
851
- while (head < queue.length) {
852
- const entry = queue[head++];
853
- if (entry.cancelled) {
854
- entry.done = true;
855
- continue;
856
- }
857
- entry.run();
858
- entry.done = true;
859
- }
860
- } finally {
861
- queue = [];
862
- head = 0;
863
- flushing = false;
864
- }
865
- }
866
- /**
867
- * Dev-only: `batchInjection: true` batches nothing unless a window opens, and no
868
- * window has ever opened, so no provider is in the tree. Called by the injector
869
- * the first time it declines to batch.
870
- */
871
- function warnBatchProviderMissing() {
872
- if (everOpened || warnedNoProvider || !isDevEnv()) return;
873
- warnedNoProvider = true;
874
- warn("[Tasty] batchInjection needs <TastyBatchProvider> mounted to batch.");
875
- }
876
- /**
877
- * Drop every pending write without applying it. Test helper — production code
878
- * should call `flushStyles()` instead.
879
- */
880
- function resetStyleBatch() {
881
- queue = [];
882
- head = 0;
883
- microtaskScheduled = false;
884
- flushing = false;
885
- windowOpen = false;
886
- everOpened = false;
887
- warnedNoProvider = false;
888
- }
889
- //#endregion
890
- //#region src/properties/property-type-resolver.ts
891
- /**
892
- * PropertyTypeResolver
893
- *
894
- * Automatically infers CSS @property types from custom property values.
895
- * Supports deferred resolution for var() reference chains of arbitrary depth.
896
- */
897
- /**
898
- * One `--name: value` declaration, with the surrounding whitespace dropped.
899
- *
900
- * The value is anchored on `\S` at both ends so no two quantifiers can claim
901
- * the same character. A lazy `(.+?)\s*$` is quadratic on a value of nothing
902
- * but spaces — every split point gets tried (CodeQL `js/polynomial-redos`).
903
- */
904
- const CUSTOM_PROP_DECL = /^\s*(--[a-z0-9_-]+)\s*:\s*(\S|\S.*\S)\s*$/i;
905
- const SINGLE_VAR_REF = /^var\((--[a-z0-9_-]+)\)$/i;
906
- var PropertyTypeResolver = class {
907
- /** propName → the prop it depends on */
908
- pendingDeps = /* @__PURE__ */ new Map();
909
- /** propName → list of props waiting on it */
910
- reverseDeps = /* @__PURE__ */ new Map();
911
- /**
912
- * Scan CSS declarations and auto-register @property for custom properties
913
- * whose types can be inferred from their values.
914
- */
915
- scanDeclarations(declarations, isPropertyDefined, registerProperty) {
916
- if (!declarations.includes("--")) return;
917
- const parts = declarations.split(/;+/);
918
- for (const part of parts) {
919
- if (!part.trim()) continue;
920
- const match = CUSTOM_PROP_DECL.exec(part);
921
- if (!match) continue;
922
- const propName = match[1];
923
- const value = match[2].trim();
924
- if (isPropertyDefined(propName)) continue;
925
- if (propName.endsWith("-color")) {
926
- registerProperty(propName, "<color>", "transparent");
927
- continue;
928
- }
929
- if (propName.endsWith("-line-height")) {
930
- registerProperty(propName, "<number> | <length-percentage>", "0");
931
- continue;
932
- }
933
- if (propName.endsWith("-opacity")) {
934
- registerProperty(propName, "<number> | <percentage>", "0");
935
- continue;
936
- }
937
- const varMatch = SINGLE_VAR_REF.exec(value);
938
- if (varMatch) {
939
- const depName = varMatch[1];
940
- this.addDependency(propName, depName);
941
- continue;
942
- }
943
- if (this.isComplexValue(value)) continue;
944
- const inferred = inferSyntaxFromValue(value);
945
- if (!inferred) continue;
946
- this.resolve(propName, inferred.syntax, inferred.initialValue, isPropertyDefined, registerProperty);
947
- }
948
- }
949
- addDependency(propName, depName) {
950
- if (propName === depName) return;
951
- this.pendingDeps.set(propName, depName);
952
- let dependents = this.reverseDeps.get(depName);
953
- if (!dependents) {
954
- dependents = [];
955
- this.reverseDeps.set(depName, dependents);
956
- }
957
- if (!dependents.includes(propName)) dependents.push(propName);
958
- }
959
- resolve(propName, syntax, initialValue, isPropertyDefined, registerProperty, resolving) {
960
- if (!resolving) resolving = /* @__PURE__ */ new Set();
961
- if (resolving.has(propName)) return;
962
- resolving.add(propName);
963
- if (!isPropertyDefined(propName)) registerProperty(propName, syntax, initialValue);
964
- const dependents = this.reverseDeps.get(propName);
965
- if (dependents) {
966
- this.reverseDeps.delete(propName);
967
- for (const dependent of dependents) {
968
- this.pendingDeps.delete(dependent);
969
- if (isPropertyDefined(dependent)) continue;
970
- this.resolve(dependent, syntax, initialValue, isPropertyDefined, registerProperty, resolving);
971
- }
972
- }
973
- }
974
- isComplexValue(value) {
975
- if (value.includes("calc(")) return true;
976
- const firstVar = value.indexOf("var(");
977
- if (firstVar === -1) return false;
978
- if (value.indexOf("var(", firstVar + 4) !== -1) return true;
979
- return !SINGLE_VAR_REF.test(value);
980
- }
981
- };
982
- //#endregion
983
- //#region src/injector/sheet-manager.ts
984
- const supportsConstructableSheets = typeof CSSStyleSheet !== "undefined" && (() => {
985
- try {
986
- new CSSStyleSheet();
987
- return true;
988
- } catch {
989
- return false;
990
- }
991
- })();
992
- var SheetManager = class {
993
- rootRegistries = /* @__PURE__ */ new WeakMap();
994
- /** Strong set of active roots so background GC can iterate them all */
995
- activeRoots = /* @__PURE__ */ new Set();
996
- config;
997
- /** Dedicated style elements for raw CSS per root */
998
- rawStyleElements = /* @__PURE__ */ new WeakMap();
999
- /** Constructable sheets for raw CSS in adopted mode */
1000
- rawConstructableSheets = /* @__PURE__ */ new WeakMap();
1001
- /** Tracking for raw CSS blocks per root */
1002
- rawCSSBlocks = /* @__PURE__ */ new WeakMap();
1003
- /** Counter for generating unique raw CSS IDs */
1004
- rawCSSCounter = 0;
1005
- constructor(config) {
1006
- this.config = config;
1007
- }
1008
- /**
1009
- * Resolve the underlying CSSStyleSheet from a SheetInfo,
1010
- * abstracting away adopted vs style-element modes.
1011
- */
1012
- getCSSSheet(sheetInfo) {
1013
- if (sheetInfo.constructableSheet) return sheetInfo.constructableSheet;
1014
- return sheetInfo.sheet?.sheet ?? null;
1015
- }
1016
- /**
1017
- * Record an inserted rule text at its rule index (text mode only).
1018
- *
1019
- * `textRules` mirrors the sheet's rule order. `textContent` cannot be edited
1020
- * rule-by-rule the way CSSOM can, so keeping the texts is what makes
1021
- * deletion possible at all in this mode.
1022
- */
1023
- trackTextRule(sheet, ruleIndex, ruleText) {
1024
- if (!sheet.textMode) return;
1025
- const rules = sheet.textRules ??= [];
1026
- while (rules.length < ruleIndex) rules.push("");
1027
- rules[ruleIndex] = ruleText;
1028
- }
1029
- /**
1030
- * Remove rule indices from a text-mode sheet and rewrite the element's text.
1031
- * Returns the indices that were actually removed.
1032
- */
1033
- deleteTextRules(sheet, indices) {
1034
- const rules = sheet.textRules;
1035
- if (!rules) return [];
1036
- const removed = [...new Set(indices)].filter((idx) => idx >= 0 && idx < rules.length).sort((a, b) => b - a);
1037
- for (const idx of removed) rules.splice(idx, 1);
1038
- if (removed.length > 0 && sheet.sheet) sheet.sheet.textContent = rules.length ? "\n" + rules.join("\n") : "";
1039
- return removed;
1040
- }
1041
- /**
1042
- * Determine the injection mode for a root.
1043
- * ShadowRoot uses adopted stylesheets when supported; Document uses <style> elements.
1044
- */
1045
- detectInjectionMode(root) {
1046
- if (root instanceof ShadowRoot && supportsConstructableSheets && !this.config.forceTextInjection) return "adopted";
1047
- return "style-element";
1048
- }
1049
- /**
1050
- * Get or create registry for a root (Document or ShadowRoot)
1051
- */
1052
- getRegistry(root) {
1053
- let registry = this.rootRegistries.get(root);
1054
- if (!registry) {
1055
- const metrics = this.config.devMode ? {
1056
- hits: 0,
1057
- misses: 0,
1058
- bulkCleanups: 0,
1059
- totalInsertions: 0,
1060
- totalUnused: 0,
1061
- stylesCleanedUp: 0,
1062
- cleanupHistory: [],
1063
- startTime: Date.now()
1064
- } : void 0;
1065
- registry = {
1066
- sheets: [],
1067
- pinCounts: /* @__PURE__ */ new Map(),
1068
- rules: /* @__PURE__ */ new Map(),
1069
- cacheKeyToClassName: /* @__PURE__ */ new Map(),
1070
- ruleTextSet: /* @__PURE__ */ new Set(),
1071
- metrics,
1072
- keyframesCache: /* @__PURE__ */ new Map(),
1073
- keyframesNameToContent: /* @__PURE__ */ new Map(),
1074
- keyframesCounter: 0,
1075
- injectedProperties: /* @__PURE__ */ new Map(),
1076
- injectedFontFaces: /* @__PURE__ */ new Set(),
1077
- injectedCounterStyles: /* @__PURE__ */ new Map(),
1078
- injectedFunctions: /* @__PURE__ */ new Map(),
1079
- globalRules: /* @__PURE__ */ new Map(),
1080
- propertyTypeResolver: new PropertyTypeResolver(),
1081
- unusedSince: /* @__PURE__ */ new Map(),
1082
- localKeyframes: /* @__PURE__ */ new Map(),
1083
- touchCount: 0,
1084
- serverClassSyncIndex: 0,
1085
- rscStylesScanned: false,
1086
- injectionMode: this.detectInjectionMode(root)
1087
- };
1088
- this.rootRegistries.set(root, registry);
1089
- this.activeRoots.add(root);
1090
- }
1091
- return registry;
1092
- }
1093
- /** Return all roots with active registries (for background GC sweep). */
1094
- getActiveRoots() {
1095
- return this.activeRoots;
1096
- }
1097
- /** Check whether any roots have active registries. */
1098
- hasActiveRoots() {
1099
- return this.activeRoots.size > 0;
1100
- }
1101
- /** Remove registries for ShadowRoots whose host has been detached from the DOM. */
1102
- pruneDisconnectedRoots() {
1103
- const toPrune = [];
1104
- for (const root of this.activeRoots) if (root !== document && !root.host?.isConnected) toPrune.push(root);
1105
- for (const root of toPrune) this.cleanup(root);
1106
- }
1107
- /**
1108
- * Create a new stylesheet for the registry.
1109
- * In adopted mode (ShadowRoot), creates a constructable CSSStyleSheet and
1110
- * pushes it to adoptedStyleSheets. Otherwise creates a <style> element.
1111
- */
1112
- createSheet(registry, root) {
1113
- if (registry.injectionMode === "adopted") {
1114
- const constructableSheet = new CSSStyleSheet();
1115
- root.adoptedStyleSheets = [...root.adoptedStyleSheets, constructableSheet];
1116
- const sheetInfo = {
1117
- sheet: null,
1118
- constructableSheet,
1119
- ruleCount: 0,
1120
- holes: []
1121
- };
1122
- registry.sheets.push(sheetInfo);
1123
- return sheetInfo;
1124
- }
1125
- const sheet = this.createStyleElement(root);
1126
- const textMode = this.config.forceTextInjection === true || sheet.sheet == null;
1127
- const sheetInfo = {
1128
- sheet,
1129
- ruleCount: 0,
1130
- holes: [],
1131
- textMode,
1132
- ...textMode ? { textRules: [] } : {}
1133
- };
1134
- registry.sheets.push(sheetInfo);
1135
- return sheetInfo;
1136
- }
1137
- /**
1138
- * Create a style element and append to document
1139
- */
1140
- createStyleElement(root) {
1141
- const style = root.createElement?.("style") || document.createElement("style");
1142
- if (this.config.nonce) style.nonce = this.config.nonce;
1143
- style.setAttribute("data-tasty", "");
1144
- if ("head" in root && root.head) root.head.appendChild(style);
1145
- else if ("appendChild" in root) root.appendChild(style);
1146
- else document.head.appendChild(style);
1147
- if (!style.isConnected && !this.config.forceTextInjection) console.error("[Tasty] SheetManager: style element failed to connect to the DOM.", {
1148
- parentNode: style.parentNode?.nodeName,
1149
- isConnected: style.isConnected
1150
- });
1151
- return style;
1152
- }
1153
- /**
1154
- * Insert CSS rules as a single block
1155
- */
1156
- insertRule(registry, flattenedRules, className, root) {
1157
- let targetSheet = this.findAvailableSheet(registry, root);
1158
- if (!targetSheet) targetSheet = this.createSheet(registry, root);
1159
- const sheetIndex = registry.sheets.indexOf(targetSheet);
1160
- try {
1161
- const groupedRules = [];
1162
- const groupMap = /* @__PURE__ */ new Map();
1163
- const atKey = (at) => at && at.length ? at.join("|") : "";
1164
- flattenedRules.forEach((r) => {
1165
- const key = `${atKey(r.atRules)}||${r.selector}||${r.startingStyle ? "1" : "0"}`;
1166
- const existing = groupMap.get(key);
1167
- if (existing) existing.declarations = existing.declarations ? `${existing.declarations} ${r.declarations}` : r.declarations;
1168
- else {
1169
- groupMap.set(key, {
1170
- idx: groupedRules.length,
1171
- selector: r.selector,
1172
- atRules: r.atRules,
1173
- startingStyle: r.startingStyle,
1174
- declarations: r.declarations
1175
- });
1176
- groupedRules.push({ ...r });
1177
- }
1178
- });
1179
- groupMap.forEach((val) => {
1180
- groupedRules[val.idx] = {
1181
- selector: val.selector,
1182
- atRules: val.atRules,
1183
- startingStyle: val.startingStyle,
1184
- declarations: val.declarations
1185
- };
1186
- });
1187
- const insertedRuleTexts = [];
1188
- const insertedIndices = [];
1189
- let currentRuleIndex = this.findAvailableRuleIndex(targetSheet);
1190
- let firstInsertedIndex = null;
1191
- let lastInsertedIndex = null;
1192
- for (const rule of groupedRules) {
1193
- const declarations = rule.declarations;
1194
- const innerContent = rule.startingStyle ? `@starting-style { ${declarations} }` : declarations;
1195
- const baseRule = `${rule.selector} { ${innerContent} }`;
1196
- let fullRule = baseRule;
1197
- if (rule.atRules && rule.atRules.length > 0) fullRule = rule.atRules.reduce((css, atRule) => `${atRule} { ${css} }`, baseRule);
1198
- const styleElement = targetSheet.sheet;
1199
- const styleSheet = this.getCSSSheet(targetSheet);
1200
- if (!targetSheet.textMode && styleSheet) {
1201
- const maxIndex = styleSheet.cssRules.length;
1202
- const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
1203
- const safeIndex = Math.min(Math.max(0, atomicRuleIndex), maxIndex);
1204
- const splitSelectorsSafely = (selectorList) => {
1205
- const parts = [];
1206
- let buf = "";
1207
- let depthSq = 0;
1208
- let depthPar = 0;
1209
- let inStr = "";
1210
- for (let i = 0; i < selectorList.length; i++) {
1211
- const ch = selectorList[i];
1212
- if (inStr) {
1213
- if (ch === inStr && selectorList[i - 1] !== "\\") inStr = "";
1214
- buf += ch;
1215
- continue;
1216
- }
1217
- if (ch === "\"" || ch === "'") {
1218
- inStr = ch;
1219
- buf += ch;
1220
- continue;
1221
- }
1222
- if (ch === "[") depthSq++;
1223
- else if (ch === "]") depthSq = Math.max(0, depthSq - 1);
1224
- else if (ch === "(") depthPar++;
1225
- else if (ch === ")") depthPar = Math.max(0, depthPar - 1);
1226
- if (ch === "," && depthSq === 0 && depthPar === 0) {
1227
- const part = buf.trim();
1228
- if (part) parts.push(part);
1229
- buf = "";
1230
- } else buf += ch;
1231
- }
1232
- const tail = buf.trim();
1233
- if (tail) parts.push(tail);
1234
- return parts;
1235
- };
1236
- try {
1237
- styleSheet.insertRule(fullRule, safeIndex);
1238
- targetSheet.ruleCount++;
1239
- insertedIndices.push(safeIndex);
1240
- if (firstInsertedIndex == null) firstInsertedIndex = safeIndex;
1241
- lastInsertedIndex = safeIndex;
1242
- currentRuleIndex = safeIndex + 1;
1243
- } catch (e) {
1244
- const selectors = splitSelectorsSafely(rule.selector);
1245
- if (selectors.length > 1) {
1246
- let anyInserted = false;
1247
- for (const sel of selectors) {
1248
- const singleBase = `${sel} { ${declarations} }`;
1249
- let singleRule = singleBase;
1250
- if (rule.atRules && rule.atRules.length > 0) singleRule = rule.atRules.reduce((css, atRule) => `${atRule} { ${css} }`, singleBase);
1251
- try {
1252
- const maxIdx = styleSheet.cssRules.length;
1253
- const atomicIdx = this.findAvailableRuleIndex(targetSheet);
1254
- const idx = Math.min(Math.max(0, atomicIdx), maxIdx);
1255
- styleSheet.insertRule(singleRule, idx);
1256
- targetSheet.ruleCount++;
1257
- insertedIndices.push(idx);
1258
- if (firstInsertedIndex == null) firstInsertedIndex = idx;
1259
- lastInsertedIndex = idx;
1260
- currentRuleIndex = idx + 1;
1261
- anyInserted = true;
1262
- } catch (singleErr) {
1263
- console.warn("[Tasty] Browser rejected CSS rule:", singleRule, singleErr);
1264
- }
1265
- }
1266
- if (!anyInserted) {}
1267
- } else if (!(fullRule.startsWith("@property ") && !this.engineSupportsAtProperty(registry, styleSheet))) console.warn("[Tasty] Browser rejected CSS rule:", fullRule, e);
1268
- }
1269
- } else if (styleElement) {
1270
- const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
1271
- this.trackTextRule(targetSheet, atomicRuleIndex, fullRule);
1272
- styleElement.textContent = (styleElement.textContent || "") + "\n" + fullRule;
1273
- targetSheet.ruleCount++;
1274
- insertedIndices.push(atomicRuleIndex);
1275
- if (firstInsertedIndex == null) firstInsertedIndex = atomicRuleIndex;
1276
- lastInsertedIndex = atomicRuleIndex;
1277
- currentRuleIndex = atomicRuleIndex + 1;
1278
- }
1279
- if (styleElement && !styleElement.parentNode && !this.config.forceTextInjection) console.error("[Tasty] SheetManager: style element is not attached to the DOM; rules will not apply.", {
1280
- className,
1281
- ruleIndex: currentRuleIndex
1282
- });
1283
- if (this.config.devMode) {
1284
- insertedRuleTexts.push(fullRule);
1285
- try {
1286
- registry.ruleTextSet.add(fullRule);
1287
- } catch {}
1288
- }
1289
- }
1290
- if (insertedIndices.length === 0) return null;
1291
- return {
1292
- className,
1293
- ruleIndex: firstInsertedIndex ?? 0,
1294
- sheetIndex,
1295
- cssText: this.config.devMode ? insertedRuleTexts : void 0,
1296
- endRuleIndex: lastInsertedIndex ?? firstInsertedIndex ?? 0,
1297
- indices: insertedIndices
1298
- };
1299
- } catch (error) {
1300
- console.warn("[Tasty] Failed to insert CSS rules:", error, {
1301
- flattenedRules,
1302
- className
1303
- });
1304
- return null;
1305
- }
1306
- }
1307
- /**
1308
- * Insert global CSS rules
1309
- */
1310
- insertGlobalRule(registry, flattenedRules, globalKey, root) {
1311
- const ruleInfo = this.insertRule(registry, flattenedRules, globalKey, root);
1312
- if (ruleInfo) registry.globalRules.set(globalKey, ruleInfo);
1313
- return ruleInfo;
1314
- }
1315
- /**
1316
- * Delete a global CSS rule by key
1317
- */
1318
- deleteGlobalRule(registry, globalKey) {
1319
- const ruleInfo = registry.globalRules.get(globalKey);
1320
- if (!ruleInfo) return;
1321
- this.deleteRule(registry, ruleInfo);
1322
- registry.globalRules.delete(globalKey);
1323
- }
1324
- /**
1325
- * Adjust rule indices after deletion to account for shifting
1326
- */
1327
- adjustIndicesAfterDeletion(registry, sheetIndex, startIdx, endIdx, deleteCount, deletedRuleInfo, deletedIndices) {
1328
- try {
1329
- const sortedDeleted = deletedIndices && deletedIndices.length > 0 ? [...deletedIndices].sort((a, b) => a - b) : null;
1330
- const countDeletedBefore = (sorted, idx) => {
1331
- let shift = 0;
1332
- for (const delIdx of sorted) if (delIdx < idx) shift++;
1333
- else break;
1334
- return shift;
1335
- };
1336
- const adjustRuleInfo = (info) => {
1337
- if (info === deletedRuleInfo) return;
1338
- if (info.sheetIndex !== sheetIndex) return;
1339
- if (!info.indices || info.indices.length === 0) return;
1340
- if (sortedDeleted) info.indices = info.indices.map((idx) => {
1341
- return idx - countDeletedBefore(sortedDeleted, idx);
1342
- });
1343
- else info.indices = info.indices.map((idx) => idx > endIdx ? Math.max(0, idx - deleteCount) : idx);
1344
- if (info.indices.length > 0) {
1345
- info.ruleIndex = Math.min(...info.indices);
1346
- info.endRuleIndex = Math.max(...info.indices);
1347
- }
1348
- };
1349
- for (const info of registry.rules.values()) adjustRuleInfo(info);
1350
- for (const info of registry.globalRules.values()) adjustRuleInfo(info);
1351
- for (const entry of registry.keyframesCache.values()) {
1352
- const ki = entry.info;
1353
- if (ki.sheetIndex !== sheetIndex) continue;
1354
- if (sortedDeleted) {
1355
- const shift = countDeletedBefore(sortedDeleted, ki.ruleIndex);
1356
- if (shift > 0) ki.ruleIndex = Math.max(0, ki.ruleIndex - shift);
1357
- } else if (ki.ruleIndex > endIdx) ki.ruleIndex = Math.max(0, ki.ruleIndex - deleteCount);
1358
- }
1359
- } catch {}
1360
- }
1361
- /**
1362
- * Delete a CSS rule from the sheet
1363
- */
1364
- deleteRule(registry, ruleInfo) {
1365
- const sheet = registry.sheets[ruleInfo.sheetIndex];
1366
- if (!sheet) return;
1367
- try {
1368
- const texts = this.config.devMode && Array.isArray(ruleInfo.cssText) ? ruleInfo.cssText.slice() : [];
1369
- const styleSheet = this.getCSSSheet(sheet);
1370
- if (sheet.textMode) {
1371
- let targetIndices;
1372
- if (ruleInfo.indices && ruleInfo.indices.length > 0) targetIndices = ruleInfo.indices;
1373
- else {
1374
- const startIdx = Math.max(0, ruleInfo.ruleIndex);
1375
- const endIdx = Math.min((sheet.textRules?.length ?? 0) - 1, Number.isFinite(ruleInfo.endRuleIndex) ? ruleInfo.endRuleIndex : startIdx);
1376
- targetIndices = [];
1377
- for (let idx = startIdx; idx <= endIdx; idx++) targetIndices.push(idx);
1378
- }
1379
- const deletedIndices = this.deleteTextRules(sheet, targetIndices);
1380
- if (deletedIndices.length > 0) {
1381
- sheet.ruleCount = Math.max(0, sheet.ruleCount - deletedIndices.length);
1382
- this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, Math.min(...deletedIndices), Math.max(...deletedIndices), deletedIndices.length, ruleInfo, deletedIndices);
1383
- }
1384
- } else if (styleSheet) {
1385
- const rules = styleSheet.cssRules;
1386
- if (ruleInfo.indices && ruleInfo.indices.length > 0) {
1387
- const sortedIndices = [...ruleInfo.indices].sort((a, b) => b - a);
1388
- const deletedIndices = [];
1389
- for (const idx of sortedIndices) if (idx >= 0 && idx < styleSheet.cssRules.length) try {
1390
- styleSheet.deleteRule(idx);
1391
- deletedIndices.push(idx);
1392
- } catch (e) {
1393
- console.warn(`[Tasty] Failed to delete rule at index ${idx}:`, e);
1394
- }
1395
- sheet.ruleCount = Math.max(0, sheet.ruleCount - deletedIndices.length);
1396
- if (deletedIndices.length > 0) this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, Math.min(...deletedIndices), Math.max(...deletedIndices), deletedIndices.length, ruleInfo, deletedIndices);
1397
- } else {
1398
- const startIdx = Math.max(0, ruleInfo.ruleIndex);
1399
- const endIdx = Math.min(rules.length - 1, Number.isFinite(ruleInfo.endRuleIndex) ? ruleInfo.endRuleIndex : startIdx);
1400
- if (Number.isFinite(startIdx) && endIdx >= startIdx) {
1401
- const deleteCount = endIdx - startIdx + 1;
1402
- for (let idx = endIdx; idx >= startIdx; idx--) {
1403
- if (idx < 0 || idx >= styleSheet.cssRules.length) continue;
1404
- styleSheet.deleteRule(idx);
1405
- }
1406
- sheet.ruleCount = Math.max(0, sheet.ruleCount - deleteCount);
1407
- this.adjustIndicesAfterDeletion(registry, ruleInfo.sheetIndex, startIdx, endIdx, deleteCount, ruleInfo);
1408
- }
1409
- }
1410
- }
1411
- if (this.config.devMode && texts.length) try {
1412
- for (const text of texts) registry.ruleTextSet.delete(text);
1413
- } catch {}
1414
- } catch (error) {
1415
- console.warn("[Tasty] Failed to delete CSS rule:", error);
1416
- }
1417
- }
1418
- /**
1419
- * Find a sheet with available space or return null
1420
- */
1421
- findAvailableSheet(registry, _root) {
1422
- const maxRules = this.config.maxRulesPerSheet;
1423
- if (!maxRules) return registry.sheets[registry.sheets.length - 1] || null;
1424
- for (const sheet of registry.sheets) if (sheet.ruleCount < maxRules) return sheet;
1425
- return null;
1426
- }
1427
- /**
1428
- * Find an available rule index in the sheet
1429
- */
1430
- findAvailableRuleIndex(sheet) {
1431
- return sheet.ruleCount;
1432
- }
1433
- /**
1434
- * Probe whether the underlying CSS engine supports `@property` at-rules.
1435
- * Result is cached per registry on `registry.atPropertySupported`.
1436
- *
1437
- * The probe inserts and immediately deletes a minimal known-valid rule
1438
- * (`@property --__tasty_probe__ { syntax: "*"; inherits: true; }`).
1439
- * Engines that lack `@property` support (jsdom, happy-dom) reject any
1440
- * `@property` rule including this one, so a probe failure is a reliable
1441
- * signal that further `@property` rejections are environmental noise and
1442
- * not user-authored bugs.
1443
- *
1444
- * The probe is intentionally a separate operation from the user's failing
1445
- * insertion: we don't want to leak `--__tasty_probe__` into the sheet, so
1446
- * on success we delete the probe rule immediately, leaving `ruleCount`
1447
- * and `cssRules.length` unchanged.
1448
- */
1449
- engineSupportsAtProperty(registry, styleSheet) {
1450
- if (registry.atPropertySupported !== void 0) return registry.atPropertySupported;
1451
- const probeRule = "@property --__tasty_probe__ { syntax: \"*\"; inherits: true; }";
1452
- try {
1453
- const probeIdx = styleSheet.cssRules.length;
1454
- styleSheet.insertRule(probeRule, probeIdx);
1455
- try {
1456
- styleSheet.deleteRule(probeIdx);
1457
- } catch {}
1458
- registry.atPropertySupported = true;
1459
- } catch {
1460
- registry.atPropertySupported = false;
1461
- }
1462
- return registry.atPropertySupported;
1463
- }
1464
- /**
1465
- * Delete the given classes: their rules leave the sheets and every registry
1466
- * entry pointing at them is dropped.
1467
- *
1468
- * Deciding *what* is unused belongs to `StyleInjector.gc()`, which owns the
1469
- * DOM scan and the capacity policy. This only re-checks that each class is
1470
- * still safe to delete, and reports how many were.
1471
- *
1472
- * @returns Number of classes actually deleted.
1473
- */
1474
- deleteClasses(registry, classNames) {
1475
- const cleanupStartTime = Date.now();
1476
- const selected = Array.from(classNames).map((className) => {
1477
- const ruleInfo = registry.rules.get(className);
1478
- return ruleInfo ? {
1479
- className,
1480
- ruleInfo
1481
- } : null;
1482
- }).filter((entry) => entry != null);
1483
- if (selected.length === 0) return 0;
1484
- const deleted = /* @__PURE__ */ new Set();
1485
- let cleanedUpCount = 0;
1486
- let totalCssSize = 0;
1487
- let totalRulesDeleted = 0;
1488
- const rulesBySheet = /* @__PURE__ */ new Map();
1489
- for (const { className, ruleInfo } of selected) {
1490
- const sheetIndex = ruleInfo.sheetIndex;
1491
- if (this.config.devMode && Array.isArray(ruleInfo.cssText)) {
1492
- const cssSize = ruleInfo.cssText.reduce((total, css) => total + css.length, 0);
1493
- totalCssSize += cssSize;
1494
- totalRulesDeleted += ruleInfo.cssText.length;
1495
- }
1496
- if (!rulesBySheet.has(sheetIndex)) rulesBySheet.set(sheetIndex, []);
1497
- rulesBySheet.get(sheetIndex).push({
1498
- className,
1499
- ruleInfo
1500
- });
1501
- }
1502
- for (const [_sheetIndex, rulesInSheet] of rulesBySheet) {
1503
- rulesInSheet.sort((a, b) => b.ruleInfo.ruleIndex - a.ruleInfo.ruleIndex);
1504
- for (const { className, ruleInfo } of rulesInSheet) {
1505
- if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
1506
- if (registry.rules.get(className) !== ruleInfo) continue;
1507
- const sheetInfo = registry.sheets[ruleInfo.sheetIndex];
1508
- if (!sheetInfo || !sheetInfo.sheet && !sheetInfo.constructableSheet) continue;
1509
- const styleSheet = this.getCSSSheet(sheetInfo);
1510
- if (!sheetInfo.textMode && !styleSheet) continue;
1511
- const maxRuleIndex = (sheetInfo.textMode ? sheetInfo.textRules?.length ?? 0 : styleSheet?.cssRules.length ?? 0) - 1;
1512
- const startIdx = ruleInfo.ruleIndex;
1513
- const endIdx = ruleInfo.endRuleIndex ?? ruleInfo.ruleIndex;
1514
- if (startIdx < 0 || endIdx > maxRuleIndex || startIdx > endIdx) continue;
1515
- this.deleteRule(registry, ruleInfo);
1516
- registry.rules.delete(className);
1517
- registry.pinCounts.delete(className);
1518
- registry.unusedSince.delete(className);
1519
- for (const [key, entry] of registry.localKeyframes) {
1520
- if (!entry.owners.delete(className)) continue;
1521
- if (entry.owners.size === 0) {
1522
- entry.dispose();
1523
- registry.localKeyframes.delete(key);
1524
- }
1525
- }
1526
- deleted.add(className);
1527
- cleanedUpCount++;
1528
- }
1529
- }
1530
- if (deleted.size > 0) {
1531
- for (const [key, mappedClassName] of registry.cacheKeyToClassName) if (deleted.has(mappedClassName)) registry.cacheKeyToClassName.delete(key);
1532
- }
1533
- if (registry.metrics) {
1534
- registry.metrics.bulkCleanups++;
1535
- registry.metrics.stylesCleanedUp += cleanedUpCount;
1536
- registry.metrics.cleanupHistory.push({
1537
- timestamp: cleanupStartTime,
1538
- classesDeleted: cleanedUpCount,
1539
- cssSize: totalCssSize,
1540
- rulesDeleted: totalRulesDeleted
1541
- });
1542
- }
1543
- return cleanedUpCount;
1544
- }
1545
- /**
1546
- * Get total number of rules across all sheets
1547
- */
1548
- getTotalRuleCount(registry) {
1549
- return registry.sheets.reduce((total, sheet) => total + sheet.ruleCount - sheet.holes.length, 0);
1550
- }
1551
- /**
1552
- * The CSS of one managed sheet, or `null` when it holds none this can read —
1553
- * an empty style element, or a sheet the engine refuses to hand over.
1554
- */
1555
- readSheetCSS(sheetInfo) {
1556
- try {
1557
- if (sheetInfo.constructableSheet) return Array.from(sheetInfo.constructableSheet.cssRules).map((rule) => rule.cssText).join("\n");
1558
- if (sheetInfo.sheet) {
1559
- const styleElement = sheetInfo.sheet;
1560
- if (styleElement.textContent) return styleElement.textContent;
1561
- if (styleElement.sheet) return Array.from(styleElement.sheet.cssRules).map((rule) => rule.cssText).join("\n");
1562
- }
1563
- } catch (error) {
1564
- console.warn("[Tasty] Failed to read CSS from sheet:", error);
1565
- }
1566
- return null;
1567
- }
1568
- /**
1569
- * Get CSS text from all sheets (for SSR)
1570
- */
1571
- getCSSText(registry) {
1572
- const cssChunks = [];
1573
- for (const sheetInfo of registry.sheets) {
1574
- const css = this.readSheetCSS(sheetInfo);
1575
- if (css !== null) cssChunks.push(css);
1576
- }
1577
- return cssChunks.join("\n");
1578
- }
1579
- /**
1580
- * Get cache performance metrics
1581
- */
1582
- getMetrics(registry) {
1583
- if (!registry.metrics) return null;
1584
- return {
1585
- ...registry.metrics,
1586
- unusedHits: 0
1587
- };
1588
- }
1589
- /**
1590
- * Reset cache performance metrics
1591
- */
1592
- resetMetrics(registry) {
1593
- if (registry.metrics) registry.metrics = {
1594
- hits: 0,
1595
- misses: 0,
1596
- bulkCleanups: 0,
1597
- totalInsertions: 0,
1598
- totalUnused: 0,
1599
- stylesCleanedUp: 0,
1600
- cleanupHistory: [],
1601
- startTime: Date.now()
1602
- };
1603
- }
1604
- /**
1605
- * Convert keyframes steps to CSS string.
1606
- * Public so the SSR collector can format keyframes without DOM access.
1607
- * Returns both the CSS text and a combined declarations string for property type scanning.
1608
- */
1609
- stepsToCSS(steps) {
1610
- const rules = [];
1611
- const allDeclarations = [];
1612
- for (const [key, value] of Object.entries(steps)) {
1613
- if (typeof value === "string") {
1614
- rules.push(`${key} { ${value.trim()} }`);
1615
- allDeclarations.push(value.trim());
1616
- continue;
1617
- }
1618
- const styleMap = value || {};
1619
- const styleNames = Object.keys(styleMap).sort();
1620
- const handlerQueue = [];
1621
- const seenHandlers = /* @__PURE__ */ new Set();
1622
- styleNames.forEach((styleName) => {
1623
- let handlers = STYLE_HANDLER_MAP[styleName];
1624
- if (!handlers) handlers = STYLE_HANDLER_MAP[styleName] = [createStyle(styleName)];
1625
- handlers.forEach((handler) => {
1626
- if (!seenHandlers.has(handler)) {
1627
- seenHandlers.add(handler);
1628
- handlerQueue.push(handler);
1629
- }
1630
- });
1631
- });
1632
- const declarationPairs = [];
1633
- handlerQueue.forEach((handler) => {
1634
- const result = handler(handler.__lookupStyles.reduce((acc, name) => {
1635
- const v = styleMap[name];
1636
- if (v !== void 0) acc[name] = v;
1637
- return acc;
1638
- }, {}));
1639
- if (!result) return;
1640
- (Array.isArray(result) ? result : [result]).forEach((cssMap) => {
1641
- if (!cssMap || typeof cssMap !== "object") return;
1642
- const { $: _$, ...props } = cssMap;
1643
- Object.entries(props).forEach(([prop, val]) => {
1644
- if (val == null || val === "") return;
1645
- if (Array.isArray(val)) val.forEach((v) => {
1646
- if (v != null && v !== "") declarationPairs.push({
1647
- prop,
1648
- value: String(v)
1649
- });
1650
- });
1651
- else declarationPairs.push({
1652
- prop,
1653
- value: String(val)
1654
- });
1655
- });
1656
- });
1657
- });
1658
- const declarations = declarationPairs.map((d) => `${d.prop}: ${d.value}`).join("; ");
1659
- rules.push(`${key} { ${declarations.trim()} }`);
1660
- allDeclarations.push(declarations);
1661
- }
1662
- return {
1663
- css: rules.join(" "),
1664
- declarations: allDeclarations.join("; ")
1665
- };
1666
- }
1667
- /**
1668
- * Insert keyframes rule.
1669
- * Returns the KeyframesInfo and the raw declarations string for property type scanning.
1670
- */
1671
- insertKeyframes(registry, steps, name, root) {
1672
- let targetSheet = this.findAvailableSheet(registry, root);
1673
- if (!targetSheet) targetSheet = this.createSheet(registry, root);
1674
- const ruleIndex = this.findAvailableRuleIndex(targetSheet);
1675
- const sheetIndex = registry.sheets.indexOf(targetSheet);
1676
- try {
1677
- const { css: cssSteps, declarations } = this.stepsToCSS(steps);
1678
- const fullRule = `@keyframes ${name} { ${cssSteps} }`;
1679
- const styleSheet = this.getCSSSheet(targetSheet);
1680
- if (!targetSheet.textMode && styleSheet) {
1681
- const safeIndex = Math.min(Math.max(0, ruleIndex), styleSheet.cssRules.length);
1682
- styleSheet.insertRule(fullRule, safeIndex);
1683
- } else if (targetSheet.sheet) {
1684
- this.trackTextRule(targetSheet, ruleIndex, fullRule);
1685
- targetSheet.sheet.textContent = (targetSheet.sheet.textContent || "") + "\n" + fullRule;
1686
- }
1687
- targetSheet.ruleCount++;
1688
- return {
1689
- info: {
1690
- name,
1691
- ruleIndex,
1692
- sheetIndex,
1693
- cssText: this.config.devMode ? fullRule : void 0
1694
- },
1695
- declarations
1696
- };
1697
- } catch (error) {
1698
- console.warn("[Tasty] Failed to insert keyframes:", error);
1699
- return null;
1700
- }
1701
- }
1702
- /**
1703
- * Delete keyframes rule
1704
- */
1705
- deleteKeyframes(registry, info) {
1706
- const sheet = registry.sheets[info.sheetIndex];
1707
- if (!sheet) return;
1708
- try {
1709
- const styleSheet = this.getCSSSheet(sheet);
1710
- const adjustIndices = () => this.adjustIndicesAfterDeletion(registry, info.sheetIndex, info.ruleIndex, info.ruleIndex, 1, {
1711
- className: "",
1712
- ruleIndex: info.ruleIndex,
1713
- sheetIndex: info.sheetIndex
1714
- }, [info.ruleIndex]);
1715
- if (sheet.textMode) {
1716
- if (this.deleteTextRules(sheet, [info.ruleIndex]).length > 0) {
1717
- sheet.ruleCount = Math.max(0, sheet.ruleCount - 1);
1718
- adjustIndices();
1719
- }
1720
- } else if (styleSheet) {
1721
- if (info.ruleIndex >= 0 && info.ruleIndex < styleSheet.cssRules.length) {
1722
- styleSheet.deleteRule(info.ruleIndex);
1723
- sheet.ruleCount = Math.max(0, sheet.ruleCount - 1);
1724
- adjustIndices();
1725
- }
1726
- }
1727
- } catch (error) {
1728
- console.warn("[Tasty] Failed to delete keyframes:", error);
1729
- }
1730
- }
1731
- /**
1732
- * Clean up resources for a root
1733
- */
1734
- cleanup(root) {
1735
- const registry = this.rootRegistries.get(root);
1736
- if (!registry) return;
1737
- if (registry.injectionMode === "adopted") {
1738
- const shadowRoot = root;
1739
- const ownedSheets = /* @__PURE__ */ new Set();
1740
- for (const sheetInfo of registry.sheets) if (sheetInfo.constructableSheet) ownedSheets.add(sheetInfo.constructableSheet);
1741
- const rawSheet = this.rawConstructableSheets.get(shadowRoot);
1742
- if (rawSheet) {
1743
- ownedSheets.add(rawSheet);
1744
- this.rawConstructableSheets.delete(shadowRoot);
1745
- }
1746
- if (ownedSheets.size > 0) shadowRoot.adoptedStyleSheets = shadowRoot.adoptedStyleSheets.filter((s) => !ownedSheets.has(s));
1747
- } else {
1748
- for (const sheet of registry.sheets) try {
1749
- const styleElement = sheet.sheet;
1750
- if (styleElement?.parentNode) styleElement.parentNode.removeChild(styleElement);
1751
- } catch (error) {
1752
- console.warn("[Tasty] Failed to cleanup sheet:", error);
1753
- }
1754
- const rawStyleElement = this.rawStyleElements.get(root);
1755
- if (rawStyleElement?.parentNode) rawStyleElement.parentNode.removeChild(rawStyleElement);
1756
- this.rawStyleElements.delete(root);
1757
- }
1758
- this.rootRegistries.delete(root);
1759
- this.activeRoots.delete(root);
1760
- this.rawCSSBlocks.delete(root);
1761
- }
1762
- /**
1763
- * Check if a root uses adopted injection mode.
1764
- */
1765
- isAdoptedMode(root) {
1766
- const registry = this.rootRegistries.get(root);
1767
- if (registry) return registry.injectionMode === "adopted";
1768
- return this.detectInjectionMode(root) === "adopted";
1769
- }
1770
- /**
1771
- * Get or create a constructable CSSStyleSheet for raw CSS in adopted mode.
1772
- * The raw sheet is prepended to adoptedStyleSheets so it precedes tasty rules.
1773
- */
1774
- getOrCreateRawAdoptedSheet(root) {
1775
- let sheet = this.rawConstructableSheets.get(root);
1776
- if (!sheet) {
1777
- sheet = new CSSStyleSheet();
1778
- root.adoptedStyleSheets = [sheet, ...root.adoptedStyleSheets];
1779
- this.rawConstructableSheets.set(root, sheet);
1780
- if (!this.rawCSSBlocks.has(root)) this.rawCSSBlocks.set(root, /* @__PURE__ */ new Map());
1781
- }
1782
- return sheet;
1783
- }
1784
- /**
1785
- * Get or create a dedicated style element for raw CSS
1786
- * Raw CSS is kept separate from tasty-managed sheets to avoid index conflicts
1787
- */
1788
- getOrCreateRawStyleElement(root) {
1789
- let styleElement = this.rawStyleElements.get(root);
1790
- if (!styleElement) {
1791
- styleElement = root.createElement?.("style") || document.createElement("style");
1792
- if (this.config.nonce) styleElement.nonce = this.config.nonce;
1793
- styleElement.setAttribute("data-tasty-raw", "");
1794
- if ("head" in root && root.head) root.head.appendChild(styleElement);
1795
- else if ("appendChild" in root) root.appendChild(styleElement);
1796
- else document.head.appendChild(styleElement);
1797
- this.rawStyleElements.set(root, styleElement);
1798
- this.rawCSSBlocks.set(root, /* @__PURE__ */ new Map());
1799
- }
1800
- return styleElement;
1801
- }
1802
- /**
1803
- * Inject raw CSS text directly without parsing
1804
- * Returns a dispose function to remove the injected CSS
1805
- */
1806
- injectRawCSS(css, root) {
1807
- if (!css.trim()) return { dispose: () => {} };
1808
- const id = `raw_${this.rawCSSCounter++}`;
1809
- if (this.isAdoptedMode(root)) {
1810
- this.getOrCreateRawAdoptedSheet(root);
1811
- const blocksMap = this.rawCSSBlocks.get(root);
1812
- const info = {
1813
- id,
1814
- css,
1815
- startOffset: 0,
1816
- endOffset: css.length
1817
- };
1818
- blocksMap.set(id, info);
1819
- this.rebuildRawAdoptedSheet(root);
1820
- return { dispose: () => {
1821
- this.disposeRawCSS(id, root);
1822
- } };
1823
- }
1824
- const styleElement = this.getOrCreateRawStyleElement(root);
1825
- const blocksMap = this.rawCSSBlocks.get(root);
1826
- const currentContent = styleElement.textContent || "";
1827
- const startOffset = currentContent.length;
1828
- const cssWithNewline = (currentContent ? "\n" : "") + css;
1829
- const endOffset = startOffset + cssWithNewline.length;
1830
- styleElement.textContent = currentContent + cssWithNewline;
1831
- const info = {
1832
- id,
1833
- css,
1834
- startOffset,
1835
- endOffset
1836
- };
1837
- blocksMap.set(id, info);
1838
- return { dispose: () => {
1839
- this.disposeRawCSS(id, root);
1840
- } };
1841
- }
1842
- /**
1843
- * Rebuild the raw CSS constructable sheet from all tracked blocks.
1844
- */
1845
- rebuildRawAdoptedSheet(root) {
1846
- const sheet = this.rawConstructableSheets.get(root);
1847
- const blocksMap = this.rawCSSBlocks.get(root);
1848
- if (!sheet || !blocksMap) return;
1849
- if (blocksMap.size === 0) {
1850
- sheet.replaceSync("");
1851
- return;
1852
- }
1853
- const blocks = Array.from(blocksMap.values());
1854
- blocks.sort((a, b) => a.startOffset - b.startOffset);
1855
- const allCSS = blocks.map((b) => b.css).join("\n");
1856
- sheet.replaceSync(allCSS);
1857
- }
1858
- /**
1859
- * Remove a raw CSS block by ID
1860
- */
1861
- disposeRawCSS(id, root) {
1862
- const blocksMap = this.rawCSSBlocks.get(root);
1863
- if (!blocksMap) return;
1864
- if (!blocksMap.get(id)) return;
1865
- blocksMap.delete(id);
1866
- if (this.isAdoptedMode(root)) {
1867
- this.rebuildRawAdoptedSheet(root);
1868
- return;
1869
- }
1870
- const styleElement = this.rawStyleElements.get(root);
1871
- if (!styleElement) return;
1872
- const remainingBlocks = Array.from(blocksMap.values());
1873
- if (remainingBlocks.length === 0) styleElement.textContent = "";
1874
- else {
1875
- remainingBlocks.sort((a, b) => a.startOffset - b.startOffset);
1876
- styleElement.textContent = remainingBlocks.map((block) => block.css).join("\n");
1877
- let offset = 0;
1878
- for (const block of remainingBlocks) {
1879
- block.startOffset = offset;
1880
- block.endOffset = offset + block.css.length;
1881
- offset = block.endOffset + 1;
1882
- }
1883
- }
1884
- }
1885
- /**
1886
- * Get the raw CSS content
1887
- */
1888
- /**
1889
- * The CSS this injector owns in `root`, one entry per sheet, in the order the
1890
- * engine applies them.
1891
- *
1892
- * `getCSSText()` walks the managed sheets only, and raw CSS has its own —
1893
- * which sits before, after, or *between* them depending on how it got there:
1894
- * prepended to `adoptedStyleSheets`, or wherever in `<head>` the first raw
1895
- * injection happened to land, with every managed sheet opened afterwards
1896
- * following it. Reporting a fixed order would describe the opposite winner
1897
- * from the live page whenever two rules of equal specificity meet, so the raw
1898
- * sheet is spliced in at the position the DOM actually gives it.
1899
- */
1900
- getOwnedCSSInOrder(registry, root) {
1901
- const raw = this.getRawCSSText(root);
1902
- const rawElement = this.isAdoptedMode(root) ? null : this.rawStyleElements.get(root);
1903
- let rawIndex = raw && !rawElement ? 0 : -1;
1904
- const chunks = [];
1905
- for (const sheetInfo of registry.sheets) {
1906
- const css = this.readSheetCSS(sheetInfo);
1907
- if (css === null) continue;
1908
- if (rawIndex < 0 && rawElement && sheetInfo.sheet && rawElement.compareDocumentPosition(sheetInfo.sheet) & Node.DOCUMENT_POSITION_FOLLOWING) rawIndex = chunks.length;
1909
- chunks.push(css);
1910
- }
1911
- if (!raw) return chunks;
1912
- chunks.splice(rawIndex < 0 ? chunks.length : rawIndex, 0, raw);
1913
- return chunks;
1914
- }
1915
- /**
1916
- * Top-level rules in the raw sheet.
1917
- *
1918
- * Read from the sheet the engine parsed, not from the text: a raw block is
1919
- * one string but any number of rules, and one rule — `@keyframes`, `@media` —
1920
- * can contain any number of blocks, so counting braces answers neither
1921
- * question.
1922
- */
1923
- getRawRuleCount(root) {
1924
- const sheet = this.isAdoptedMode(root) ? this.rawConstructableSheets.get(root) : this.rawStyleElements.get(root)?.sheet;
1925
- try {
1926
- return sheet?.cssRules.length ?? 0;
1927
- } catch {
1928
- return 0;
1929
- }
1930
- }
1931
- getRawCSSText(root) {
1932
- if (this.isAdoptedMode(root)) {
1933
- const blocksMap = this.rawCSSBlocks.get(root);
1934
- if (!blocksMap || blocksMap.size === 0) return "";
1935
- const blocks = Array.from(blocksMap.values());
1936
- blocks.sort((a, b) => a.startOffset - b.startOffset);
1937
- return blocks.map((b) => b.css).join("\n");
1938
- }
1939
- return this.rawStyleElements.get(root)?.textContent || "";
1940
- }
1941
- };
1942
- //#endregion
1943
- //#region src/font-face/index.ts
1944
- const FONT_FACE_KEY = "@font-face";
1945
- /**
1946
- * Check if styles object has local @font-face definition.
1947
- */
1948
- function hasLocalFontFace(styles) {
1949
- return FONT_FACE_KEY in styles;
1950
- }
1951
- /**
1952
- * Extract local @font-face from styles object.
1953
- * Returns null if no local font faces (fast path).
1954
- */
1955
- function extractLocalFontFace(styles) {
1956
- const fontFace = styles[FONT_FACE_KEY];
1957
- if (!fontFace || typeof fontFace !== "object") return null;
1958
- return fontFace;
1959
- }
1960
- const FONT_FACE_DESCRIPTOR_MAP = {
1961
- fontWeight: "font-weight",
1962
- fontStyle: "font-style",
1963
- fontStretch: "font-stretch",
1964
- fontDisplay: "font-display",
1965
- unicodeRange: "unicode-range",
1966
- ascentOverride: "ascent-override",
1967
- descentOverride: "descent-override",
1968
- lineGapOverride: "line-gap-override",
1969
- sizeAdjust: "size-adjust",
1970
- fontFeatureSettings: "font-feature-settings",
1971
- fontVariationSettings: "font-variation-settings"
1972
- };
1973
- /**
1974
- * Format the inner declarations of a @font-face rule (no wrapper).
1975
- * Used by the injector which needs selector and declarations separately.
1976
- */
1977
- function formatFontFaceDeclarations(family, descriptors) {
1978
- const parts = [];
1979
- parts.push(`font-family: "${family}";`);
1980
- parts.push(`src: ${descriptors.src};`);
1981
- for (const [key, cssName] of Object.entries(FONT_FACE_DESCRIPTOR_MAP)) {
1982
- const value = descriptors[key];
1983
- if (value !== void 0) parts.push(`${cssName}: ${value};`);
1984
- }
1985
- return parts.join(" ");
1986
- }
1987
- /**
1988
- * Format a single @font-face rule as CSS.
1989
- */
1990
- function formatFontFaceRule(family, descriptors) {
1991
- return `@font-face { ${formatFontFaceDeclarations(family, descriptors)} }`;
1992
- }
1993
- /**
1994
- * Generate a content hash for deduplication of a single font-face rule.
1995
- */
1996
- function fontFaceContentHash(family, descriptors) {
1997
- return JSON.stringify({
1998
- family,
1999
- ...descriptors
2000
- });
2001
- }
2002
- //#endregion
2003
- //#region src/counter-style/index.ts
2004
- const COUNTER_STYLE_KEY = "@counter-style";
2005
- /**
2006
- * Check if styles object has local @counter-style definition.
2007
- */
2008
- function hasLocalCounterStyle(styles) {
2009
- return COUNTER_STYLE_KEY in styles;
2010
- }
2011
- /**
2012
- * Extract local @counter-style from styles object.
2013
- * Returns null if no local counter styles (fast path).
2014
- */
2015
- function extractLocalCounterStyle(styles) {
2016
- const counterStyle = styles[COUNTER_STYLE_KEY];
2017
- if (!counterStyle || typeof counterStyle !== "object") return null;
2018
- return counterStyle;
2019
- }
2020
- const COUNTER_STYLE_DESCRIPTOR_MAP = {
2021
- system: "system",
2022
- symbols: "symbols",
2023
- additiveSymbols: "additive-symbols",
2024
- prefix: "prefix",
2025
- suffix: "suffix",
2026
- negative: "negative",
2027
- range: "range",
2028
- pad: "pad",
2029
- fallback: "fallback",
2030
- speakAs: "speak-as"
2031
- };
2032
- /**
2033
- * Format the inner declarations of a @counter-style rule (no wrapper).
2034
- * Used by the injector which needs selector and declarations separately.
2035
- */
2036
- function formatCounterStyleDeclarations(descriptors) {
2037
- const parts = [];
2038
- for (const [key, cssName] of Object.entries(COUNTER_STYLE_DESCRIPTOR_MAP)) {
2039
- const value = descriptors[key];
2040
- if (value !== void 0) parts.push(`${cssName}: ${value};`);
2041
- }
2042
- return parts.join(" ");
2043
- }
2044
- /**
2045
- * Format a @counter-style rule as CSS.
2046
- */
2047
- function formatCounterStyleRule(name, descriptors) {
2048
- return `@counter-style ${name} { ${formatCounterStyleDeclarations(descriptors)} }`;
2049
- }
2050
- //#endregion
2051
- //#region src/injector/injector.ts
2052
- /**
2053
- * How long a class is left alone after it was last known to be wanted.
2054
- * Long enough that a render cannot plausibly still be waiting to commit one.
2055
- */
2056
- const DEFAULT_GC_GRACE = 1e4;
2057
- /** Dispose handle handed back for injections that took no pin. */
2058
- const noop = () => {};
2059
- /**
2060
- * Placeholder `KeyframesInfo` for a cache entry reserved by a batched write and
2061
- * not yet backed by a real rule. Only ever read through `entry.pending`, which
2062
- * is checked before the info is used.
2063
- */
2064
- const EMPTY_KEYFRAMES_INFO = {
2065
- name: "",
2066
- ruleIndex: -3,
2067
- sheetIndex: -3
2068
- };
2069
- /**
2070
- * Extract class names from `<style data-tasty-rsc>` tags.
2071
- * The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
2072
- */
2073
- function extractRSCClassNames(rscClassRegex) {
2074
- if (typeof document === "undefined") return [];
2075
- const styles = document.querySelectorAll("style[data-tasty-rsc]");
2076
- if (styles.length === 0) return [];
2077
- const classSet = /* @__PURE__ */ new Set();
2078
- for (const style of styles) {
2079
- const text = style.textContent;
2080
- if (!text) continue;
2081
- let match;
2082
- rscClassRegex.lastIndex = 0;
2083
- while ((match = rscClassRegex.exec(text)) !== null) classSet.add(match[1]);
2084
- }
2085
- return Array.from(classSet);
2086
- }
2087
- /**
2088
- * Lazily sync server-rendered class names into the client registry.
2089
- *
2090
- * Sources:
2091
- * 1. `window.__TASTY__` — pushed by SSR/RSC streaming scripts
2092
- * 2. `<style data-tasty-rsc>` tags — inline CSS emitted by RSC components
2093
- *
2094
- * Called inside `inject()` / `allocateClassName()` to pick up
2095
- * class names rendered on the server (including during SPA navigation).
2096
- */
2097
- function syncServerClasses(registry, rscClassRegex) {
2098
- if (typeof window === "undefined") return;
2099
- const classes = window.__TASTY__;
2100
- if (classes && classes.length > registry.serverClassSyncIndex) {
2101
- for (let i = registry.serverClassSyncIndex; i < classes.length; i++) registerHydratedClass(registry, classes[i]);
2102
- registry.serverClassSyncIndex = classes.length;
2103
- }
2104
- if (!registry.rscStylesScanned) {
2105
- registry.rscStylesScanned = true;
2106
- for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
2107
- }
2108
- }
2109
- function registerHydratedClass(registry, className) {
2110
- if (registry.rules.has(className)) return;
2111
- registry.rules.set(className, {
2112
- className,
2113
- ruleIndex: -2,
2114
- sheetIndex: -2
2115
- });
2116
- registry.pinCounts.set(className, 0);
2117
- }
2118
- var StyleInjector = class {
2119
- sheetManager;
2120
- config;
2121
- globalRuleCounter = 0;
2122
- /** Cancels the scheduled sweep, whichever timer scheduled it. */
2123
- cancelPendingGC = null;
2124
- namePrefix;
2125
- classRegex;
2126
- rscClassRegex;
2127
- /** @internal — exposed for debug utilities only */
2128
- get _sheetManager() {
2129
- return this.sheetManager;
2130
- }
2131
- /**
2132
- * Whether sheet writes should be queued instead of applied immediately.
2133
- *
2134
- * Only ever true on the client: SSR collects CSS as text and the RSC path
2135
- * returns it as strings, so neither has a live sheet to batch writes against.
2136
- *
2137
- * In the default `true` mode a write is only queued inside an open batch
2138
- * window — a commit in which `<TastyBatchProvider>` rendered and will
2139
- * therefore flush in its insertion effect, before any layout effect can
2140
- * measure. Everything else falls through to a synchronous write, so enabling
2141
- * the flag cannot make a `useLayoutEffect` read an unstyled element.
2142
- * `'always'` drops the gate and accepts that trade for wider coverage.
2143
- */
2144
- get batching() {
2145
- const mode = this.config.batchInjection;
2146
- if (!mode || typeof document === "undefined") return false;
2147
- if (mode === "always") return true;
2148
- if (isBatchWindowOpen()) return true;
2149
- warnBatchProviderMissing();
2150
- return false;
2151
- }
2152
- /**
2153
- * Apply a sheet write now, or queue it when batching is on.
2154
- * Returns the queue handle when deferred, so the caller can cancel it if it
2155
- * disposes before the flush.
2156
- */
2157
- writeSheet(task) {
2158
- if (this.batching) return enqueueStyleWrite(task);
2159
- task();
2160
- return null;
2161
- }
2162
- /**
2163
- * Insert a global (non-class) rule, honouring batching.
2164
- *
2165
- * `onApplied` records the caller's dedupe bookkeeping. When the write is
2166
- * queued it runs eagerly, so a repeat call before the flush bails out instead
2167
- * of queueing the same rule twice — the same reasoning as
2168
- * `insertPropertyRule`'s eager marking. When written synchronously it runs
2169
- * only on success, preserving the existing retry-on-failure behaviour.
2170
- */
2171
- writeGlobalRule(registry, rules, key, root, onApplied) {
2172
- if (this.batching) {
2173
- onApplied?.();
2174
- enqueueStyleWrite(() => {
2175
- this.sheetManager.insertGlobalRule(registry, rules, key, root);
2176
- });
2177
- return;
2178
- }
2179
- if (this.sheetManager.insertGlobalRule(registry, rules, key, root)) onApplied?.();
2180
- }
2181
- constructor(config = {}) {
2182
- if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
2183
- this.config = config;
2184
- this.sheetManager = new SheetManager(config);
2185
- this.namePrefix = config.namePrefix ?? "t";
2186
- this.classRegex = tastyClassRegex(this.namePrefix);
2187
- this.rscClassRegex = rscClassRegexGlobal(this.namePrefix);
2188
- }
2189
- /**
2190
- * Generate a deterministic class name from a cache key using content hash.
2191
- * The same cache key always produces the same class name across environments
2192
- * with the same `namePrefix`.
2193
- */
2194
- generateClassName(cacheKey) {
2195
- return makeClassName(this.namePrefix, hashString(cacheKey));
2196
- }
2197
- /**
2198
- * Check if `className` was hydrated from server-rendered styles and,
2199
- * if so, wire the cacheKey mapping. Returns true on hit.
2200
- */
2201
- tryHydratedHit(registry, cacheKey, className) {
2202
- syncServerClasses(registry, this.rscClassRegex);
2203
- const rule = registry.rules.get(className);
2204
- if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
2205
- registry.cacheKeyToClassName.set(cacheKey, className);
2206
- return true;
2207
- }
2208
- return false;
2209
- }
2210
- /**
2211
- * Allocate a className for a cacheKey without injecting styles yet.
2212
- * This allows separating className allocation (render phase) from style injection (insertion phase).
2213
- */
2214
- allocateClassName(cacheKey, options) {
2215
- const root = options?.root || document;
2216
- const registry = this.sheetManager.getRegistry(root);
2217
- if (registry.cacheKeyToClassName.has(cacheKey)) return {
2218
- className: registry.cacheKeyToClassName.get(cacheKey),
2219
- isNewAllocation: false
2220
- };
2221
- const className = this.generateClassName(cacheKey);
2222
- if (this.tryHydratedHit(registry, cacheKey, className)) return {
2223
- className,
2224
- isNewAllocation: false
2225
- };
2226
- if (registry.rules.get(className)) {
2227
- if (isDevEnv()) console.warn(`[Tasty] Hash collision: cache keys produce the same class "${className}". Styles may be incorrect.`);
2228
- registry.cacheKeyToClassName.set(cacheKey, className);
2229
- return {
2230
- className,
2231
- isNewAllocation: false
2232
- };
2233
- }
2234
- const placeholderRuleInfo = {
2235
- className,
2236
- ruleIndex: -1,
2237
- sheetIndex: -1
2238
- };
2239
- registry.rules.set(className, placeholderRuleInfo);
2240
- registry.cacheKeyToClassName.set(cacheKey, className);
2241
- return {
2242
- className,
2243
- isNewAllocation: true
2244
- };
2245
- }
2246
- /**
2247
- * Inject styles from StyleResult objects
2248
- */
2249
- inject(rules, options) {
2250
- const root = options?.root || document;
2251
- const registry = this.sheetManager.getRegistry(root);
2252
- const pin = options?.pin ?? true;
2253
- if (rules.length === 0) return {
2254
- className: "",
2255
- dispose: () => {}
2256
- };
2257
- const cacheKey = options?.cacheKey;
2258
- let className;
2259
- if (cacheKey && registry.cacheKeyToClassName.has(cacheKey)) {
2260
- className = registry.cacheKeyToClassName.get(cacheKey);
2261
- const existingRuleInfo = registry.rules.get(className);
2262
- if (existingRuleInfo.ruleIndex === -3) {
2263
- registry.unusedSince.delete(className);
2264
- if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
2265
- if (registry.metrics) registry.metrics.hits++;
2266
- return {
2267
- className,
2268
- dispose: pin ? () => this.unpin(className, registry) : noop
2269
- };
2270
- }
2271
- if (!(existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1)) {
2272
- registry.unusedSince.delete(className);
2273
- if (pin) {
2274
- const pins = registry.pinCounts.get(className) || 0;
2275
- registry.pinCounts.set(className, pins + 1);
2276
- }
2277
- if (registry.metrics) registry.metrics.hits++;
2278
- return {
2279
- className,
2280
- dispose: pin ? () => this.unpin(className, registry) : noop
2281
- };
2282
- }
2283
- } else if (cacheKey) {
2284
- className = this.generateClassName(cacheKey);
2285
- if (this.tryHydratedHit(registry, cacheKey, className)) {
2286
- if (pin) registry.pinCounts.set(className, (registry.pinCounts.get(className) || 0) + 1);
2287
- if (registry.metrics) registry.metrics.hits++;
2288
- return {
2289
- className,
2290
- dispose: pin ? () => this.unpin(className, registry) : noop
2291
- };
2292
- }
2293
- } else {
2294
- const parts = rules.map((r) => `${r.selector}\0${r.declarations}`);
2295
- className = makeClassName(this.namePrefix, hashString(parts.join("\n")));
2296
- }
2297
- const rulesToInsert = rules.map((rule) => {
2298
- let newSelector = rule.selector;
2299
- if (rule.needsClassName) {
2300
- const selectorParts = newSelector ? newSelector.split("|||") : [""];
2301
- const classPrefix = `.${className}.${className}`;
2302
- newSelector = selectorParts.map((part) => {
2303
- const classSelector = part ? `${classPrefix}${part}` : classPrefix;
2304
- if (rule.rootPrefix) return `${rule.rootPrefix} ${classSelector}`;
2305
- return classSelector;
2306
- }).join(", ");
2307
- }
2308
- return {
2309
- ...rule,
2310
- selector: newSelector,
2311
- needsClassName: void 0,
2312
- rootPrefix: void 0
2313
- };
2314
- });
2315
- const applySheetWrite = () => {
2316
- if (this.config.autoPropertyTypes !== false) {
2317
- const resolver = registry.propertyTypeResolver;
2318
- const defined = registry.injectedProperties;
2319
- for (const rule of rulesToInsert) {
2320
- if (!rule.declarations) continue;
2321
- resolver.scanDeclarations(rule.declarations, (name) => defined.has(name), (name, syntax, initialValue) => {
2322
- this.property(name, {
2323
- syntax,
2324
- inherits: true,
2325
- initialValue,
2326
- root
2327
- });
2328
- });
2329
- }
2330
- }
2331
- const ruleInfo = this.sheetManager.insertRule(registry, rulesToInsert, className, root);
2332
- if (!ruleInfo) {
2333
- if (registry.metrics) registry.metrics.misses++;
2334
- return null;
2335
- }
2336
- registry.rules.set(className, ruleInfo);
2337
- if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
2338
- if (registry.metrics) {
2339
- registry.metrics.totalInsertions++;
2340
- registry.metrics.misses++;
2341
- }
2342
- return ruleInfo;
2343
- };
2344
- if (this.batching) {
2345
- registry.rules.set(className, {
2346
- className,
2347
- ruleIndex: -3,
2348
- sheetIndex: -3
2349
- });
2350
- if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
2351
- if (pin) registry.pinCounts.set(className, 1);
2352
- registry.unusedSince.set(className, Date.now());
2353
- const queued = enqueueStyleWrite(() => {
2354
- if (applySheetWrite()) return;
2355
- registry.rules.set(className, {
2356
- className,
2357
- ruleIndex: -1,
2358
- sheetIndex: -1
2359
- });
2360
- });
2361
- return {
2362
- className,
2363
- dispose: pin ? () => {
2364
- if (!queued.done && registry.pinCounts.get(className) === 1 && registry.rules.get(className)?.ruleIndex === -3) {
2365
- queued.cancelled = true;
2366
- registry.rules.delete(className);
2367
- if (cacheKey) registry.cacheKeyToClassName.delete(cacheKey);
2368
- registry.pinCounts.set(className, 0);
2369
- return;
2370
- }
2371
- this.unpin(className, registry);
2372
- } : noop
2373
- };
2374
- }
2375
- if (!applySheetWrite()) return {
2376
- className,
2377
- dispose: () => {}
2378
- };
2379
- if (pin) registry.pinCounts.set(className, 1);
2380
- registry.unusedSince.set(className, Date.now());
2381
- return {
2382
- className,
2383
- dispose: pin ? () => this.unpin(className, registry) : noop
2384
- };
2385
- }
2386
- /**
2387
- * Inject global styles (rules without a generated tasty class selector)
2388
- * This ensures we don't reserve a tasty class name (t{number}) for global rules,
2389
- * which could otherwise collide with element-level styles and break lookups.
2390
- */
2391
- injectGlobal(rules, options) {
2392
- const root = options?.root || document;
2393
- const registry = this.sheetManager.getRegistry(root);
2394
- if (!rules || rules.length === 0) return { dispose: () => {} };
2395
- const key = `global:${this.globalRuleCounter++}`;
2396
- const applyWrite = () => {
2397
- if (this.config.autoPropertyTypes !== false) {
2398
- const resolver = registry.propertyTypeResolver;
2399
- const defined = registry.injectedProperties;
2400
- for (const rule of rules) {
2401
- if (!rule.declarations) continue;
2402
- resolver.scanDeclarations(rule.declarations, (name) => defined.has(name), (name, syntax, initialValue) => {
2403
- this.property(name, {
2404
- syntax,
2405
- inherits: true,
2406
- initialValue,
2407
- root
2408
- });
2409
- });
2410
- }
2411
- }
2412
- const inserted = this.sheetManager.insertGlobalRule(registry, rules, key, root);
2413
- if (registry.metrics) registry.metrics.totalInsertions++;
2414
- return inserted;
2415
- };
2416
- if (this.batching) {
2417
- let info = null;
2418
- const queued = enqueueStyleWrite(() => {
2419
- info = applyWrite();
2420
- });
2421
- return { dispose: () => {
2422
- if (!queued.done) {
2423
- queued.cancelled = true;
2424
- return;
2425
- }
2426
- if (info) this.sheetManager.deleteGlobalRule(registry, key);
2427
- } };
2428
- }
2429
- const info = applyWrite();
2430
- return { dispose: () => {
2431
- if (info) this.sheetManager.deleteGlobalRule(registry, key);
2432
- } };
2433
- }
2434
- /**
2435
- * Inject raw CSS text directly without parsing
2436
- * This is a low-overhead alternative to createGlobalStyle for raw CSS
2437
- * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking
2438
- */
2439
- injectRawCSS(css, options) {
2440
- const root = options?.root || document;
2441
- if (!this.batching) return this.sheetManager.injectRawCSS(css, root);
2442
- let result = null;
2443
- const queued = enqueueStyleWrite(() => {
2444
- result = this.sheetManager.injectRawCSS(css, root);
2445
- });
2446
- return { dispose: () => {
2447
- if (!queued.done) {
2448
- queued.cancelled = true;
2449
- return;
2450
- }
2451
- result?.dispose();
2452
- } };
2453
- }
2454
- /**
2455
- * Get raw CSS text for SSR
2456
- */
2457
- getRawCSSText(options) {
2458
- flushStyles();
2459
- const root = options?.root || document;
2460
- return this.sheetManager.getRawCSSText(root);
2461
- }
2462
- /**
2463
- * Take a reference on local `@keyframes`, under the deterministic names the
2464
- * caller resolved.
2465
- *
2466
- * One reference per distinct set of steps, shared by every class that ends up
2467
- * animating it, so a repeat render takes nothing further. Ownership is
2468
- * assigned by `ownKeyframes()` once the rules exist to be inspected.
2469
- */
2470
- holdKeyframes(steps, names, options) {
2471
- const root = options?.root || document;
2472
- const registry = this.sheetManager.getRegistry(root);
2473
- const keys = /* @__PURE__ */ new Map();
2474
- for (const [authored, definition] of Object.entries(steps)) {
2475
- const resolved = names.get(authored) ?? authored;
2476
- let entry = registry.localKeyframes.get(resolved);
2477
- if (!entry) {
2478
- const injected = this.keyframes(definition, {
2479
- name: resolved,
2480
- root,
2481
- distinctByName: true
2482
- });
2483
- entry = {
2484
- name: injected.toString(),
2485
- dispose: injected.dispose,
2486
- owners: /* @__PURE__ */ new Set()
2487
- };
2488
- registry.localKeyframes.set(resolved, entry);
2489
- }
2490
- keys.set(authored, resolved);
2491
- }
2492
- return keys;
2493
- }
2494
- /**
2495
- * Record that `className` animates these keyframes, so the reference is
2496
- * released when the last such class is collected.
2497
- *
2498
- * Only classes whose rules actually reference the animation should be here:
2499
- * a class that merely rendered alongside one would otherwise keep the
2500
- * keyframes alive for as long as it lives.
2501
- */
2502
- ownKeyframes(key, className, options) {
2503
- this.sheetManager.getRegistry(options?.root || document).localKeyframes.get(key)?.owners.add(className);
2504
- }
2505
- /**
2506
- * Pin an already-injected cacheKey and return the handle that releases it.
2507
- * For callers that skipped the pipeline on a cache hit but still need the
2508
- * class held. Returns null if the cacheKey is not found.
2509
- */
2510
- trackRef(cacheKey, options) {
2511
- const root = options?.root || document;
2512
- const registry = this.sheetManager.getRegistry(root);
2513
- if (!registry.cacheKeyToClassName.has(cacheKey)) return null;
2514
- const className = registry.cacheKeyToClassName.get(cacheKey);
2515
- const pins = registry.pinCounts.get(className) || 0;
2516
- registry.pinCounts.set(className, pins + 1);
2517
- if (registry.metrics) registry.metrics.hits++;
2518
- return {
2519
- className,
2520
- dispose: () => this.unpin(className, registry)
2521
- };
2522
- }
2523
- /**
2524
- * Release one pin on a className. At zero pins the class is not deleted — it
2525
- * stays cached and collectible, and `gc()` decides when it actually goes.
2526
- */
2527
- unpin(className, registry) {
2528
- const pins = registry.pinCounts.get(className);
2529
- if (pins == null || pins <= 0) return;
2530
- const remaining = pins - 1;
2531
- registry.pinCounts.set(className, remaining);
2532
- if (remaining === 0 && registry.metrics) registry.metrics.totalUnused++;
2533
- }
2534
- /**
2535
- * Remove every style that is neither in the DOM nor referenced by an
2536
- * outstanding `inject()` handle, ignoring the GC capacity threshold.
2537
- */
2538
- cleanup(root) {
2539
- this.gc({
2540
- root,
2541
- force: true
2542
- });
2543
- }
2544
- /**
2545
- * Get CSS text from all sheets (for SSR)
2546
- */
2547
- getCSSText(options) {
2548
- flushStyles();
2549
- const root = options?.root || document;
2550
- const registry = this.sheetManager.getRegistry(root);
2551
- return this.sheetManager.getCSSText(registry);
2552
- }
2553
- /**
2554
- * Get CSS only for the provided tasty classNames (e.g., ["t0","t3"])
2555
- */
2556
- getCSSTextForClasses(classNames, options) {
2557
- flushStyles();
2558
- const root = options?.root || document;
2559
- const registry = this.sheetManager.getRegistry(root);
2560
- const cssChunks = [];
2561
- for (const cls of classNames) {
2562
- const info = registry.rules.get(cls);
2563
- if (info) {
2564
- const sheet = registry.sheets[info.sheetIndex];
2565
- const styleSheet = sheet ? this.sheetManager.getCSSSheet(sheet) : null;
2566
- if (styleSheet) {
2567
- const start = Math.max(0, info.ruleIndex);
2568
- const end = Math.min(styleSheet.cssRules.length - 1, info.endRuleIndex ?? info.ruleIndex);
2569
- if (start >= 0 && end >= start && start < styleSheet.cssRules.length) for (let i = start; i <= end; i++) {
2570
- const rule = styleSheet.cssRules[i];
2571
- if (rule) cssChunks.push(rule.cssText);
2572
- }
2573
- } else if (info.cssText && info.cssText.length) cssChunks.push(...info.cssText);
2574
- }
2575
- }
2576
- return cssChunks.join("\n");
2577
- }
2578
- /**
2579
- * Get cache performance metrics
2580
- */
2581
- getMetrics(options) {
2582
- flushStyles();
2583
- const root = options?.root || document;
2584
- const registry = this.sheetManager.getRegistry(root);
2585
- const metrics = this.sheetManager.getMetrics(registry);
2586
- if (metrics && typeof document !== "undefined") metrics.unusedHits = this.collectUnused(registry, root).length;
2587
- return metrics;
2588
- }
2589
- /**
2590
- * Reset cache performance metrics
2591
- */
2592
- resetMetrics(options) {
2593
- const root = options?.root || document;
2594
- const registry = this.sheetManager.getRegistry(root);
2595
- this.sheetManager.resetMetrics(registry);
2596
- }
2597
- /**
2598
- * Define a CSS @property custom property.
2599
- *
2600
- * Accepts tasty token syntax for the property name:
2601
- * - `$name` → defines `--name`
2602
- * - `#name` → defines `--name-color` (auto-sets syntax: '<color>', defaults initialValue: 'transparent')
2603
- * - `--name` → defines `--name` (legacy format)
2604
- *
2605
- * Example:
2606
- * @property --rotation { syntax: "<angle>"; inherits: false; initial-value: 45deg; }
2607
- *
2608
- * Note: No caching or dispose — this defines a global property.
2609
- *
2610
- * If the same property is registered with different options, a warning is emitted
2611
- * but the original definition is preserved (CSS @property cannot be redefined).
2612
- */
2613
- property(name, options) {
2614
- const root = options?.root || document;
2615
- const registry = this.sheetManager.getRegistry(root);
2616
- const effectiveResult = getEffectiveDefinition(name, {
2617
- syntax: options?.syntax,
2618
- inherits: options?.inherits,
2619
- initialValue: options?.initialValue
2620
- });
2621
- if (!effectiveResult.isValid) {
2622
- if (isDevEnv()) console.warn(`[Tasty] property(): ${effectiveResult.error}. Got: "${name}"`);
2623
- return;
2624
- }
2625
- const cssName = effectiveResult.cssName;
2626
- const definition = effectiveResult.definition;
2627
- this.insertPropertyRule(registry, root, cssName, definition, name);
2628
- }
2629
- /**
2630
- * Build and insert a single `@property` rule into the given registry.
2631
- * No-op if the property was already injected.
2632
- */
2633
- insertPropertyRule(registry, root, cssName, definition, cacheKey) {
2634
- if (registry.injectedProperties.has(cssName)) return;
2635
- const parts = [];
2636
- if (definition.syntax != null) {
2637
- let syntax = String(definition.syntax).trim();
2638
- if (!/^['"]/u.test(syntax)) syntax = `"${syntax}"`;
2639
- parts.push(`syntax: ${syntax};`);
2640
- }
2641
- const inherits = definition.inherits ?? true;
2642
- parts.push(`inherits: ${inherits ? "true" : "false"};`);
2643
- if (definition.initialValue != null) {
2644
- let initialValueStr;
2645
- if (typeof definition.initialValue === "number") initialValueStr = String(definition.initialValue);
2646
- else initialValueStr = parseStyle(definition.initialValue).output;
2647
- parts.push(`initial-value: ${initialValueStr};`);
2648
- }
2649
- const declarations = parts.join(" ").trim();
2650
- const rule = {
2651
- selector: `@property ${cssName}`,
2652
- declarations
2653
- };
2654
- registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
2655
- this.writeGlobalRule(registry, [rule], `property:${cacheKey}`, root);
2656
- }
2657
- /**
2658
- * Check whether a given @property name was already injected by this injector.
2659
- *
2660
- * Accepts tasty token syntax:
2661
- * - `$name` → checks `--name`
2662
- * - `#name` → checks `--name-color`
2663
- * - `--name` → checks `--name` (legacy format)
2664
- */
2665
- isPropertyDefined(name, options) {
2666
- flushStyles();
2667
- const root = options?.root || document;
2668
- const registry = this.sheetManager.getRegistry(root);
2669
- const effectiveResult = getEffectiveDefinition(name, {});
2670
- if (!effectiveResult.isValid) return false;
2671
- return registry.injectedProperties.has(effectiveResult.cssName);
2672
- }
2673
- /**
2674
- * Inject a CSS @font-face rule.
2675
- *
2676
- * Permanent and global — no dispose or ref-counting.
2677
- * Deduplicates by content hash (family + descriptors).
2678
- */
2679
- fontFace(family, descriptors, options) {
2680
- const root = options?.root || document;
2681
- const registry = this.sheetManager.getRegistry(root);
2682
- const hash = fontFaceContentHash(family, descriptors);
2683
- if (registry.injectedFontFaces.has(hash)) return;
2684
- const rule = {
2685
- selector: "@font-face",
2686
- declarations: formatFontFaceDeclarations(family, descriptors)
2687
- };
2688
- this.writeGlobalRule(registry, [rule], `fontface:${hash}`, root, () => {
2689
- registry.injectedFontFaces.add(hash);
2690
- });
2691
- }
2692
- /**
2693
- * Inject a CSS @counter-style rule.
2694
- *
2695
- * Permanent and global — no dispose or ref-counting. Deduplicates by name.
2696
- * By default a definition overrides a previously injected one of the same
2697
- * name. Pass `weak: true` for global `configure()` definitions, which must
2698
- * never clobber an existing rule (so component-local definitions win
2699
- * regardless of injection order).
2700
- */
2701
- counterStyle(name, descriptors, options) {
2702
- const root = options?.root || document;
2703
- const registry = this.sheetManager.getRegistry(root);
2704
- const isWeak = options?.weak === true;
2705
- const existingIsStrong = registry.injectedCounterStyles.get(name);
2706
- if (existingIsStrong !== void 0) {
2707
- if (isWeak || existingIsStrong === true) return;
2708
- this.writeSheet(() => {
2709
- this.sheetManager.deleteGlobalRule(registry, `counterstyle:${name}`);
2710
- });
2711
- }
2712
- const rule = {
2713
- selector: `@counter-style ${name}`,
2714
- declarations: formatCounterStyleDeclarations(descriptors)
2715
- };
2716
- this.writeGlobalRule(registry, [rule], `counterstyle:${name}`, root, () => {
2717
- registry.injectedCounterStyles.set(name, !isWeak);
2718
- });
2719
- }
2720
- /**
2721
- * Inject a CSS @function rule (custom function).
2722
- *
2723
- * Permanent and global — no dispose or ref-counting. Deduplicates by function
2724
- * name. By default a definition overrides a previously injected one of the
2725
- * same name. Pass `weak: true` for global `configure()` definitions, which
2726
- * must never clobber an existing rule (so component-local definitions win
2727
- * regardless of injection order).
2728
- */
2729
- func(name, definition, options) {
2730
- const root = options?.root || document;
2731
- const registry = this.sheetManager.getRegistry(root);
2732
- const isWeak = options?.weak === true;
2733
- const cssName = parseFunctionName(name);
2734
- const existingIsStrong = registry.injectedFunctions.get(cssName);
2735
- if (existingIsStrong !== void 0) {
2736
- if (isWeak || existingIsStrong === true) return;
2737
- this.writeSheet(() => {
2738
- this.sheetManager.deleteGlobalRule(registry, `function:${cssName}`);
2739
- });
2740
- }
2741
- const rule = {
2742
- selector: formatFunctionPrelude(name, definition.args, definition.returns),
2743
- declarations: formatFunctionDeclarations(definition)
2744
- };
2745
- this.writeGlobalRule(registry, [rule], `function:${cssName}`, root, () => {
2746
- registry.injectedFunctions.set(cssName, !isWeak);
2747
- });
2748
- }
2749
- /**
2750
- * Inject keyframes and return object with toString() and dispose()
2751
- *
2752
- * Keyframes are cached by content (steps). If the same content is injected
2753
- * multiple times with different provided names, the first injected name is reused.
2754
- *
2755
- * If the same name is provided with different content (collision), a unique
2756
- * name is generated to avoid overwriting the existing keyframes.
2757
- */
2758
- keyframes(steps, nameOrOptions) {
2759
- const isStringName = typeof nameOrOptions === "string";
2760
- const providedName = isStringName ? nameOrOptions : nameOrOptions?.name;
2761
- const distinctByName = isStringName ? false : nameOrOptions?.distinctByName ?? false;
2762
- const root = isStringName ? document : nameOrOptions?.root || document;
2763
- const registry = this.sheetManager.getRegistry(root);
2764
- if (Object.keys(steps).length === 0) return {
2765
- toString: () => "",
2766
- dispose: () => {}
2767
- };
2768
- const contentHash = distinctByName && providedName ? `${providedName}\u0000${JSON.stringify(steps)}` : JSON.stringify(steps);
2769
- const existing = registry.keyframesCache.get(contentHash);
2770
- if (existing) {
2771
- existing.refCount++;
2772
- return {
2773
- toString: () => existing.name,
2774
- dispose: () => this.disposeKeyframes(contentHash, registry)
2775
- };
2776
- }
2777
- let actualName;
2778
- if (providedName) {
2779
- const existingContentForName = registry.keyframesNameToContent.get(providedName);
2780
- if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
2781
- else {
2782
- actualName = providedName;
2783
- registry.keyframesNameToContent.set(providedName, contentHash);
2784
- }
2785
- } else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
2786
- const applyWrite = () => {
2787
- const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
2788
- if (!result) return false;
2789
- const { info, declarations } = result;
2790
- if (this.config.autoPropertyTypes !== false && declarations) registry.propertyTypeResolver.scanDeclarations(declarations, (name) => registry.injectedProperties.has(name), (name, syntax, initialValue) => {
2791
- this.property(name, {
2792
- syntax,
2793
- inherits: true,
2794
- initialValue,
2795
- root
2796
- });
2797
- });
2798
- const entry = registry.keyframesCache.get(contentHash);
2799
- if (entry) {
2800
- entry.info = info;
2801
- entry.pending = void 0;
2802
- } else registry.keyframesCache.set(contentHash, {
2803
- name: actualName,
2804
- refCount: 1,
2805
- info
2806
- });
2807
- if (registry.metrics) {
2808
- registry.metrics.totalInsertions++;
2809
- registry.metrics.misses++;
2810
- }
2811
- return true;
2812
- };
2813
- if (this.batching) {
2814
- const entry = {
2815
- name: actualName,
2816
- refCount: 1,
2817
- info: EMPTY_KEYFRAMES_INFO
2818
- };
2819
- registry.keyframesCache.set(contentHash, entry);
2820
- entry.pending = enqueueStyleWrite(() => {
2821
- if (applyWrite()) return;
2822
- if (registry.keyframesCache.get(contentHash) === entry) registry.keyframesCache.delete(contentHash);
2823
- });
2824
- return {
2825
- toString: () => actualName,
2826
- dispose: () => this.disposeKeyframes(contentHash, registry)
2827
- };
2828
- }
2829
- if (!applyWrite()) return {
2830
- toString: () => "",
2831
- dispose: () => {}
2832
- };
2833
- return {
2834
- toString: () => actualName,
2835
- dispose: () => this.disposeKeyframes(contentHash, registry)
2836
- };
2837
- }
2838
- /**
2839
- * Dispose keyframes
2840
- */
2841
- disposeKeyframes(contentHash, registry) {
2842
- const entry = registry.keyframesCache.get(contentHash);
2843
- if (!entry) return;
2844
- entry.refCount--;
2845
- if (entry.refCount <= 0) {
2846
- if (entry.pending && !entry.pending.done) entry.pending.cancelled = true;
2847
- else this.sheetManager.deleteKeyframes(registry, entry.info);
2848
- registry.keyframesCache.delete(contentHash);
2849
- for (const [name, hash] of registry.keyframesNameToContent.entries()) if (hash === contentHash) {
2850
- registry.keyframesNameToContent.delete(name);
2851
- break;
2852
- }
2853
- if (registry.metrics) {
2854
- registry.metrics.totalUnused++;
2855
- registry.metrics.stylesCleanedUp++;
2856
- }
2857
- }
2858
- }
2859
- /**
2860
- * Count a render, and schedule a collection pass every `touchInterval` of
2861
- * them. Nothing else: what a class is worth keeping is decided by the sweep's
2862
- * own DOM scan, so rendering does not track usage at all.
2863
- *
2864
- * @deprecated The class name is ignored — pass anything, or stop calling it.
2865
- * Collection no longer records per-class usage, and scheduling does not need
2866
- * to know which class was rendered. A class handed back by `inject()` is
2867
- * marked wanted there, which is what reuse actually goes through.
2868
- */
2869
- touch(_className, options) {
2870
- if (typeof document === "undefined") return;
2871
- if (!this.config.gc) return;
2872
- const registry = this.sheetManager.getRegistry(options?.root || document);
2873
- if (++registry.touchCount >= (this.config.gc.touchInterval ?? 1e3)) {
2874
- registry.touchCount = 0;
2875
- this.scheduleGC();
2876
- }
2877
- }
2878
- /**
2879
- * Schedule a GC in idle time. Runs GC on all active roots, and avoids
2880
- * double-scheduling.
2881
- *
2882
- * Idle only. Without `requestIdleCallback` nothing is collected
2883
- * automatically: running the sweep inline here would put it inside the render
2884
- * that touched the class, and collection is never urgent enough for that.
2885
- */
2886
- scheduleGC() {
2887
- if (this.cancelPendingGC) return;
2888
- const runGC = () => {
2889
- this.cancelPendingGC = null;
2890
- this.sheetManager.pruneDisconnectedRoots();
2891
- for (const root of this.sheetManager.getActiveRoots()) this.gc({ root });
2892
- };
2893
- if (typeof requestIdleCallback !== "undefined") {
2894
- const handle = requestIdleCallback(() => runGC());
2895
- this.cancelPendingGC = () => cancelIdleCallback(handle);
2896
- }
2897
- }
2898
- /**
2899
- * The one definition of "unused": this injector owns the class's rules, no
2900
- * element in `root` carries it, and nobody pinned it.
2901
- *
2902
- * `gc()` evicts from this set, `getMetrics()` counts it, and `tastyDebug`
2903
- * reports it — all through here, so the three can never disagree about what
2904
- * "unused" means. Entries carry their last `touch()` so callers can drop the
2905
- * oldest first; a class that was never touched sorts as oldest.
2906
- */
2907
- /**
2908
- * Everything the injector holds falls into one of five bands, and only the
2909
- * last of them is ever deleted:
2910
- *
2911
- * 1. rendered — some element carries the class right now
2912
- * 2. not ours — queued for a batched write, pre-allocated, or server-rendered
2913
- * 3. hot — nothing carries it, but that was noticed less than `grace` ago
2914
- * 4. cached — cold, but within `capacity` when ordered by when it went cold
2915
- * 5. the rest
2916
- *
2917
- * This returns bands 4 and 5 together, most recently cold first; `gc()` draws
2918
- * the capacity line through them. Band 3 is what makes collection safe
2919
- * without a commit signal: a render can resolve a class and commit it a
2920
- * little later, and during that gap nothing on the page carries it.
2921
- */
2922
- collectUnused(registry, root) {
2923
- const now = Date.now();
2924
- const grace = this.config.gc?.grace ?? DEFAULT_GC_GRACE;
2925
- const liveClasses = /* @__PURE__ */ new Set();
2926
- for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
2927
- const unused = [];
2928
- for (const [className, ruleInfo] of registry.rules) {
2929
- if (ruleInfo.sheetIndex < 0) continue;
2930
- if (liveClasses.has(className)) {
2931
- registry.unusedSince.delete(className);
2932
- continue;
2933
- }
2934
- if ((registry.pinCounts.get(className) ?? 0) > 0) continue;
2935
- let since = registry.unusedSince.get(className);
2936
- if (since === void 0) {
2937
- since = now;
2938
- registry.unusedSince.set(className, now);
2939
- }
2940
- if (now - since < grace) continue;
2941
- unused.push(className);
2942
- }
2943
- unused.sort((a, b) => (registry.unusedSince.get(b) ?? 0) - (registry.unusedSince.get(a) ?? 0));
2944
- return unused;
2945
- }
2946
- /**
2947
- * Class names this injector holds CSS for that nothing renders and nobody
2948
- * pinned — exactly what `gc({ force: true })` would delete. Unordered.
2949
- */
2950
- getUnusedClasses(options) {
2951
- flushStyles();
2952
- if (typeof document === "undefined") return [];
2953
- const root = options?.root || document;
2954
- const registry = this.sheetManager.getRegistry(root);
2955
- return this.collectUnused(registry, root);
2956
- }
2957
- /**
2958
- * Synchronous garbage collection.
2959
- *
2960
- * 1. Quick upper-bound check: skip if the registry is smaller than capacity.
2961
- * 2. Scans the DOM for live tasty classNames — the DOM, not a ref count, is
2962
- * what says a class rendered by a component is still in use.
2963
- * 3. With `force: true`: deletes every unused class.
2964
- * Without `force`: keeps the `capacity` most recently touched and deletes
2965
- * the rest, oldest first.
2966
- *
2967
- * @returns Number of styles evicted.
2968
- */
2969
- gc(options) {
2970
- flushStyles();
2971
- if (typeof document === "undefined") return 0;
2972
- this.cancelPendingGC?.();
2973
- this.cancelPendingGC = null;
2974
- const root = options?.root || document;
2975
- const force = options?.force;
2976
- const registry = this.sheetManager.getRegistry(root);
2977
- const capacity = this.config.gc?.capacity ?? 1e3;
2978
- if (!force && registry.rules.size <= capacity) return 0;
2979
- const unused = this.collectUnused(registry, root);
2980
- let doomed;
2981
- if (force) doomed = unused;
2982
- else if (unused.length > capacity) doomed = unused.slice(capacity);
2983
- else return 0;
2984
- if (doomed.length === 0) return 0;
2985
- return this.sheetManager.deleteClasses(registry, doomed);
2986
- }
2987
- /**
2988
- * Destroy all resources for a root
2989
- */
2990
- destroy(root) {
2991
- flushStyles();
2992
- const targetRoot = root || document;
2993
- this.sheetManager.cleanup(targetRoot);
2994
- if (this.cancelPendingGC && !this.sheetManager.hasActiveRoots()) {
2995
- this.cancelPendingGC();
2996
- this.cancelPendingGC = null;
2997
- }
2998
- }
2999
- };
3000
- //#endregion
3001
- //#region src/utils/typography.ts
3002
- const RESERVED_PRESET_NAMES = new Set([
3003
- "strong",
3004
- "bold",
3005
- "italic",
3006
- "icon",
3007
- "tight"
3008
- ]);
3009
- /**
3010
- * Generate typography tokens with $ prefix for CSS custom properties.
3011
- *
3012
- * Each preset generates the following CSS custom properties:
3013
- * - `${name}-font-size`
3014
- * - `${name}-line-height`
3015
- * - `${name}-letter-spacing`
3016
- * - `${name}-font-weight`
3017
- * - `${name}-bold-font-weight` (if defined)
3018
- * - `${name}-icon-size` (if defined)
3019
- * - `${name}-text-transform` (if defined)
3020
- * - `${name}-font-family` (if defined)
3021
- * - `${name}-font-style` (if defined)
3022
- *
3023
- * @param presets - Typography presets object
3024
- * @returns ConfigTokens object with $ prefixed keys
3025
- *
3026
- * @example
3027
- * const customTokens = generateTypographyTokens({
3028
- * myHeading: { fontSize: '24px', lineHeight: '32px', fontWeight: '700' },
3029
- * body: { fontSize: '16px', lineHeight: '24px', fontWeight: '400' },
3030
- * });
3031
- */
3032
- function generateTypographyTokens(presets) {
3033
- const tokens = {};
3034
- for (const [name, preset] of Object.entries(presets)) {
3035
- if (RESERVED_PRESET_NAMES.has(name)) throw new Error(`Invalid typography preset name "${name}". This name is reserved as a preset modifier.`);
3036
- tokens[`$${name}-font-size`] = preset.fontSize;
3037
- tokens[`$${name}-line-height`] = preset.lineHeight;
3038
- tokens[`$${name}-letter-spacing`] = preset.letterSpacing ?? "normal";
3039
- tokens[`$${name}-font-weight`] = preset.fontWeight;
3040
- if (preset.boldFontWeight !== void 0) tokens[`$${name}-bold-font-weight`] = preset.boldFontWeight;
3041
- if (preset.iconSize !== void 0) tokens[`$${name}-icon-size`] = preset.iconSize;
3042
- if (preset.textTransform !== void 0) tokens[`$${name}-text-transform`] = preset.textTransform;
3043
- if (preset.fontFamily !== void 0) tokens[`$${name}-font-family`] = preset.fontFamily;
3044
- if (preset.fontStyle !== void 0) tokens[`$${name}-font-style`] = preset.fontStyle;
3045
- }
3046
- return tokens;
3047
- }
3048
- //#endregion
3049
- //#region src/config.ts
3050
- /**
3051
- * Tasty Configuration Module
3052
- *
3053
- * Centralizes all tasty configuration, including:
3054
- * - Style injector settings (nonce, cleanup thresholds, etc.)
3055
- * - Global predefined states for advanced state mapping
3056
- * - stylesGenerated flag that locks configuration after first style generation
3057
- *
3058
- * Configuration must be done BEFORE any styles are generated.
3059
- * After the first `inject()` call, configuration is locked and attempts to
3060
- * reconfigure will emit a warning and be ignored.
3061
- */
3062
- const emittedWarnings = /* @__PURE__ */ new Set();
3063
- const devMode$1 = isDevEnv();
3064
- /**
3065
- * Emit a warning only once
3066
- */
3067
- function warnOnce(key, message) {
3068
- if (devMode$1 && !emittedWarnings.has(key)) {
3069
- emittedWarnings.add(key);
3070
- console.warn(message);
3071
- }
3072
- }
3073
- let currentConfig = null;
3074
- let globalKeyframes = null;
3075
- let globalFontFace = null;
3076
- let globalCounterStyle = null;
3077
- let globalFunction = null;
3078
- let globalPolyfills = null;
3079
- let globalProperties = null;
3080
- let globalRecipes = null;
3081
- let globalConfigTokens = null;
3082
- let globalStyles = null;
3083
- const GTKEY_TOKENS = "__tasty_cfg_tokens__";
3084
- const GTKEY_FONT_FACE = "__tasty_cfg_font_face__";
3085
- const GTKEY_COUNTER_STYLE = "__tasty_cfg_counter_style__";
3086
- const GTKEY_FUNCTION = "__tasty_cfg_function__";
3087
- const GTKEY_POLYFILLS = "__tasty_cfg_polyfills__";
3088
- const GTKEY_PROPERTIES = "__tasty_cfg_properties__";
3089
- const GTKEY_GLOBAL_STYLES = "__tasty_cfg_global_styles__";
3090
- function setOnGlobalThis(key, value) {
3091
- globalThis[key] = value;
3092
- }
3093
- function getFromGlobalThis(key) {
3094
- return globalThis[key];
3095
- }
3096
- function clearGlobalThisConfig() {
3097
- const g = globalThis;
3098
- delete g[GTKEY_TOKENS];
3099
- delete g[GTKEY_FONT_FACE];
3100
- delete g[GTKEY_COUNTER_STYLE];
3101
- delete g[GTKEY_FUNCTION];
3102
- delete g[GTKEY_POLYFILLS];
3103
- delete g[GTKEY_PROPERTIES];
3104
- delete g[GTKEY_GLOBAL_STYLES];
3105
- }
3106
- /**
3107
- * Default properties shipped with tasty.
3108
- * These are always included unless explicitly overridden via `configure({ properties })`.
3109
- * Keys use tasty token syntax (#name for colors, $name for other properties).
3110
- *
3111
- * For properties with CSS @property-compatible types (length, time, number, color),
3112
- * an `initialValue` is provided so the property works even without a project-level token.
3113
- */
3114
- const DEFAULT_PROPERTIES = {
3115
- "#tasty-second-fill": {
3116
- inherits: false,
3117
- initialValue: "transparent"
3118
- },
3119
- "#current": {
3120
- inherits: true,
3121
- initialValue: "currentcolor"
3122
- },
3123
- "#white": {
3124
- inherits: true,
3125
- initialValue: "rgb(255 255 255)"
3126
- },
3127
- "#black": {
3128
- inherits: true,
3129
- initialValue: "rgb(0 0 0)"
3130
- },
3131
- "#clear": {
3132
- inherits: true,
3133
- initialValue: "transparent"
3134
- },
3135
- "#border": {
3136
- inherits: true,
3137
- initialValue: "rgb(0 0 0)"
3138
- },
3139
- $gap: {
3140
- syntax: "<length>",
3141
- inherits: true,
3142
- initialValue: "4px"
3143
- },
3144
- $radius: {
3145
- syntax: "<length>",
3146
- inherits: true,
3147
- initialValue: "6px"
3148
- },
3149
- "$border-width": {
3150
- syntax: "<length>",
3151
- inherits: true,
3152
- initialValue: "1px"
3153
- },
3154
- "$outline-width": {
3155
- syntax: "<length>",
3156
- inherits: true,
3157
- initialValue: "3px"
3158
- },
3159
- $transition: {
3160
- syntax: "<time>",
3161
- inherits: true,
3162
- initialValue: "80ms"
3163
- },
3164
- "$sharp-radius": {
3165
- syntax: "<length>",
3166
- inherits: true,
3167
- initialValue: "0px"
3168
- },
3169
- "$bold-font-weight": {
3170
- syntax: "<number>",
3171
- inherits: true,
3172
- initialValue: "700"
3173
- },
3174
- "$font-sans-fallback": {
3175
- syntax: "*",
3176
- inherits: true,
3177
- initialValue: "system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, \"Apple Color Emoji\", \"Segoe UI Emoji\", sans-serif"
3178
- },
3179
- "$font-mono-fallback": {
3180
- syntax: "*",
3181
- inherits: true,
3182
- initialValue: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"
3183
- }
3184
- };
3185
- const GLOBAL_INJECTOR_KEY = "__TASTY_GLOBAL_INJECTOR__";
3186
- /**
3187
- * Detect if we're running in a test environment
3188
- */
3189
- function isTestEnvironment() {
3190
- if (typeof global !== "undefined") {
3191
- const g = global;
3192
- if (g.vi || g.jest || g.expect || g.describe || g.it) return true;
3193
- }
3194
- if (typeof window !== "undefined") {
3195
- const ua = window.navigator?.userAgent;
3196
- if (ua?.includes("jsdom") || ua?.includes("HappyDOM")) return true;
3197
- }
3198
- if (typeof globalThis !== "undefined") {
3199
- const gt = globalThis;
3200
- if (gt.vitest || gt.mocha) return true;
3201
- }
3202
- return false;
3203
- }
3204
- /**
3205
- * Create default configuration with optional test environment detection
3206
- */
3207
- function createDefaultConfig(isTest) {
3208
- return {
3209
- maxRulesPerSheet: 8192,
3210
- forceTextInjection: isTest ?? false,
3211
- devMode: isDevEnv(),
3212
- namePrefix: "t"
3213
- };
3214
- }
3215
- /**
3216
- * Mark that styles have been generated (called by injector on first inject)
3217
- * This locks the configuration - no further changes allowed.
3218
- * Also injects internal and global properties.
3219
- */
3220
- function markStylesGenerated() {
3221
- if (!markStylesGeneratedState()) return;
3222
- if (typeof document !== "undefined" && document.querySelector("[data-tasty-ssr]")) {
3223
- warnOnce("ssr-globals-skip", "[Tasty] SSR styles detected — skipping client-side global CSS injection to avoid duplicates.");
3224
- return;
3225
- }
3226
- const injector = getGlobalInjector();
3227
- for (const [token, definition] of Object.entries(getEffectiveProperties())) injector.property(token, definition);
3228
- if (globalFontFace && Object.keys(globalFontFace).length > 0) for (const [family, input] of Object.entries(globalFontFace)) {
3229
- const descriptors = Array.isArray(input) ? input : [input];
3230
- for (const desc of descriptors) injector.fontFace(family, desc);
3231
- }
3232
- if (globalCounterStyle && Object.keys(globalCounterStyle).length > 0) for (const [name, descriptors] of Object.entries(globalCounterStyle)) injector.counterStyle(name, descriptors, { weak: true });
3233
- if (globalFunction && Object.keys(globalFunction).length > 0) for (const [name, definition] of Object.entries(globalFunction)) injector.func(name, definition, { weak: true });
3234
- if (globalConfigTokens && Object.keys(globalConfigTokens).length > 0) {
3235
- const tokenRules = renderStyles(globalConfigTokens, ":root");
3236
- if (tokenRules.length > 0) injector.injectGlobal(tokenRules);
3237
- }
3238
- if (globalStyles) {
3239
- for (const [selector, styles] of Object.entries(globalStyles)) if (Object.keys(styles).length > 0) {
3240
- const rules = renderStyles(styles, selector);
3241
- if (rules.length > 0) injector.injectGlobal(rules);
3242
- }
3243
- }
3244
- }
3245
- let _hasGlobalKeyframes = false;
3246
- /**
3247
- * Check if any global keyframes are configured.
3248
- * Uses a pre-computed flag to avoid Object.keys() allocation on every call.
3249
- */
3250
- function hasGlobalKeyframes() {
3251
- return _hasGlobalKeyframes;
3252
- }
3253
- /**
3254
- * Get global keyframes configuration.
3255
- * Returns null if no keyframes configured (fast path for zero-overhead).
3256
- */
3257
- function getGlobalKeyframes() {
3258
- return globalKeyframes;
3259
- }
3260
- /**
3261
- * Set global keyframes (called from configure).
3262
- * Internal use only.
3263
- */
3264
- function setGlobalKeyframes(keyframes) {
3265
- if (hasStylesGenerated()) {
3266
- warnOnce("keyframes-after-styles", "[Tasty] Cannot update keyframes after styles have been generated.\nThe new keyframes will be ignored.");
3267
- return;
3268
- }
3269
- globalKeyframes = {
3270
- ...globalKeyframes ?? {},
3271
- ...keyframes
3272
- };
3273
- _hasGlobalKeyframes = Object.keys(globalKeyframes).length > 0;
3274
- }
3275
- /**
3276
- * Set global properties (called from configure).
3277
- * Internal use only.
3278
- */
3279
- function setGlobalProperties(properties) {
3280
- if (hasStylesGenerated()) {
3281
- warnOnce("properties-after-styles", "[Tasty] Cannot update properties after styles have been generated.\nThe new properties will be ignored.");
3282
- return;
3283
- }
3284
- globalProperties = {
3285
- ...globalProperties ?? getFromGlobalThis(GTKEY_PROPERTIES) ?? {},
3286
- ...properties
3287
- };
3288
- setOnGlobalThis(GTKEY_PROPERTIES, globalProperties);
3289
- }
3290
- /**
3291
- * Get the effective properties: DEFAULT_PROPERTIES merged with user-configured
3292
- * properties. User properties override defaults with matching keys.
3293
- * Reads from globalThis first for cross-module SSR support.
3294
- */
3295
- function getEffectiveProperties() {
3296
- const props = globalProperties ?? getFromGlobalThis(GTKEY_PROPERTIES);
3297
- if (!props) return DEFAULT_PROPERTIES;
3298
- return {
3299
- ...DEFAULT_PROPERTIES,
3300
- ...props
3301
- };
3302
- }
3303
- /**
3304
- * Get global font-face configuration.
3305
- * Returns null if no font faces configured.
3306
- * Reads from globalThis first for cross-module SSR support.
3307
- */
3308
- function getGlobalFontFaces() {
3309
- return globalFontFace ?? getFromGlobalThis(GTKEY_FONT_FACE) ?? null;
3310
- }
3311
- /**
3312
- * Set global font faces (called from configure).
3313
- * Internal use only.
3314
- */
3315
- function setGlobalFontFace(fontFace) {
3316
- if (hasStylesGenerated()) {
3317
- warnOnce("fontface-after-styles", "[Tasty] Cannot update fontFaces after styles have been generated.\nThe new font faces will be ignored.");
3318
- return;
3319
- }
3320
- globalFontFace = {
3321
- ...getGlobalFontFaces() ?? {},
3322
- ...fontFace
3323
- };
3324
- setOnGlobalThis(GTKEY_FONT_FACE, globalFontFace);
3325
- }
3326
- /**
3327
- * Get global counter-style configuration.
3328
- * Returns null if no counter styles configured.
3329
- * Reads from globalThis first for cross-module SSR support.
3330
- */
3331
- function getGlobalCounterStyles() {
3332
- return globalCounterStyle ?? getFromGlobalThis(GTKEY_COUNTER_STYLE) ?? null;
3333
- }
3334
- /**
3335
- * Set global counter styles (called from configure).
3336
- * Internal use only.
3337
- */
3338
- function setGlobalCounterStyle(counterStyle) {
3339
- if (hasStylesGenerated()) {
3340
- warnOnce("counterstyle-after-styles", "[Tasty] Cannot update counterStyles after styles have been generated.\nThe new counter styles will be ignored.");
3341
- return;
3342
- }
3343
- globalCounterStyle = {
3344
- ...getGlobalCounterStyles() ?? {},
3345
- ...counterStyle
3346
- };
3347
- setOnGlobalThis(GTKEY_COUNTER_STYLE, globalCounterStyle);
3348
- }
3349
- /**
3350
- * Get global @function configuration.
3351
- * Returns null if no functions configured.
3352
- * Reads from globalThis first for cross-module SSR support.
3353
- */
3354
- function getGlobalFunctions() {
3355
- return globalFunction ?? getFromGlobalThis(GTKEY_FUNCTION) ?? null;
3356
- }
3357
- /**
3358
- * Set global functions (called from configure).
3359
- * Internal use only.
3360
- */
3361
- function setGlobalFunction(functions) {
3362
- if (hasStylesGenerated()) {
3363
- warnOnce("function-after-styles", "[Tasty] Cannot update functions after styles have been generated.\nThe new functions will be ignored.");
3364
- return;
3365
- }
3366
- globalFunction = {
3367
- ...getGlobalFunctions() ?? {},
3368
- ...functions
3369
- };
3370
- setOnGlobalThis(GTKEY_FUNCTION, globalFunction);
3371
- }
3372
- /**
3373
- * Whether the CSS `@function` polyfill (inline expansion) is enabled.
3374
- * Reads from globalThis first for cross-module SSR/zero-runtime support.
3375
- */
3376
- function isFunctionsPolyfillEnabled() {
3377
- return (globalPolyfills ?? getFromGlobalThis(GTKEY_POLYFILLS) ?? null)?.functions === true;
3378
- }
3379
- /**
3380
- * Set global polyfill toggles (called from configure).
3381
- * Internal use only.
3382
- */
3383
- function setGlobalPolyfills(polyfills) {
3384
- if (hasStylesGenerated()) {
3385
- warnOnce("polyfills-after-styles", "[Tasty] Cannot update polyfills after styles have been generated.\nThe new polyfill toggles will be ignored.");
3386
- return;
3387
- }
3388
- globalPolyfills = {
3389
- ...globalPolyfills ?? {},
3390
- ...polyfills
3391
- };
3392
- setOnGlobalThis(GTKEY_POLYFILLS, globalPolyfills);
3393
- }
3394
- /**
3395
- * Check if any global recipes are configured.
3396
- * Fast path: returns false if no recipes were ever set.
3397
- */
3398
- function hasGlobalRecipes() {
3399
- return globalRecipes !== null && Object.keys(globalRecipes).length > 0;
3400
- }
3401
- /**
3402
- * Get global recipes configuration.
3403
- * Returns null if no recipes configured (fast path for zero-overhead).
3404
- */
3405
- function getGlobalRecipes() {
3406
- return globalRecipes;
3407
- }
3408
- /**
3409
- * Set global recipes (called from configure).
3410
- * Internal use only.
3411
- */
3412
- function setGlobalRecipes(recipes) {
3413
- if (hasStylesGenerated()) {
3414
- warnOnce("recipes-after-styles", "[Tasty] Cannot update recipes after styles have been generated.\nThe new recipes will be ignored.");
3415
- return;
3416
- }
3417
- if (devMode$1) for (const [name, recipeStyles] of Object.entries(recipes)) {
3418
- 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.");
3419
- for (const key of Object.keys(recipeStyles)) {
3420
- 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.`);
3421
- 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'.`);
3422
- }
3423
- }
3424
- globalRecipes = {
3425
- ...globalRecipes ?? {},
3426
- ...recipes
3427
- };
3428
- }
3429
- /**
3430
- * Get global token styles for :root injection.
3431
- * Returns null if no tokens configured.
3432
- * Reads from globalThis first for cross-module SSR support.
3433
- */
3434
- function getGlobalConfigTokens() {
3435
- return globalConfigTokens ?? getFromGlobalThis(GTKEY_TOKENS) ?? null;
3436
- }
3437
- /**
3438
- * Set global token styles (called from configure).
3439
- * Internal use only.
3440
- */
3441
- function setGlobalConfigTokens(styles) {
3442
- if (hasStylesGenerated()) {
3443
- warnOnce("tokens-after-styles", "[Tasty] Cannot update tokens after styles have been generated.\nThe new tokens will be ignored.");
3444
- return;
3445
- }
3446
- globalConfigTokens = globalConfigTokens ? {
3447
- ...globalConfigTokens,
3448
- ...styles
3449
- } : styles;
3450
- setOnGlobalThis(GTKEY_TOKENS, globalConfigTokens);
3451
- }
3452
- /**
3453
- * Get configured global styles for injection.
3454
- * Returns null if no global styles configured.
3455
- * Reads from globalThis first for cross-module SSR support.
3456
- */
3457
- function getGlobalStyles() {
3458
- return globalStyles ?? getFromGlobalThis(GTKEY_GLOBAL_STYLES) ?? null;
3459
- }
3460
- /**
3461
- * Set configured global styles (called from configure).
3462
- * Internal use only.
3463
- */
3464
- function setGlobalStyles(styles) {
3465
- if (hasStylesGenerated()) {
3466
- warnOnce("globalStyles-after-styles", "[Tasty] Cannot update globalStyles after styles have been generated.\nThe new global styles will be ignored.");
3467
- return;
3468
- }
3469
- if (globalStyles) for (const [selector, selectorStyles] of Object.entries(styles)) globalStyles[selector] = globalStyles[selector] ? {
3470
- ...globalStyles[selector],
3471
- ...selectorStyles
3472
- } : selectorStyles;
3473
- else globalStyles = { ...styles };
3474
- setOnGlobalThis(GTKEY_GLOBAL_STYLES, globalStyles);
3475
- }
3476
- /**
3477
- * Check if configuration is locked (styles have been generated)
3478
- */
3479
- function isConfigLocked() {
3480
- return hasStylesGenerated();
3481
- }
3482
- /**
3483
- * Configure the Tasty style system.
3484
- *
3485
- * Must be called BEFORE any styles are generated (before first render that uses tasty).
3486
- * After styles are generated, configuration is locked and calls to configure() will
3487
- * emit a warning and be ignored.
3488
- *
3489
- * @example
3490
- * ```ts
3491
- * import { configure } from '@tenphi/tasty';
3492
- *
3493
- * // Configure before app renders
3494
- * configure({
3495
- * nonce: 'abc123',
3496
- * states: {
3497
- * '@mobile': '@media(w < 768px)',
3498
- * '@dark': '@root(theme=dark)',
3499
- * },
3500
- * });
3501
- * ```
3502
- */
3503
- function configure(config = {}) {
3504
- if (hasStylesGenerated()) {
3505
- 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.");
3506
- return;
3507
- }
3508
- if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
3509
- let mergedStates = {};
3510
- let mergedUnits = {};
3511
- let mergedFunctions = {};
3512
- let mergedHandlers = {};
3513
- /** Where each handler key came from, so registration warnings can name it. */
3514
- const handlerSources = /* @__PURE__ */ new Map();
3515
- let mergedPropHandlers = {};
3516
- /** Where each prop handler key came from, for its warnings. */
3517
- const propHandlerSources = /* @__PURE__ */ new Map();
3518
- const mergedBaseStyleProps = [];
3519
- let mergedProperties = {};
3520
- let mergedKeyframes = {};
3521
- let mergedFontFaces = {};
3522
- let mergedCounterStyles = {};
3523
- let mergedReplaceTokens = {};
3524
- let mergedConfigTokens = {};
3525
- let mergedRecipes = {};
3526
- let mergedPresets = {};
3527
- const mergedGlobalStyles = {};
3528
- if (config.plugins) for (const plugin of config.plugins) {
3529
- if (plugin.states) mergedStates = {
3530
- ...mergedStates,
3531
- ...plugin.states
3532
- };
3533
- if (plugin.units) mergedUnits = {
3534
- ...mergedUnits,
3535
- ...plugin.units
3536
- };
3537
- if (plugin.functions) mergedFunctions = {
3538
- ...mergedFunctions,
3539
- ...plugin.functions
3540
- };
3541
- if (plugin.handlers) {
3542
- mergedHandlers = {
3543
- ...mergedHandlers,
3544
- ...plugin.handlers
3545
- };
3546
- for (const key of Object.keys(plugin.handlers)) handlerSources.set(key, `plugin "${plugin.name}"`);
3547
- }
3548
- if (plugin.propHandlers) {
3549
- mergedPropHandlers = {
3550
- ...mergedPropHandlers,
3551
- ...plugin.propHandlers
3552
- };
3553
- for (const key of Object.keys(plugin.propHandlers)) propHandlerSources.set(key, `plugin "${plugin.name}"`);
3554
- }
3555
- if (plugin.baseStyleProps) mergedBaseStyleProps.push(...plugin.baseStyleProps);
3556
- if (plugin.properties) mergedProperties = {
3557
- ...mergedProperties,
3558
- ...plugin.properties
3559
- };
3560
- if (plugin.keyframes) mergedKeyframes = {
3561
- ...mergedKeyframes,
3562
- ...plugin.keyframes
3563
- };
3564
- if (plugin.fontFaces) mergedFontFaces = {
3565
- ...mergedFontFaces,
3566
- ...plugin.fontFaces
3567
- };
3568
- if (plugin.counterStyles) mergedCounterStyles = {
3569
- ...mergedCounterStyles,
3570
- ...plugin.counterStyles
3571
- };
3572
- if (plugin.replaceTokens) mergedReplaceTokens = {
3573
- ...mergedReplaceTokens,
3574
- ...plugin.replaceTokens
3575
- };
3576
- if (plugin.tokens) mergedConfigTokens = {
3577
- ...mergedConfigTokens,
3578
- ...plugin.tokens
3579
- };
3580
- if (plugin.recipes) mergedRecipes = {
3581
- ...mergedRecipes,
3582
- ...plugin.recipes
3583
- };
3584
- if (plugin.presets) mergedPresets = {
3585
- ...mergedPresets,
3586
- ...plugin.presets
3587
- };
3588
- if (plugin.globalStyles) for (const [sel, styles] of Object.entries(plugin.globalStyles)) mergedGlobalStyles[sel] = mergedGlobalStyles[sel] ? {
3589
- ...mergedGlobalStyles[sel],
3590
- ...styles
3591
- } : styles;
3592
- }
3593
- if (config.states) mergedStates = {
3594
- ...mergedStates,
3595
- ...config.states
3596
- };
3597
- if (config.units) mergedUnits = {
3598
- ...mergedUnits,
3599
- ...config.units
3600
- };
3601
- if (config.functions) mergedFunctions = {
3602
- ...mergedFunctions,
3603
- ...config.functions
3604
- };
3605
- if (config.handlers) {
3606
- mergedHandlers = {
3607
- ...mergedHandlers,
3608
- ...config.handlers
3609
- };
3610
- for (const key of Object.keys(config.handlers)) handlerSources.set(key, "configure()");
3611
- }
3612
- if (config.propHandlers) {
3613
- mergedPropHandlers = {
3614
- ...mergedPropHandlers,
3615
- ...config.propHandlers
3616
- };
3617
- for (const key of Object.keys(config.propHandlers)) propHandlerSources.set(key, "configure()");
3618
- }
3619
- if (config.baseStyleProps) mergedBaseStyleProps.push(...config.baseStyleProps);
3620
- if (config.properties) mergedProperties = {
3621
- ...mergedProperties,
3622
- ...config.properties
3623
- };
3624
- if (config.keyframes) mergedKeyframes = {
3625
- ...mergedKeyframes,
3626
- ...config.keyframes
3627
- };
3628
- if (config.fontFaces) mergedFontFaces = {
3629
- ...mergedFontFaces,
3630
- ...config.fontFaces
3631
- };
3632
- if (config.counterStyles) mergedCounterStyles = {
3633
- ...mergedCounterStyles,
3634
- ...config.counterStyles
3635
- };
3636
- if (config.replaceTokens) mergedReplaceTokens = {
3637
- ...mergedReplaceTokens,
3638
- ...config.replaceTokens
3639
- };
3640
- if (config.presets) mergedPresets = {
3641
- ...mergedPresets,
3642
- ...config.presets
3643
- };
3644
- if (Object.keys(mergedPresets).length > 0) mergedConfigTokens = {
3645
- ...generateTypographyTokens(mergedPresets),
3646
- ...mergedConfigTokens
3647
- };
3648
- if (config.tokens) mergedConfigTokens = {
3649
- ...mergedConfigTokens,
3650
- ...config.tokens
3651
- };
3652
- if (config.recipes) mergedRecipes = {
3653
- ...mergedRecipes,
3654
- ...config.recipes
3655
- };
3656
- if (config.globalStyles) for (const [sel, styles] of Object.entries(config.globalStyles)) mergedGlobalStyles[sel] = mergedGlobalStyles[sel] ? {
3657
- ...mergedGlobalStyles[sel],
3658
- ...styles
3659
- } : styles;
3660
- if (devMode$1) {
3661
- const tokenKeys = new Set(Object.keys(mergedConfigTokens));
3662
- for (const key of Object.keys(mergedReplaceTokens)) if (tokenKeys.has(key)) warnOnce(`token-conflict-${key}`, `[Tasty] Token "${key}" is defined in both \`tokens\` and \`replaceTokens\`. \`replaceTokens\` performs parse-time substitution, so the \`tokens\` CSS custom property will be injected but never used by Tasty styles. Remove it from one of the two.`);
3663
- }
3664
- if (config.colorSpace) warnOnce("colorSpace-deprecated", "[Tasty] configure({ colorSpace }) no longer has any effect and will be removed in the next major. Colors are emitted as authored, and opacity is applied with relative color syntax, which reads a color's channels in the browser. To address channels yourself, write `oklch(from var(--name-color) l c h)` against the token.");
3665
- if (Object.keys(mergedStates).length > 0) setGlobalPredefinedStates(mergedStates);
3666
- const parser = getGlobalParser();
3667
- if (config.parserCacheSize !== void 0) parser.updateOptions({ cacheSize: config.parserCacheSize });
3668
- if (Object.keys(mergedUnits).length > 0) {
3669
- const currentUnits = parser.getUnits() ?? CUSTOM_UNITS;
3670
- parser.setUnits({
3671
- ...currentUnits,
3672
- ...mergedUnits
3673
- });
3674
- }
3675
- if (config.polyfills) setGlobalPolyfills(config.polyfills);
3676
- const { parseFuncs, functionDefs } = splitFunctions(mergedFunctions, (key, kind) => {
3677
- warnOnce(`functions-mismatch-${key}`, kind === "expected-definition" ? `[Tasty] functions["${key}"]: a "$$"-prefixed key denotes a CSS @function and expects a definition object, but received a function. Entry ignored.` : `[Tasty] functions["${key}"]: a bare key denotes a parse function and expects a function value, but received an object. Did you mean "$$${key}"? Entry ignored.`);
3678
- });
3679
- if (Object.keys(parseFuncs).length > 0) {
3680
- const currentFuncs = getGlobalParseFunctions();
3681
- const finalFuncs = {
3682
- ...currentFuncs,
3683
- ...parseFuncs
3684
- };
3685
- parser.setFunctions(finalFuncs);
3686
- Object.assign(currentFuncs, parseFuncs);
3687
- }
3688
- if (Object.keys(functionDefs).length > 0) if (isFunctionsPolyfillEnabled()) for (const [name, definition] of Object.entries(functionDefs)) registerFunctionPolyfill(name, definition);
3689
- else setGlobalFunction(functionDefs);
3690
- if (Object.keys(mergedKeyframes).length > 0) setGlobalKeyframes(mergedKeyframes);
3691
- if (Object.keys(mergedProperties).length > 0) setGlobalProperties(mergedProperties);
3692
- if (Object.keys(mergedFontFaces).length > 0) setGlobalFontFace(mergedFontFaces);
3693
- if (Object.keys(mergedCounterStyles).length > 0) setGlobalCounterStyle(mergedCounterStyles);
3694
- if (Object.keys(mergedHandlers).length > 0) for (const [name, definition] of Object.entries(mergedHandlers)) registerHandler(normalizeHandlerDefinition(name, definition), {
3695
- key: name,
3696
- source: handlerSources.get(name)
3697
- });
3698
- if (Object.keys(mergedPropHandlers).length > 0) for (const [name, definition] of Object.entries(mergedPropHandlers)) registerPropHandler(name, definition, { source: propHandlerSources.get(name) });
3699
- if (mergedBaseStyleProps.length > 0) registerBaseStyleProps(mergedBaseStyleProps);
3700
- if (Object.keys(mergedReplaceTokens).length > 0) {
3701
- const processedTokens = {};
3702
- for (const [key, value] of Object.entries(mergedReplaceTokens)) if (key.startsWith("#")) {
3703
- const normalized = normalizeColorTokenValue(value);
3704
- if (normalized === null) continue;
3705
- processedTokens[key] = String(normalized);
3706
- } else if (value === false) continue;
3707
- else processedTokens[key] = String(value);
3708
- setGlobalPredefinedTokens(processedTokens);
3709
- }
3710
- if (Object.keys(mergedConfigTokens).length > 0) setGlobalConfigTokens(mergedConfigTokens);
3711
- if (Object.keys(mergedRecipes).length > 0) setGlobalRecipes(mergedRecipes);
3712
- if (Object.keys(mergedGlobalStyles).length > 0) setGlobalStyles(mergedGlobalStyles);
3713
- 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;
3714
- const fullConfig = {
3715
- ...createDefaultConfig(),
3716
- ...currentConfig,
3717
- ...injectorConfig
3718
- };
3719
- currentConfig = fullConfig;
3720
- const storage = typeof window !== "undefined" ? window : globalThis;
3721
- storage[GLOBAL_INJECTOR_KEY] = new StyleInjector(fullConfig);
3722
- }
3723
- /**
3724
- * Get the current configuration.
3725
- * If not configured, returns default configuration.
3726
- */
3727
- function getConfig() {
3728
- if (!currentConfig) currentConfig = createDefaultConfig(isTestEnvironment());
3729
- return currentConfig;
3730
- }
3731
- /**
3732
- * Get the configured prefix used for every generated identifier
3733
- * (class names, keyframe names, counter-style names).
3734
- *
3735
- * Falls back to the default prefix (`'t'`) when `configure()` has not
3736
- * been called yet — this matches the auto-configuration behavior used
3737
- * by the rest of the system.
3738
- */
3739
- function getNamePrefix() {
3740
- return currentConfig?.namePrefix ?? "t";
3741
- }
3742
- /**
3743
- * Get the global injector instance.
3744
- * Auto-configures with defaults if not already configured.
3745
- */
3746
- function getGlobalInjector() {
3747
- const storage = typeof window !== "undefined" ? window : globalThis;
3748
- if (!storage[GLOBAL_INJECTOR_KEY]) configure();
3749
- return storage[GLOBAL_INJECTOR_KEY];
3750
- }
3751
- /**
3752
- * Reset configuration (for testing only).
3753
- * Clears the global injector and allows reconfiguration.
3754
- */
3755
- function resetConfig() {
3756
- resetStylesGeneratedState();
3757
- currentConfig = null;
3758
- globalKeyframes = null;
3759
- _hasGlobalKeyframes = false;
3760
- globalProperties = null;
3761
- globalFontFace = null;
3762
- globalCounterStyle = null;
3763
- globalFunction = null;
3764
- globalPolyfills = null;
3765
- globalRecipes = null;
3766
- globalConfigTokens = null;
3767
- globalStyles = null;
3768
- clearGlobalThisConfig();
3769
- resetGlobalPredefinedTokens();
3770
- resetGlobalParseFunctions();
3771
- resetFunctionPolyfills();
3772
- resetHandlers();
3773
- resetStyleChunks();
3774
- resetPropHandlers();
3775
- resetBaseStyleProps();
3776
- clearPipelineCache();
3777
- emittedWarnings.clear();
3778
- resetStyleWarnings();
3779
- const storage = typeof window !== "undefined" ? window : globalThis;
3780
- delete storage[GLOBAL_INJECTOR_KEY];
3781
- }
3782
- //#endregion
3783
- //#region src/injector/chunk-sheet-registry.ts
3784
- /**
3785
- * Global registry mapping CSS content hashes to shared constructable
3786
- * CSSStyleSheet objects with reference counting.
3787
- *
3788
- * Multiple shadow roots adopting the same chunk share a single underlying
3789
- * stylesheet object — parse once, adopt everywhere.
3790
- */
3791
- var ChunkSheetRegistry = class {
3792
- sheets = /* @__PURE__ */ new Map();
3793
- sheetToHash = /* @__PURE__ */ new WeakMap();
3794
- /**
3795
- * Get or create a CSSStyleSheet for the given CSS text.
3796
- * Increments refCount. Uses content hash as the dedup key.
3797
- */
3798
- acquire(cssText) {
3799
- const hash = hashString(cssText);
3800
- const existing = this.sheets.get(hash);
3801
- if (existing) {
3802
- existing.refCount++;
3803
- return existing.sheet;
3804
- }
3805
- const sheet = new CSSStyleSheet();
3806
- sheet.replaceSync(cssText);
3807
- const entry = {
3808
- sheet,
3809
- cssText,
3810
- refCount: 1
3811
- };
3812
- this.sheets.set(hash, entry);
3813
- this.sheetToHash.set(sheet, hash);
3814
- return sheet;
3815
- }
3816
- /**
3817
- * Decrement refCount for a sheet. When refCount reaches 0,
3818
- * the sheet is removed from the registry.
3819
- */
3820
- release(sheet) {
3821
- const hash = this.sheetToHash.get(sheet);
3822
- if (!hash) return;
3823
- const entry = this.sheets.get(hash);
3824
- if (!entry) return;
3825
- entry.refCount--;
3826
- if (entry.refCount <= 0) {
3827
- this.sheets.delete(hash);
3828
- this.sheetToHash.delete(sheet);
3829
- }
3830
- }
3831
- /**
3832
- * Bulk acquire — returns an array of CSSStyleSheet in the same order.
3833
- */
3834
- acquireAll(cssTexts) {
3835
- return cssTexts.map((text) => this.acquire(text));
3836
- }
3837
- /**
3838
- * Bulk release — decrements refCount for each sheet.
3839
- */
3840
- releaseAll(sheets) {
3841
- for (const sheet of sheets) this.release(sheet);
3842
- }
3843
- /** Number of unique sheets currently held. */
3844
- get size() {
3845
- return this.sheets.size;
3846
- }
3847
- };
3848
- /** Module-level singleton shared across the entire application. */
3849
- const chunkSheetRegistry = new ChunkSheetRegistry();
3850
- //#endregion
3851
- //#region src/injector/index.ts
3852
- /**
3853
- * Inject styles and return className with dispose function
3854
- */
3855
- function inject(rules, options) {
3856
- const injector = getGlobalInjector();
3857
- markStylesGenerated();
3858
- return injector.inject(rules, options);
3859
- }
3860
- /**
3861
- * Inject global rules that should not reserve tasty class names
3862
- */
3863
- function injectGlobal(rules, options) {
3864
- return getGlobalInjector().injectGlobal(rules, options);
3865
- }
3866
- /**
3867
- * Inject raw CSS text directly without parsing
3868
- * This is a low-overhead method for injecting raw CSS that doesn't need tasty processing.
3869
- * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking.
3870
- *
3871
- * @example
3872
- * ```tsx
3873
- * // Inject raw CSS
3874
- * const { dispose } = injectRawCSS(`
3875
- * body { margin: 0; padding: 0; }
3876
- * .my-class { color: red; }
3877
- * `);
3878
- *
3879
- * // Later, remove the injected CSS
3880
- * dispose();
3881
- * ```
3882
- */
3883
- function injectRawCSS(css, options) {
3884
- return getGlobalInjector().injectRawCSS(css, options);
3885
- }
3886
- /**
3887
- * Get raw CSS text for SSR
3888
- */
3889
- function getRawCSSText(options) {
3890
- return getGlobalInjector().getRawCSSText(options);
3891
- }
3892
- /**
3893
- * Inject keyframes and return object with toString() and dispose()
3894
- */
3895
- function keyframes(steps, nameOrOptions) {
3896
- return getGlobalInjector().keyframes(steps, nameOrOptions);
3897
- }
3898
- /**
3899
- * Define a CSS @property custom property.
3900
- * This enables advanced features like animating custom properties.
3901
- *
3902
- * Note: @property rules are global and persistent once defined.
3903
- * Re-registering the same property name is a no-op.
3904
- *
3905
- * @param name - The custom property name (must start with --)
3906
- * @param options - Property configuration
3907
- *
3908
- * @example
3909
- * ```ts
3910
- * // Define a color property that can be animated
3911
- * property('--my-color', {
3912
- * syntax: '<color>',
3913
- * initialValue: 'red',
3914
- * });
3915
- *
3916
- * // Define an angle property
3917
- * property('--rotation', {
3918
- * syntax: '<angle>',
3919
- * inherits: false,
3920
- * initialValue: '0deg',
3921
- * });
3922
- * ```
3923
- */
3924
- function property(name, options) {
3925
- return getGlobalInjector().property(name, options);
3926
- }
3927
- /**
3928
- * Check if a CSS @property has already been defined
3929
- *
3930
- * @param name - The custom property name to check
3931
- * @param options - Options including root
3932
- */
3933
- function isPropertyDefined(name, options) {
3934
- return getGlobalInjector().isPropertyDefined(name, options);
3935
- }
3936
- /**
3937
- * Inject a CSS @font-face rule.
3938
- *
3939
- * Permanent and global — no dispose or ref-counting.
3940
- * Deduplicates by content hash (family + descriptors).
3941
- */
3942
- function fontFace(family, descriptors, options) {
3943
- return getGlobalInjector().fontFace(family, descriptors, options);
3944
- }
3945
- /**
3946
- * Inject a CSS @counter-style rule.
3947
- *
3948
- * Permanent and global — no dispose or ref-counting. Deduplicates by name and
3949
- * overrides an existing rule by default. Pass `weak: true` for global
3950
- * `configure()` definitions, which never clobber an existing rule.
3951
- */
3952
- function counterStyle(name, descriptors, options) {
3953
- return getGlobalInjector().counterStyle(name, descriptors, options);
3954
- }
3955
- /**
3956
- * Inject a CSS @function rule (custom function).
3957
- *
3958
- * Permanent and global — no dispose or ref-counting. Deduplicates by function
3959
- * name and overrides an existing rule by default. Pass `weak: true` for global
3960
- * `configure()` definitions, which never clobber an existing rule.
3961
- *
3962
- * @param name - The function name (`$$name`, `$name`, or `--name`)
3963
- * @param definition - Function definition (args, returns, result, local vars)
3964
- *
3965
- * @example
3966
- * ```ts
3967
- * func('$$negative', { args: ['$value'], result: '(-1 * $value)' });
3968
- * ```
3969
- */
3970
- function func(name, definition, options) {
3971
- return getGlobalInjector().func(name, definition, options);
3972
- }
3973
- /**
3974
- * Get CSS text from all sheets (for SSR)
3975
- */
3976
- function getCSSText(options) {
3977
- return getGlobalInjector().getCSSText(options);
3978
- }
3979
- /**
3980
- * Collect only CSS used by a rendered subtree (like jest-styled-components).
3981
- * Pass the container returned by render(...).
3982
- */
3983
- function getCSSTextForNode(node, options) {
3984
- const classRegex = tastyClassRegex(getNamePrefix());
3985
- const classSet = /* @__PURE__ */ new Set();
3986
- const readClasses = (el) => {
3987
- const cls = el.getAttribute("class");
3988
- if (!cls) return;
3989
- for (const token of cls.split(/\s+/)) if (classRegex.test(token)) classSet.add(token);
3990
- };
3991
- if (node.getAttribute) readClasses(node);
3992
- const elements = node.querySelectorAll ? node.querySelectorAll("[class]") : [];
3993
- if (elements) elements.forEach(readClasses);
3994
- return getGlobalInjector().getCSSTextForClasses(classSet, options);
3995
- }
3996
- /**
3997
- * Take a reference on local `@keyframes` under the deterministic names the
3998
- * caller resolved, and report which entry holds each. Pair with
3999
- * `ownKeyframes()` once the rules exist to inspect.
4000
- */
4001
- function holdKeyframes(steps, names, options) {
4002
- return getGlobalInjector().holdKeyframes(steps, names, options);
4003
- }
4004
- /**
4005
- * Record that a class animates these keyframes, so the reference is released
4006
- * when the last such class is collected.
4007
- */
4008
- function ownKeyframes(key, className, options) {
4009
- getGlobalInjector().ownKeyframes(key, className, options);
4010
- }
4011
- /**
4012
- * Remove every injected rule that is neither in the DOM nor held by an
4013
- * outstanding `inject()` handle. Equivalent to `gc({ force: true })`.
4014
- */
4015
- function cleanup(root) {
4016
- return getGlobalInjector().cleanup(root);
4017
- }
4018
- /**
4019
- * Count a render, so that a collection pass is scheduled every
4020
- * `gc.touchInterval` of them. Used internally by computeStyles and tasty().
4021
- *
4022
- * @deprecated The class name is ignored — collection asks the DOM what is
4023
- * rendered rather than recording usage per class. The argument is kept so
4024
- * existing calls still compile.
4025
- */
4026
- function touch(className, options) {
4027
- if (!getConfig().gc) return;
4028
- getGlobalInjector().touch(className, options);
4029
- }
4030
- /**
4031
- * Synchronous garbage collection of unused styles — those no element carries
4032
- * and no `inject()` handle references. Evicts the oldest ones once they exceed
4033
- * capacity. With `{ force: true }`, removes all of them regardless of capacity.
4034
- *
4035
- * @returns Number of styles evicted.
4036
- */
4037
- function gc(options) {
4038
- return getGlobalInjector().gc(options);
4039
- }
4040
- /**
4041
- * Get the global injector instance for debugging
4042
- */
4043
- const injector = { get instance() {
4044
- return getGlobalInjector();
4045
- } };
4046
- /**
4047
- * Destroy all resources and clean up
4048
- */
4049
- function destroy(root) {
4050
- return getGlobalInjector().destroy(root);
4051
- }
4052
- /**
4053
- * Create a new isolated injector instance
4054
- */
4055
- function createInjector(config = {}) {
4056
- return new StyleInjector({
4057
- ...getConfig(),
4058
- forceTextInjection: config.forceTextInjection ?? isTestEnvironment(),
4059
- ...config
4060
- });
4061
- }
4062
- //#endregion
4063
- //#region src/keyframes/index.ts
4064
- const KEYFRAMES_KEY = "@keyframes";
4065
- /**
4066
- * Pattern to extract animation names from CSS animation property values.
4067
- * Animation name is typically the first identifier in the shorthand.
4068
- * Handles: "fadeIn 300ms ease-in", "pulse 1s infinite", etc.
4069
- *
4070
- * CSS animation shorthand order (all optional except name):
4071
- * animation: name | duration | timing | delay | iteration | direction | fill-mode | play-state
4072
- *
4073
- * Animation names must:
4074
- * - Start with a letter, underscore, or hyphen (but not a digit or CSS keyword)
4075
- * - Not be CSS keywords: none, initial, inherit, unset, revert
4076
- */
4077
- const CSS_KEYWORDS = new Set([
4078
- "none",
4079
- "initial",
4080
- "inherit",
4081
- "unset",
4082
- "revert",
4083
- "auto",
4084
- "normal",
4085
- "running",
4086
- "paused"
4087
- ]);
4088
- /**
4089
- * Pattern to match animation name at the start of an animation value.
4090
- * Must start with letter, underscore, or hyphen (not digit).
4091
- */
4092
- const ANIMATION_NAME_PATTERN = /^([a-zA-Z_-][a-zA-Z0-9_-]*)/;
4093
- /**
4094
- * Check if styles object has local @keyframes definition.
4095
- * Fast path: single property lookup.
4096
- */
4097
- function hasLocalKeyframes(styles) {
4098
- return KEYFRAMES_KEY in styles;
4099
- }
4100
- /**
4101
- * Extract local @keyframes from styles object.
4102
- * Returns null if no local keyframes (fast path).
4103
- */
4104
- function extractLocalKeyframes(styles) {
4105
- const keyframes = styles[KEYFRAMES_KEY];
4106
- if (!keyframes || typeof keyframes !== "object") return null;
4107
- return keyframes;
4108
- }
4109
- /**
4110
- * Merge local and global keyframes.
4111
- * Local keyframes take priority over global.
4112
- * Returns null if no keyframes exist (fast path).
4113
- */
4114
- function mergeKeyframes(local, global) {
4115
- if (!local && !global) return null;
4116
- if (!local) return global;
4117
- if (!global) return local;
4118
- return {
4119
- ...global,
4120
- ...local
4121
- };
4122
- }
4123
- /**
4124
- * Extract animation name from a single animation value.
4125
- * Returns null if no valid name found.
4126
- *
4127
- * Examples:
4128
- * - "fadeIn 300ms ease-in" → "fadeIn"
4129
- * - "1s pulse infinite" → "pulse" (name can be anywhere)
4130
- * - "none" → null (CSS keyword)
4131
- * - "300ms ease-in" → null (no name, just duration/timing)
4132
- */
4133
- function extractAnimationNameFromValue(value) {
4134
- const trimmed = value.trim();
4135
- if (!trimmed) return null;
4136
- const parts = trimmed.split(/\s+/);
4137
- for (const part of parts) {
4138
- if (CSS_KEYWORDS.has(part.toLowerCase())) continue;
4139
- if (/^-?[\d.]+m?s$/i.test(part)) continue;
4140
- if (part === "infinite" || /^\d+$/.test(part)) continue;
4141
- if ([
4142
- "normal",
4143
- "reverse",
4144
- "alternate",
4145
- "alternate-reverse"
4146
- ].includes(part.toLowerCase())) continue;
4147
- if ([
4148
- "forwards",
4149
- "backwards",
4150
- "both"
4151
- ].includes(part.toLowerCase())) continue;
4152
- if (["running", "paused"].includes(part.toLowerCase())) continue;
4153
- if (/^(ease|linear|ease-in|ease-out|ease-in-out|step-start|step-end)$/i.test(part)) continue;
4154
- if (/^(cubic-bezier|steps)\(/i.test(part)) continue;
4155
- const match = ANIMATION_NAME_PATTERN.exec(part);
4156
- if (match) return match[1];
4157
- }
4158
- return null;
4159
- }
4160
- /**
4161
- * Extract all animation names from an animation property value.
4162
- * Handles multiple animations separated by commas.
4163
- *
4164
- * Example: "fadeIn 300ms, slideIn 500ms ease-out" → ["fadeIn", "slideIn"]
4165
- */
4166
- function extractAnimationNamesFromAnimationValue(value) {
4167
- const names = [];
4168
- const animations = value.split(",");
4169
- for (const animation of animations) {
4170
- const name = extractAnimationNameFromValue(animation);
4171
- if (name && !names.includes(name)) names.push(name);
4172
- }
4173
- return names;
4174
- }
4175
- /**
4176
- * Extract animation names from a style value (handles mappings and arrays).
4177
- */
4178
- function extractAnimationNamesFromStyleValue(value, names) {
4179
- if (typeof value === "string") for (const name of extractAnimationNamesFromAnimationValue(value)) names.add(name);
4180
- else if (Array.isArray(value)) for (const v of value) extractAnimationNamesFromStyleValue(v, names);
4181
- else if (value && typeof value === "object") for (const v of Object.values(value)) extractAnimationNamesFromStyleValue(v, names);
4182
- }
4183
- /**
4184
- * Extract all animation names referenced in styles.
4185
- * Scans 'animation' and 'animationName' properties including in state mappings.
4186
- * Returns empty set if no animation properties found (fast path).
4187
- */
4188
- function extractAnimationNamesFromStyles(styles) {
4189
- const names = /* @__PURE__ */ new Set();
4190
- if ("animation" in styles) extractAnimationNamesFromStyleValue(styles.animation, names);
4191
- if ("animationName" in styles) extractAnimationNamesFromStyleValue(styles.animationName, names);
4192
- for (const [key, value] of Object.entries(styles)) {
4193
- if (key === "$" || key === KEYFRAMES_KEY) continue;
4194
- if ((key.startsWith("&") || key.startsWith(".") || /^[A-Z]/.test(key)) && value && typeof value === "object") {
4195
- const nestedNames = extractAnimationNamesFromStyles(value);
4196
- for (const name of nestedNames) names.add(name);
4197
- }
4198
- }
4199
- return names;
4200
- }
4201
- /**
4202
- * Replace animation names in CSS declarations with injected names.
4203
- * Optimized to avoid regex creation - uses simple string replacement.
4204
- *
4205
- * @param declarations CSS declarations string
4206
- * @param nameMap Map from original name to injected name (only contains names that differ)
4207
- * @returns Updated declarations string
4208
- */
4209
- /**
4210
- * The name a local `@keyframes` block is emitted under.
4211
- *
4212
- * Content-addressed, and a pure function of what was authored: the same steps
4213
- * always produce the same name, and different steps under the same authored
4214
- * name cannot collide. That is what lets the server and the client agree —
4215
- * a name handed out by whichever injector happened to see the definition first
4216
- * depends on run order, so the two would disagree on both the animation name
4217
- * and the class that references it.
4218
- */
4219
- function resolveKeyframesName(authored, steps) {
4220
- return `${authored}-${hashString(JSON.stringify(steps))}`;
4221
- }
4222
- /**
4223
- * The emitted name for every local `@keyframes` in `styles`, by authored name.
4224
- * Pure — the server, the RSC pass and the client all reach the same answer.
4225
- */
4226
- function resolveKeyframesNames(used) {
4227
- const names = /* @__PURE__ */ new Map();
4228
- for (const [authored, steps] of Object.entries(used)) names.set(authored, resolveKeyframesName(authored, steps));
4229
- return names;
4230
- }
4231
- /**
4232
- * Whether `declarations` names `animation` as an animation to run.
4233
- *
4234
- * Only `animation` and `animation-name` values count, and only whole tokens
4235
- * within them: an animation called `crossfade` does not use keyframes called
4236
- * `fade`. Same parsing as `replaceAnimationNames`, so the two agree on what
4237
- * counts as a reference.
4238
- */
4239
- function referencesAnimation(declarations, animation) {
4240
- if (!declarations.includes("animation")) return false;
4241
- for (const part of declarations.split(";")) {
4242
- const colonIdx = part.indexOf(":");
4243
- if (colonIdx === -1) continue;
4244
- const prop = part.slice(0, colonIdx).trim().toLowerCase();
4245
- if (prop !== "animation" && prop !== "animation-name") continue;
4246
- const value = part.slice(colonIdx + 1);
4247
- if (replaceWord(value, animation, "\0") !== value) return true;
4248
- }
4249
- return false;
4250
- }
4251
- function replaceAnimationNames(declarations, nameMap) {
4252
- if (!declarations.includes("animation")) return declarations;
4253
- const parts = declarations.split(";");
4254
- let modified = false;
4255
- for (let i = 0; i < parts.length; i++) {
4256
- const part = parts[i];
4257
- const colonIdx = part.indexOf(":");
4258
- if (colonIdx === -1) continue;
4259
- const prop = part.slice(0, colonIdx).trim().toLowerCase();
4260
- if (prop === "animation" || prop === "animation-name") {
4261
- const prefix = part.slice(0, colonIdx + 1);
4262
- let value = part.slice(colonIdx + 1);
4263
- for (const [original, injected] of nameMap) {
4264
- const newValue = replaceWord(value, original, injected);
4265
- if (newValue !== value) {
4266
- value = newValue;
4267
- modified = true;
4268
- }
4269
- }
4270
- parts[i] = prefix + value;
4271
- }
4272
- }
4273
- return modified ? parts.join(";") : declarations;
4274
- }
4275
- /**
4276
- * Replace a word in a string (word boundary aware, no regex).
4277
- */
4278
- function replaceWord(str, word, replacement) {
4279
- let result = str;
4280
- let idx = 0;
4281
- while ((idx = result.indexOf(word, idx)) !== -1) {
4282
- const before = idx === 0 ? " " : result[idx - 1];
4283
- const after = idx + word.length >= result.length ? " " : result[idx + word.length];
4284
- const isWordBoundaryBefore = !/[a-zA-Z0-9_-]/.test(before);
4285
- const isWordBoundaryAfter = !/[a-zA-Z0-9_-]/.test(after);
4286
- if (isWordBoundaryBefore && isWordBoundaryAfter) {
4287
- result = result.slice(0, idx) + replacement + result.slice(idx + word.length);
4288
- idx += replacement.length;
4289
- } else idx += word.length;
4290
- }
4291
- return result;
4292
- }
4293
- /**
4294
- * Filter keyframes to only those that are actually used.
4295
- * Returns null if no keyframes are used (fast path).
4296
- */
4297
- function filterUsedKeyframes(keyframes, usedNames) {
4298
- if (!keyframes || usedNames.size === 0) return null;
4299
- const used = {};
4300
- let hasAny = false;
4301
- for (const name of usedNames) if (keyframes[name]) {
4302
- used[name] = keyframes[name];
4303
- hasAny = true;
4304
- }
4305
- return hasAny ? used : null;
4306
- }
4307
- //#endregion
4308
- //#region src/ssr/ssr-collector-ref.ts
4309
- const GETTER_KEY = "__tasty_ssr_collector_getter__";
4310
- let _getSSRCollector = null;
4311
- /**
4312
- * Register the collector getter in the current module graph only.
4313
- * Used by Next.js TastyRegistry.
4314
- */
4315
- function registerSSRCollectorGetter(fn) {
4316
- _getSSRCollector = fn;
4317
- }
4318
- /**
4319
- * Register the collector getter on globalThis so it is visible across
4320
- * separate module graphs (e.g. Astro middleware ↔ page components).
4321
- */
4322
- function registerSSRCollectorGetterGlobal(fn) {
4323
- globalThis[GETTER_KEY] = fn;
4324
- }
4325
- /**
4326
- * Retrieve the SSR collector: module-level first, globalThis fallback.
4327
- */
4328
- function getRegisteredSSRCollector() {
4329
- if (_getSSRCollector) return _getSSRCollector();
4330
- const getter = globalThis[GETTER_KEY];
4331
- return getter ? getter() : null;
4332
- }
4333
- //#endregion
4334
- //#region src/ssr/format-property.ts
4335
- /**
4336
- * Format a single @property rule as a CSS string.
4337
- *
4338
- * Returns the full `@property --name { ... }` text, or empty string
4339
- * if the token is invalid.
4340
- */
4341
- function formatPropertyCSS(token, definition) {
4342
- const result = getEffectiveDefinition(token, definition);
4343
- if (!result.isValid) return "";
4344
- return buildPropertyRule(result.cssName, result.definition);
4345
- }
4346
- function buildPropertyRule(cssName, definition) {
4347
- const parts = [];
4348
- if (definition.syntax != null) {
4349
- let syntax = String(definition.syntax).trim();
4350
- if (!/^['"]/u.test(syntax)) syntax = `"${syntax}"`;
4351
- parts.push(`syntax: ${syntax};`);
4352
- }
4353
- const inherits = definition.inherits ?? true;
4354
- parts.push(`inherits: ${inherits ? "true" : "false"};`);
4355
- if (definition.initialValue != null) {
4356
- let initialValueStr;
4357
- if (typeof definition.initialValue === "number") initialValueStr = String(definition.initialValue);
4358
- else initialValueStr = parseStyle(definition.initialValue).output;
4359
- parts.push(`initial-value: ${initialValueStr};`);
4360
- }
4361
- return `@property ${cssName} { ${parts.join(" ").trim()} }`;
4362
- }
4363
- //#endregion
4364
- //#region src/ssr/format-keyframes.ts
4365
- /**
4366
- * Convert keyframes steps to a CSS string.
4367
- * Replicates SheetManager.stepsToCSS() without the class instance.
4368
- */
4369
- function stepsToCSS(steps) {
4370
- const rules = [];
4371
- for (const [key, value] of Object.entries(steps)) {
4372
- if (typeof value === "string") {
4373
- rules.push(`${key} { ${value.trim()} }`);
4374
- continue;
4375
- }
4376
- const styleMap = value || {};
4377
- const styleNames = Object.keys(styleMap).sort();
4378
- const handlerQueue = [];
4379
- const seenHandlers = /* @__PURE__ */ new Set();
4380
- styleNames.forEach((styleName) => {
4381
- let handlers = STYLE_HANDLER_MAP[styleName];
4382
- if (!handlers) handlers = STYLE_HANDLER_MAP[styleName] = [createStyle(styleName)];
4383
- handlers.forEach((handler) => {
4384
- if (!seenHandlers.has(handler)) {
4385
- seenHandlers.add(handler);
4386
- handlerQueue.push(handler);
4387
- }
4388
- });
4389
- });
4390
- const declarationPairs = [];
4391
- handlerQueue.forEach((handler) => {
4392
- const result = handler(handler.__lookupStyles.reduce((acc, name) => {
4393
- const v = styleMap[name];
4394
- if (v !== void 0) acc[name] = v;
4395
- return acc;
4396
- }, {}));
4397
- if (!result) return;
4398
- (Array.isArray(result) ? result : [result]).forEach((cssMap) => {
4399
- if (!cssMap || typeof cssMap !== "object") return;
4400
- const { $: _$, ...props } = cssMap;
4401
- Object.entries(props).forEach(([prop, val]) => {
4402
- if (val == null || val === "") return;
4403
- if (Array.isArray(val)) val.forEach((v) => {
4404
- if (v != null && v !== "") declarationPairs.push({
4405
- prop,
4406
- value: String(v)
4407
- });
4408
- });
4409
- else declarationPairs.push({
4410
- prop,
4411
- value: String(val)
4412
- });
4413
- });
4414
- });
4415
- });
4416
- const declarations = declarationPairs.map((d) => `${d.prop}: ${d.value}`).join("; ");
4417
- rules.push(`${key} { ${declarations.trim()} }`);
4418
- }
4419
- return rules.join(" ");
4420
- }
4421
- /**
4422
- * Format a @keyframes rule as a CSS string.
4423
- */
4424
- function formatKeyframesCSS(name, steps) {
4425
- return `@keyframes ${name} { ${stepsToCSS(steps)} }`;
4426
- }
4427
- //#endregion
4428
- //#region src/ssr/format-rules.ts
4429
- /**
4430
- * Resolve selectors for a rule, applying className-based specificity doubling
4431
- * and rootPrefix handling. Mirrors the logic in StyleInjector.inject().
4432
- */
4433
- function resolveSelector(rule, className) {
4434
- let selector = rule.selector;
4435
- if (rule.needsClassName) {
4436
- const selectorParts = selector ? selector.split("|||") : [""];
4437
- const classPrefix = `.${className}.${className}`;
4438
- selector = selectorParts.map((part) => {
4439
- const classSelector = part ? `${classPrefix}${part}` : classPrefix;
4440
- if (rule.rootPrefix) return `${rule.rootPrefix} ${classSelector}`;
4441
- return classSelector;
4442
- }).join(", ");
4443
- }
4444
- return selector;
4445
- }
4446
- /**
4447
- * Group rules by selector + at-rules + startingStyle and merge their declarations.
4448
- * Mirrors the grouping logic in SheetManager.insertRule().
4449
- */
4450
- function groupRules(rules) {
4451
- const groupMap = /* @__PURE__ */ new Map();
4452
- const order = [];
4453
- const atKey = (at) => at && at.length ? at.join("|") : "";
4454
- for (const r of rules) {
4455
- const key = `${atKey(r.atRules)}||${r.selector}||${r.startingStyle ? "1" : "0"}`;
4456
- const existing = groupMap.get(key);
4457
- if (existing) existing.declarations = existing.declarations ? `${existing.declarations} ${r.declarations}` : r.declarations;
4458
- else {
4459
- groupMap.set(key, {
4460
- selector: r.selector,
4461
- atRules: r.atRules,
4462
- startingStyle: r.startingStyle,
4463
- declarations: r.declarations
4464
- });
4465
- order.push(key);
4466
- }
4467
- }
4468
- return order.map((key) => groupMap.get(key));
4469
- }
4470
- /**
4471
- * Format an array of StyleResult rules into a CSS text string.
4472
- *
4473
- * Applies className-based specificity doubling (.cls.cls),
4474
- * groups rules by selector + at-rules, and wraps with at-rule blocks.
4475
- *
4476
- * Produces the same CSS text as SheetManager.insertRule() would insert
4477
- * into the DOM, but as a plain string suitable for SSR output.
4478
- */
4479
- function formatRules(rules, className) {
4480
- if (rules.length === 0) return "";
4481
- const grouped = groupRules(rules.map((rule) => ({
4482
- selector: resolveSelector(rule, className),
4483
- declarations: rule.declarations,
4484
- atRules: rule.atRules,
4485
- startingStyle: rule.startingStyle
4486
- })));
4487
- const cssRules = [];
4488
- for (const rule of grouped) {
4489
- const innerContent = rule.startingStyle ? `@starting-style { ${rule.declarations} }` : rule.declarations;
4490
- const baseRule = `${rule.selector} { ${innerContent} }`;
4491
- let fullRule = baseRule;
4492
- if (rule.atRules && rule.atRules.length > 0) fullRule = rule.atRules.reduce((css, atRule) => `${atRule} { ${css} }`, baseRule);
4493
- cssRules.push(fullRule);
4494
- }
4495
- return cssRules.join("\n");
4496
- }
4497
- //#endregion
4498
- //#region src/utils/resolve-recipes.ts
4499
- /**
4500
- * Recipe resolution utility.
4501
- *
4502
- * Resolves `recipe` style properties by looking up predefined recipe styles
4503
- * from global configuration and merging them with the component's own styles.
4504
- *
4505
- * Resolution order per level (top-level and each sub-element independently):
4506
- * base_recipe_1 base_recipe_2 → component styles → post_recipe_1 post_recipe_2
4507
- *
4508
- * The `/` separator splits base recipes (before component styles)
4509
- * from post recipes (after component styles). All merges use mergeStyles
4510
- * semantics: primitives and state maps with '' key fully replace;
4511
- * state maps without '' key extend the existing value.
4512
- *
4513
- * Returns the same object reference if no recipes are present (zero overhead).
4514
- */
4515
- const devMode = isDevEnv();
4516
- /**
4517
- * Parse a recipe string into base and post recipe name groups.
4518
- *
4519
- * Syntax: `'base1 base2 / post1 post2'`
4520
- * - Names are space-separated within each group
4521
- * - `/` separates base (before component) from post (after component) groups
4522
- * - `/` is optional; if absent, all names are base
4523
- * - `none` as the sole base value means "no base recipes"
4524
- *
4525
- * Returns `{ base: null, post: null }` if the string is empty or invalid.
4526
- */
4527
- function parseRecipeNames(value) {
4528
- const empty = {
4529
- base: null,
4530
- post: null
4531
- };
4532
- if (typeof value !== "string") return empty;
4533
- const trimmed = value.trim();
4534
- if (trimmed === "") return empty;
4535
- const slashIndex = trimmed.indexOf("/");
4536
- if (slashIndex === -1) {
4537
- if (trimmed === "none") return empty;
4538
- return {
4539
- base: splitNames(trimmed),
4540
- post: null
4541
- };
4542
- }
4543
- const basePart = trimmed.slice(0, slashIndex);
4544
- const postPart = trimmed.slice(slashIndex + 1);
4545
- return {
4546
- base: basePart.trim() === "none" ? null : splitNames(basePart),
4547
- post: splitNames(postPart)
4548
- };
4549
- }
4550
- function splitNames(s) {
4551
- const names = s.split(/\s+/).filter(Boolean);
4552
- return names.length > 0 ? names : null;
4553
- }
4554
- /**
4555
- * Collect merged styles for a list of recipe names.
4556
- * Each recipe is flat-spread on top of the previous.
4557
- */
4558
- function collectRecipeStyles(names, recipes) {
4559
- let merged = {};
4560
- for (const name of names) {
4561
- const recipeStyles = recipes[name];
4562
- if (!recipeStyles) {
4563
- if (devMode) console.warn(`[Tasty] Recipe "${name}" not found. Make sure it is defined in configure({ recipes: { ... } }).`);
4564
- continue;
4565
- }
4566
- merged = {
4567
- ...merged,
4568
- ...recipeStyles
4569
- };
4570
- }
4571
- return merged;
4572
- }
4573
- /**
4574
- * Resolve recipe references in a flat styles object (no sub-elements).
4575
- * Returns null if no `recipe` key is present.
4576
- *
4577
- * Resolution: base recipes → component styles → post recipes (all via mergeStyles)
4578
- */
4579
- function resolveRecipesForLevel(styles, recipes) {
4580
- if (!("recipe" in styles)) return null;
4581
- const { base, post } = parseRecipeNames(styles.recipe);
4582
- const { recipe: _recipe, ...allRest } = styles;
4583
- const flatStyles = {};
4584
- const selectorStyles = {};
4585
- for (const key of Object.keys(allRest)) if (isSelector(key)) selectorStyles[key] = allRest[key];
4586
- else flatStyles[key] = allRest[key];
4587
- if (!base && !post) return allRest;
4588
- let result;
4589
- if (base) result = mergeStyles(collectRecipeStyles(base, recipes), flatStyles);
4590
- else result = { ...flatStyles };
4591
- if (post) {
4592
- const postStyles = collectRecipeStyles(post, recipes);
4593
- result = mergeStyles(result, postStyles);
4594
- }
4595
- for (const key of Object.keys(selectorStyles)) result[key] = selectorStyles[key];
4596
- return result;
4597
- }
4598
- /**
4599
- * Resolve all `recipe` style properties in a styles object.
4600
- *
4601
- * Handles both top-level and sub-element recipe references.
4602
- * Returns the same object reference if no recipes are present anywhere
4603
- * (zero overhead for the common case).
4604
- *
4605
- * @param styles - The styles object potentially containing `recipe` keys
4606
- * @returns Resolved styles with recipe values merged in, or the original object if unchanged
4607
- */
4608
- function resolveRecipes(styles) {
4609
- const recipes = getGlobalRecipes();
4610
- if (!recipes) return styles;
4611
- let changed = false;
4612
- const topResolved = resolveRecipesForLevel(styles, recipes);
4613
- let result;
4614
- if (topResolved) {
4615
- changed = true;
4616
- result = topResolved;
4617
- } else result = styles;
4618
- const keys = Object.keys(result);
4619
- for (const key of keys) {
4620
- if (!isSelector(key)) continue;
4621
- const subStyles = result[key];
4622
- if (!subStyles || typeof subStyles !== "object" || Array.isArray(subStyles)) continue;
4623
- const subRecord = subStyles;
4624
- if (!("recipe" in subRecord)) continue;
4625
- const subResolved = resolveRecipesForLevel(subRecord, recipes);
4626
- if (subResolved) {
4627
- if (!changed) {
4628
- changed = true;
4629
- result = { ...styles };
4630
- }
4631
- result[key] = subResolved;
4632
- }
4633
- }
4634
- return changed ? result : styles;
4635
- }
4636
- //#endregion
4637
- //#region src/utils/cache-wrapper.ts
4638
- /**
4639
- * Create a function that caches the result with LRU eviction.
4640
- */
4641
- function cacheWrapper(handler, limit = 1e3) {
4642
- const cache = new Lru(limit);
4643
- return (firstArg, secondArg) => {
4644
- const key = typeof firstArg === "string" && secondArg == null ? firstArg : JSON.stringify([firstArg, secondArg]);
4645
- let result = cache.get(key);
4646
- if (result === void 0) {
4647
- result = secondArg == null ? handler(firstArg) : handler(firstArg, secondArg);
4648
- cache.set(key, result);
4649
- }
4650
- return result;
4651
- };
4652
- }
4653
- //#endregion
4654
- //#region src/ssr/format-global-rules.ts
4655
- /**
4656
- * Format an array of global StyleResult rules into a CSS text string.
4657
- *
4658
- * Rules already have their full selectors applied by renderStyles().
4659
- * Handles rootPrefix prepending and at-rule wrapping.
4660
- */
4661
- function formatGlobalRules(rules) {
4662
- if (rules.length === 0) return "";
4663
- const cssRules = [];
4664
- for (const rule of rules) {
4665
- const baseRule = `${rule.rootPrefix ? `${rule.rootPrefix} ${rule.selector}` : rule.selector} { ${rule.declarations} }`;
4666
- let fullRule = baseRule;
4667
- if (rule.atRules && rule.atRules.length > 0) fullRule = rule.atRules.reduce((css, atRule) => `${atRule} { ${css} }`, baseRule);
4668
- cssRules.push(fullRule);
4669
- }
4670
- return cssRules.join("\n");
4671
- }
4672
- //#endregion
4673
- //#region src/utils/client-state.ts
4674
- /**
4675
- * Build a per-(injector, root) client state cache for the standalone style
4676
- * functions (`useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useCounterStyle`).
4677
- *
4678
- * Two levels, both weak:
4679
- *
4680
- * - **injector** — `configure()` replaces the global injector, and every dispose
4681
- * handle and generated name we cache belongs to the one that produced it.
4682
- * Keying by injector makes stale state fall away with it, instead of letting
4683
- * change-detection keys suppress re-injection into the new sheets.
4684
- * - **root** — the same selector or slot name can be used in several shadow
4685
- * roots, and each holds its own injection.
4686
- */
4687
- function createClientState(create) {
4688
- const byInjector = /* @__PURE__ */ new WeakMap();
4689
- return (root) => {
4690
- const injector = getGlobalInjector();
4691
- let byRoot = byInjector.get(injector);
4692
- if (!byRoot) {
4693
- byRoot = /* @__PURE__ */ new WeakMap();
4694
- byInjector.set(injector, byRoot);
4695
- }
4696
- let state = byRoot.get(root);
4697
- if (!state) {
4698
- state = create();
4699
- byRoot.set(root, state);
4700
- }
4701
- return state;
4702
- };
4703
- }
4704
- //#endregion
4705
- //#region src/utils/deps-equal.ts
4706
- /**
4707
- * Shallow comparison of two dependency arrays using Object.is semantics.
4708
- * Returns true when both arrays have the same length and every element
4709
- * at the same index is identical.
4710
- */
4711
- function depsEqual(a, b) {
4712
- if (a.length !== b.length) return false;
4713
- for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
4714
- return true;
4715
- }
4716
- //#endregion
4717
- export { getNamePrefix as $, holdKeyframes as A, extractLocalProperties as At, ChunkSheetRegistry as B, destroy as C, DEFAULT_ZERO_NAME_PREFIX as Ct, getCSSText as D, tastyClassRegex as Dt, gc as E, makeKeyframeName as Et, isPropertyDefined as F, formatFunctionRule as Ft, getGlobalConfigTokens as G, configure as H, keyframes as I, hasLocalFunctions as It, getGlobalFunctions as J, getGlobalCounterStyles as K, ownKeyframes as L, parseFunctionName as Lt, injectGlobal as M, parsePropertyToken as Mt, injectRawCSS as N, propHandlerRegistry as Nt, getCSSTextForNode as O, validateNamePrefix as Ot, injector as P, extractLocalFunctions as Pt, getGlobalStyles as Q, property as R, registerFunctionPolyfill as Rt, createInjector as S, DEFAULT_NAME_PREFIX as St, func as T, makeCounterStyleName as Tt, getConfig as U, chunkSheetRegistry as V, getEffectiveProperties as W, getGlobalKeyframes as X, getGlobalInjector as Y, getGlobalRecipes as Z, referencesAnimation as _, closeBatchWindow as _t, resolveRecipes as a, resetConfig as at, cleanup as b, openBatchWindow as bt, formatPropertyCSS as c, extractLocalCounterStyle as ct, registerSSRCollectorGetterGlobal as d, extractLocalFontFace as dt, hasGlobalKeyframes as et, extractAnimationNamesFromStyles as f, fontFaceContentHash as ft, mergeKeyframes as g, PropertyTypeResolver as gt, hasLocalKeyframes as h, SheetManager as ht, cacheWrapper as i, isTestEnvironment as it, inject as j, hasLocalProperties as jt, getRawCSSText as k, hashString as kt, getRegisteredSSRCollector as l, formatCounterStyleRule as lt, filterUsedKeyframes as m, hasLocalFontFace as mt, createClientState as n, isConfigLocked as nt, formatRules as o, generateTypographyTokens as ot, extractLocalKeyframes as p, formatFontFaceRule as pt, getGlobalFontFaces as q, formatGlobalRules as r, isFunctionsPolyfillEnabled as rt, formatKeyframesCSS as s, StyleInjector as st, depsEqual as t, hasGlobalRecipes as tt, registerSSRCollectorGetter as u, hasLocalCounterStyle as ut, replaceAnimationNames as v, flushStyles as vt, fontFace as w, makeClassName as wt, counterStyle as x, resetStyleBatch as xt, resolveKeyframesNames as y, hasPendingStyleWrites as yt, touch as z, registerLocalFunctionPolyfills as zt };
4718
-
4719
- //# sourceMappingURL=runtime-engine-Y00VWjrS.js.map