@tenphi/tasty 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/compute-styles.d.ts +31 -0
  2. package/dist/compute-styles.js +357 -0
  3. package/dist/compute-styles.js.map +1 -0
  4. package/dist/config.d.ts +19 -19
  5. package/dist/config.js +25 -26
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/index.d.ts +5 -4
  8. package/dist/core/index.js +6 -5
  9. package/dist/hooks/useCounterStyle.js +3 -4
  10. package/dist/hooks/useCounterStyle.js.map +1 -1
  11. package/dist/hooks/useFontFace.js +3 -4
  12. package/dist/hooks/useFontFace.js.map +1 -1
  13. package/dist/hooks/useGlobalStyles.js +4 -5
  14. package/dist/hooks/useGlobalStyles.js.map +1 -1
  15. package/dist/hooks/useKeyframes.js +3 -4
  16. package/dist/hooks/useKeyframes.js.map +1 -1
  17. package/dist/hooks/useProperty.js +3 -4
  18. package/dist/hooks/useProperty.js.map +1 -1
  19. package/dist/hooks/useRawCSS.js +3 -4
  20. package/dist/hooks/useRawCSS.js.map +1 -1
  21. package/dist/hooks/useStyles.d.ts +4 -9
  22. package/dist/hooks/useStyles.js +6 -214
  23. package/dist/hooks/useStyles.js.map +1 -1
  24. package/dist/index.d.ts +6 -5
  25. package/dist/index.js +7 -6
  26. package/dist/injector/index.d.ts +23 -19
  27. package/dist/injector/index.js +29 -16
  28. package/dist/injector/index.js.map +1 -1
  29. package/dist/injector/injector.d.ts +32 -3
  30. package/dist/injector/injector.js +130 -7
  31. package/dist/injector/injector.js.map +1 -1
  32. package/dist/injector/sheet-manager.d.ts +9 -13
  33. package/dist/injector/sheet-manager.js +30 -66
  34. package/dist/injector/sheet-manager.js.map +1 -1
  35. package/dist/injector/types.d.ts +50 -19
  36. package/dist/ssr/collector.js +3 -10
  37. package/dist/ssr/collector.js.map +1 -1
  38. package/dist/ssr/index.d.ts +1 -2
  39. package/dist/ssr/index.js +1 -2
  40. package/dist/ssr/index.js.map +1 -1
  41. package/dist/ssr/next.d.ts +1 -3
  42. package/dist/ssr/next.js +8 -3
  43. package/dist/ssr/next.js.map +1 -1
  44. package/dist/tasty.d.ts +28 -13
  45. package/dist/tasty.js +72 -60
  46. package/dist/tasty.js.map +1 -1
  47. package/dist/utils/process-tokens.d.ts +1 -5
  48. package/dist/utils/process-tokens.js +1 -8
  49. package/dist/utils/process-tokens.js.map +1 -1
  50. package/docs/injector.md +33 -18
  51. package/docs/methodology.md +50 -1
  52. package/docs/runtime.md +90 -3
  53. package/docs/ssr.md +19 -49
  54. package/package.json +4 -4
  55. package/dist/hooks/resolve-ssr-collector.js +0 -14
  56. package/dist/hooks/resolve-ssr-collector.js.map +0 -1
  57. package/dist/ssr/context.d.ts +0 -8
  58. package/dist/ssr/context.js +0 -13
  59. package/dist/ssr/context.js.map +0 -1
@@ -12,10 +12,6 @@ import { Tokens } from "../types.js";
12
12
  * @returns CSSProperties object or undefined if no tokens to process
13
13
  */
14
14
  declare function processTokens(tokens: Tokens | undefined): CSSProperties | undefined;
15
- /**
16
- * Stringify tokens for memoization key.
17
- */
18
- declare function stringifyTokens(tokens: Tokens | undefined): string;
19
15
  //#endregion
20
- export { processTokens, stringifyTokens };
16
+ export { processTokens };
21
17
  //# sourceMappingURL=process-tokens.d.ts.map
@@ -77,14 +77,7 @@ function processTokens(tokens) {
77
77
  }
78
78
  return result;
79
79
  }
80
- /**
81
- * Stringify tokens for memoization key.
82
- */
83
- function stringifyTokens(tokens) {
84
- if (!tokens) return "";
85
- return JSON.stringify(tokens);
86
- }
87
80
  //#endregion
88
- export { processTokens, stringifyTokens };
81
+ export { processTokens };
89
82
 
90
83
  //# sourceMappingURL=process-tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-tokens.js","names":[],"sources":["../../src/utils/process-tokens.ts"],"sourcesContent":["import type { Tokens, TokenValue } from '../types';\n\nimport type { CSSProperties } from './css-types';\n\nimport { getColorSpaceComponents, getColorSpaceSuffix } from './color-space';\nimport { normalizeColorTokenValue, parseStyle } from './styles';\n\nexport { hslToRgbValues } from './color-math';\n\nconst devMode = process.env.NODE_ENV !== 'production';\n\n/**\n * Extract color components in the configured color space.\n * Returns a CSS variable reference for token colors, or decomposed\n * components as a space-separated string.\n */\nfunction extractColorSpaceValue(\n colorValue: string,\n parsedOutput: string,\n): string {\n const suffix = getColorSpaceSuffix();\n\n // If the parsed output references a color variable, use the companion variant\n const varMatch = parsedOutput.match(/var\\(--([a-z0-9-]+)-color\\)/);\n if (varMatch) {\n return `var(--${varMatch[1]}-color-${suffix})`;\n }\n\n // Try the original color value first, then parsed output\n const components = getColorSpaceComponents(colorValue);\n if (components !== colorValue) return components;\n\n const componentsFromParsed = getColorSpaceComponents(parsedOutput);\n if (componentsFromParsed !== parsedOutput) return componentsFromParsed;\n\n // Fallback: return the parsed output\n return parsedOutput;\n}\n\n/**\n * Check if a value is a valid token value (string, number, or boolean - not object).\n * Returns false for `false` values (they mean \"skip this token\").\n */\nfunction isValidTokenValue(\n value: unknown,\n): value is Exclude<TokenValue, undefined | null | false> {\n if (value === undefined || value === null || value === false) {\n return false;\n }\n\n if (typeof value === 'object') {\n if (devMode) {\n console.warn(\n 'Tasty: Object values are not allowed in tokens prop. ' +\n 'Tokens do not support state-based styling. Use a primitive value instead.',\n );\n }\n return false;\n }\n\n return (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/**\n * Process a single token value through the tasty parser.\n * Numbers are converted to strings; 0 stays as \"0\".\n */\nfunction processTokenValue(value: string | number): string {\n if (typeof value === 'number') {\n // 0 should remain as \"0\", not converted to any unit\n if (value === 0) {\n return '0';\n }\n return parseStyle(String(value)).output;\n }\n return parseStyle(value).output;\n}\n\n/**\n * Process tokens object into inline style properties.\n * - $name -> --name with parsed value\n * - #name -> --name-color AND --name-color-{colorSpace} with parsed values\n *\n * @param tokens - The tokens object to process\n * @returns CSSProperties object or undefined if no tokens to process\n */\nexport function processTokens(\n tokens: Tokens | undefined,\n): CSSProperties | undefined {\n if (!tokens) {\n return undefined;\n }\n\n const keys = Object.keys(tokens);\n if (keys.length === 0) {\n return undefined;\n }\n\n let result: Record<string, string> | undefined;\n\n for (const key of keys) {\n const value = tokens[key as keyof Tokens];\n\n // Skip undefined/null values\n if (!isValidTokenValue(value)) {\n continue;\n }\n\n if (key.startsWith('$')) {\n // Custom property token: $name -> --name\n const propName = `--${key.slice(1)}`;\n // Boolean true for custom properties converts to empty string (valid CSS value)\n const effectiveValue = value === true ? '' : value;\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[propName] = processedValue;\n } else if (key.startsWith('#')) {\n const colorName = key.slice(1);\n const suffix = getColorSpaceSuffix();\n\n // Normalize color token value (true → 'transparent', false is already filtered by isValidTokenValue)\n const effectiveValue = normalizeColorTokenValue(value);\n // Skip if normalized to null (shouldn't happen since false is filtered by isValidTokenValue)\n if (effectiveValue === null) continue;\n\n const originalValue =\n typeof effectiveValue === 'number'\n ? String(effectiveValue)\n : effectiveValue;\n const lowerValue = originalValue.toLowerCase();\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[`--${colorName}-color`] = processedValue;\n\n // Skip component generation for #current values (currentcolor is dynamic, cannot decompose)\n if (/^#current(?:\\.|$)/i.test(lowerValue)) {\n continue;\n }\n\n result[`--${colorName}-color-${suffix}`] = extractColorSpaceValue(\n originalValue,\n processedValue,\n );\n }\n }\n\n return result as CSSProperties | undefined;\n}\n\n/**\n * Stringify tokens for memoization key.\n */\nexport function stringifyTokens(tokens: Tokens | undefined): string {\n if (!tokens) return '';\n return JSON.stringify(tokens);\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAS,uBACP,YACA,cACQ;CACR,MAAM,SAAS,qBAAqB;CAGpC,MAAM,WAAW,aAAa,MAAM,8BAA8B;AAClE,KAAI,SACF,QAAO,SAAS,SAAS,GAAG,SAAS,OAAO;CAI9C,MAAM,aAAa,wBAAwB,WAAW;AACtD,KAAI,eAAe,WAAY,QAAO;CAEtC,MAAM,uBAAuB,wBAAwB,aAAa;AAClE,KAAI,yBAAyB,aAAc,QAAO;AAGlD,QAAO;;;;;;AAOT,SAAS,kBACP,OACwD;AACxD,KAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,MACrD,QAAO;AAGT,KAAI,OAAO,UAAU,UAAU;AAE3B,UAAQ,KACN,iIAED;AAEH,SAAO;;AAGT,QACE,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU;;;;;;AAQrB,SAAS,kBAAkB,OAAgC;AACzD,KAAI,OAAO,UAAU,UAAU;AAE7B,MAAI,UAAU,EACZ,QAAO;AAET,SAAO,WAAW,OAAO,MAAM,CAAC,CAAC;;AAEnC,QAAO,WAAW,MAAM,CAAC;;;;;;;;;;AAW3B,SAAgB,cACd,QAC2B;AAC3B,KAAI,CAAC,OACH;CAGF,MAAM,OAAO,OAAO,KAAK,OAAO;AAChC,KAAI,KAAK,WAAW,EAClB;CAGF,IAAI;AAEJ,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO;AAGrB,MAAI,CAAC,kBAAkB,MAAM,CAC3B;AAGF,MAAI,IAAI,WAAW,IAAI,EAAE;GAEvB,MAAM,WAAW,KAAK,IAAI,MAAM,EAAE;GAGlC,MAAM,iBAAiB,kBADA,UAAU,OAAO,KAAK,MACW;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,YAAY;aACV,IAAI,WAAW,IAAI,EAAE;GAC9B,MAAM,YAAY,IAAI,MAAM,EAAE;GAC9B,MAAM,SAAS,qBAAqB;GAGpC,MAAM,iBAAiB,yBAAyB,MAAM;AAEtD,OAAI,mBAAmB,KAAM;GAE7B,MAAM,gBACJ,OAAO,mBAAmB,WACtB,OAAO,eAAe,GACtB;GACN,MAAM,aAAa,cAAc,aAAa;GAC9C,MAAM,iBAAiB,kBAAkB,eAAe;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,KAAK,UAAU,WAAW;AAGjC,OAAI,qBAAqB,KAAK,WAAW,CACvC;AAGF,UAAO,KAAK,UAAU,SAAS,YAAY,uBACzC,eACA,eACD;;;AAIL,QAAO;;;;;AAMT,SAAgB,gBAAgB,QAAoC;AAClE,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,KAAK,UAAU,OAAO"}
1
+ {"version":3,"file":"process-tokens.js","names":[],"sources":["../../src/utils/process-tokens.ts"],"sourcesContent":["import type { Tokens, TokenValue } from '../types';\n\nimport type { CSSProperties } from './css-types';\n\nimport { getColorSpaceComponents, getColorSpaceSuffix } from './color-space';\nimport { normalizeColorTokenValue, parseStyle } from './styles';\n\nexport { hslToRgbValues } from './color-math';\n\nconst devMode = process.env.NODE_ENV !== 'production';\n\n/**\n * Extract color components in the configured color space.\n * Returns a CSS variable reference for token colors, or decomposed\n * components as a space-separated string.\n */\nfunction extractColorSpaceValue(\n colorValue: string,\n parsedOutput: string,\n): string {\n const suffix = getColorSpaceSuffix();\n\n // If the parsed output references a color variable, use the companion variant\n const varMatch = parsedOutput.match(/var\\(--([a-z0-9-]+)-color\\)/);\n if (varMatch) {\n return `var(--${varMatch[1]}-color-${suffix})`;\n }\n\n // Try the original color value first, then parsed output\n const components = getColorSpaceComponents(colorValue);\n if (components !== colorValue) return components;\n\n const componentsFromParsed = getColorSpaceComponents(parsedOutput);\n if (componentsFromParsed !== parsedOutput) return componentsFromParsed;\n\n // Fallback: return the parsed output\n return parsedOutput;\n}\n\n/**\n * Check if a value is a valid token value (string, number, or boolean - not object).\n * Returns false for `false` values (they mean \"skip this token\").\n */\nfunction isValidTokenValue(\n value: unknown,\n): value is Exclude<TokenValue, undefined | null | false> {\n if (value === undefined || value === null || value === false) {\n return false;\n }\n\n if (typeof value === 'object') {\n if (devMode) {\n console.warn(\n 'Tasty: Object values are not allowed in tokens prop. ' +\n 'Tokens do not support state-based styling. Use a primitive value instead.',\n );\n }\n return false;\n }\n\n return (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/**\n * Process a single token value through the tasty parser.\n * Numbers are converted to strings; 0 stays as \"0\".\n */\nfunction processTokenValue(value: string | number): string {\n if (typeof value === 'number') {\n // 0 should remain as \"0\", not converted to any unit\n if (value === 0) {\n return '0';\n }\n return parseStyle(String(value)).output;\n }\n return parseStyle(value).output;\n}\n\n/**\n * Process tokens object into inline style properties.\n * - $name -> --name with parsed value\n * - #name -> --name-color AND --name-color-{colorSpace} with parsed values\n *\n * @param tokens - The tokens object to process\n * @returns CSSProperties object or undefined if no tokens to process\n */\nexport function processTokens(\n tokens: Tokens | undefined,\n): CSSProperties | undefined {\n if (!tokens) {\n return undefined;\n }\n\n const keys = Object.keys(tokens);\n if (keys.length === 0) {\n return undefined;\n }\n\n let result: Record<string, string> | undefined;\n\n for (const key of keys) {\n const value = tokens[key as keyof Tokens];\n\n // Skip undefined/null values\n if (!isValidTokenValue(value)) {\n continue;\n }\n\n if (key.startsWith('$')) {\n // Custom property token: $name -> --name\n const propName = `--${key.slice(1)}`;\n // Boolean true for custom properties converts to empty string (valid CSS value)\n const effectiveValue = value === true ? '' : value;\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[propName] = processedValue;\n } else if (key.startsWith('#')) {\n const colorName = key.slice(1);\n const suffix = getColorSpaceSuffix();\n\n // Normalize color token value (true → 'transparent', false is already filtered by isValidTokenValue)\n const effectiveValue = normalizeColorTokenValue(value);\n // Skip if normalized to null (shouldn't happen since false is filtered by isValidTokenValue)\n if (effectiveValue === null) continue;\n\n const originalValue =\n typeof effectiveValue === 'number'\n ? String(effectiveValue)\n : effectiveValue;\n const lowerValue = originalValue.toLowerCase();\n const processedValue = processTokenValue(effectiveValue);\n\n if (!result) result = {};\n result[`--${colorName}-color`] = processedValue;\n\n // Skip component generation for #current values (currentcolor is dynamic, cannot decompose)\n if (/^#current(?:\\.|$)/i.test(lowerValue)) {\n continue;\n }\n\n result[`--${colorName}-color-${suffix}`] = extractColorSpaceValue(\n originalValue,\n processedValue,\n );\n }\n }\n\n return result as CSSProperties | undefined;\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAS,uBACP,YACA,cACQ;CACR,MAAM,SAAS,qBAAqB;CAGpC,MAAM,WAAW,aAAa,MAAM,8BAA8B;AAClE,KAAI,SACF,QAAO,SAAS,SAAS,GAAG,SAAS,OAAO;CAI9C,MAAM,aAAa,wBAAwB,WAAW;AACtD,KAAI,eAAe,WAAY,QAAO;CAEtC,MAAM,uBAAuB,wBAAwB,aAAa;AAClE,KAAI,yBAAyB,aAAc,QAAO;AAGlD,QAAO;;;;;;AAOT,SAAS,kBACP,OACwD;AACxD,KAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,MACrD,QAAO;AAGT,KAAI,OAAO,UAAU,UAAU;AAE3B,UAAQ,KACN,iIAED;AAEH,SAAO;;AAGT,QACE,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU;;;;;;AAQrB,SAAS,kBAAkB,OAAgC;AACzD,KAAI,OAAO,UAAU,UAAU;AAE7B,MAAI,UAAU,EACZ,QAAO;AAET,SAAO,WAAW,OAAO,MAAM,CAAC,CAAC;;AAEnC,QAAO,WAAW,MAAM,CAAC;;;;;;;;;;AAW3B,SAAgB,cACd,QAC2B;AAC3B,KAAI,CAAC,OACH;CAGF,MAAM,OAAO,OAAO,KAAK,OAAO;AAChC,KAAI,KAAK,WAAW,EAClB;CAGF,IAAI;AAEJ,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO;AAGrB,MAAI,CAAC,kBAAkB,MAAM,CAC3B;AAGF,MAAI,IAAI,WAAW,IAAI,EAAE;GAEvB,MAAM,WAAW,KAAK,IAAI,MAAM,EAAE;GAGlC,MAAM,iBAAiB,kBADA,UAAU,OAAO,KAAK,MACW;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,YAAY;aACV,IAAI,WAAW,IAAI,EAAE;GAC9B,MAAM,YAAY,IAAI,MAAM,EAAE;GAC9B,MAAM,SAAS,qBAAqB;GAGpC,MAAM,iBAAiB,yBAAyB,MAAM;AAEtD,OAAI,mBAAmB,KAAM;GAE7B,MAAM,gBACJ,OAAO,mBAAmB,WACtB,OAAO,eAAe,GACtB;GACN,MAAM,aAAa,cAAc,aAAa;GAC9C,MAAM,iBAAiB,kBAAkB,eAAe;AAExD,OAAI,CAAC,OAAQ,UAAS,EAAE;AACxB,UAAO,KAAK,UAAU,WAAW;AAGjC,OAAI,qBAAqB,KAAK,WAAW,CACvC;AAGF,UAAO,KAAK,UAAU,SAAS,YAAY,uBACzC,eACA,eACD;;;AAIL,QAAO"}
package/docs/injector.md CHANGED
@@ -207,13 +207,15 @@ import { configure } from '@tenphi/tasty';
207
207
  configure({
208
208
  devMode: true, // Enable development features (auto-detected)
209
209
  maxRulesPerSheet: 8192, // Cap rules per stylesheet (default: 8192)
210
- unusedStylesThreshold: 500, // Trigger cleanup threshold (CSS rules only)
211
- bulkCleanupDelay: 5000, // Cleanup delay (ms) - ignored if idleCleanup is true
212
- idleCleanup: true, // Use requestIdleCallback for cleanup
213
- bulkCleanupBatchRatio: 0.5, // Clean up oldest 50% per batch
214
- unusedStylesMinAgeMs: 10000, // Minimum age before cleanup (ms)
215
210
  forceTextInjection: false, // Force textContent insertion (auto-detected for tests)
216
211
  nonce: 'csp-nonce', // CSP nonce for security
212
+ gc: { // Garbage collection for unused styles
213
+ auto: true, // Enable automatic background sweep
214
+ baseMaxAge: 60000, // Base TTL (ms) for single-use styles
215
+ cooldown: 30000, // Minimum time between GC runs
216
+ autoInterval: 300000, // Background sweep interval (ms)
217
+ cacheCapacity: 5000, // Hard cap on cached styles (optional)
218
+ },
217
219
  states: { // Global predefined states for advanced state mapping
218
220
  '@mobile': '@media(w < 768px)',
219
221
  '@dark': '@root(schema=dark)',
@@ -229,7 +231,9 @@ configure({
229
231
  - Most options have sensible defaults and auto-detection
230
232
  - `configure()` is optional - the injector works with defaults
231
233
  - **Configuration is locked after styles are generated** - calling `configure()` after first render will emit a warning and be ignored
232
- - `unusedStylesMinAgeMs`: Minimum time (ms) a style must remain unused before being eligible for cleanup. Helps prevent removal of styles that might be quickly reactivated.
234
+ - `gc.baseMaxAge`: Base TTL for a style rendered only once. Popular styles get longer TTLs via logarithmic scaling (`baseMaxAge * log2(hitCount + 1)`).
235
+ - `gc.auto`: When true, runs a periodic background sweep at `gc.autoInterval` intervals using `requestIdleCallback`.
236
+ - `gc.cooldown`: Minimum time between GC runs to avoid thrashing.
233
237
 
234
238
  ---
235
239
 
@@ -291,21 +295,31 @@ comp3.dispose(); // refCount: 1 → 0, eligible for bulk cleanup
291
295
  // Next inject() with same styles will increment refCount and reuse immediately
292
296
  ```
293
297
 
294
- ### Smart Cleanup System
298
+ ### Garbage Collection
295
299
 
296
300
  ```typescript
297
- import { configure } from '@tenphi/tasty';
301
+ import { configure, gc, maybeGC } from '@tenphi/tasty';
298
302
 
299
- // CSS rules: Not immediately deleted, marked for bulk cleanup (refCount = 0)
300
303
  // Keyframes: Disposed immediately when refCount = 0 (safer for global scope)
304
+ // CSS rules: Tracked by popularity and cleaned up via gc()
301
305
 
302
306
  configure({
303
- unusedStylesThreshold: 100, // Cleanup when 100+ unused CSS rules
304
- bulkCleanupBatchRatio: 0.3, // Remove oldest 30% each time
307
+ gc: {
308
+ auto: true, // Enable background sweep
309
+ baseMaxAge: 60000, // 1-minute base TTL
310
+ cooldown: 30000, // 30s between runs
311
+ },
305
312
  });
306
313
 
314
+ // Manual GC (synchronous, returns number of swept styles):
315
+ gc();
316
+
317
+ // Event-driven GC with cooldown (e.g. on route change):
318
+ maybeGC();
319
+
307
320
  // Benefits:
308
- // - CSS rules: Batch cleanup prevents DOM manipulation overhead
321
+ // - Popularity-aware: frequently used styles survive longer
322
+ // - DOM-safe: styles currently in the DOM are never evicted
309
323
  // - Keyframes: Immediate cleanup prevents global namespace pollution
310
324
  // - Unused styles can be instantly reactivated (just increment refCount)
311
325
  ```
@@ -462,13 +476,14 @@ injectGlobal([
462
476
  { selector: '.container', declarations: 'max-width: 1200px;' }
463
477
  ]);
464
478
 
465
- // ✅ Configure appropriate thresholds for your app (BEFORE first render!)
479
+ // ✅ Configure GC for your app (BEFORE first render!)
466
480
  import { configure } from '@tenphi/tasty';
467
481
 
468
482
  configure({
469
- unusedStylesThreshold: 500, // Default threshold (adjust based on app size)
470
- bulkCleanupBatchRatio: 0.5, // Default: clean oldest 50% per batch
471
- unusedStylesMinAgeMs: 10000, // Wait 10s before cleanup eligibility
483
+ gc: {
484
+ auto: true, // Enable background sweep for long-lived pages
485
+ baseMaxAge: 60000, // Default base TTL (adjust based on app size)
486
+ },
472
487
  });
473
488
  ```
474
489
 
@@ -480,8 +495,8 @@ configure({
480
495
  // 1. Hash-based deduplication - same CSS = same className
481
496
  // 2. Reference counting - styles stay alive while in use (refCount > 0)
482
497
  // 3. Immediate keyframes cleanup - disposed instantly when refCount = 0
483
- // 4. Batch CSS cleanup - unused CSS rules (refCount = 0) cleaned in batches
484
- // 5. Non-stacking cleanups - prevents timeout accumulation
498
+ // 4. Popularity-aware GC - unused CSS rules are scored by hitCount and age
499
+ // 5. DOM safety guard - styles visible in the DOM are never evicted
485
500
 
486
501
  // Manual cleanup is rarely needed but available:
487
502
  cleanup(); // Force immediate cleanup of all unused CSS rules (refCount = 0)
@@ -256,6 +256,54 @@ The `tokens` prop sets `style="--progress: 75%"` on the DOM element. The `$progr
256
256
 
257
257
  Design tokens (via `configure({ tokens })`) are injected as CSS custom properties on `:root`. Replace tokens (via `configure({ replaceTokens })`) are resolved at parse time and baked into the generated CSS. The `tokens` prop on components is resolved at render time via inline CSS custom properties. Use design tokens for design-system constants, replace tokens for value aliases, and the `tokens` prop for truly dynamic per-instance values.
258
258
 
259
+ ### tokenProps
260
+
261
+ `tokenProps` expose token keys as top-level component props — the token equivalent of `styleProps` and `modProps`. Use them when a component has a fixed set of known dynamic token values.
262
+
263
+ #### Array form
264
+
265
+ Prop names are plain camelCase identifiers. Names ending in `Color` map to `#` color tokens; everything else maps to `$` custom property tokens:
266
+
267
+ ```tsx
268
+ const ProgressBar = tasty({
269
+ tokenProps: ['progress', 'accentColor'] as const,
270
+ styles: { width: '$progress', fill: '#accent' },
271
+ });
272
+
273
+ // Clean prop API — no tokens object needed
274
+ <ProgressBar progress="75%" accentColor="#purple" />
275
+
276
+ // Conversion:
277
+ // 'progress' → $progress → --progress
278
+ // 'accentColor' → #accent → --accent-color + --accent-color-oklch
279
+ ```
280
+
281
+ #### Object form
282
+
283
+ Keys are prop names; values are `$`/`#`-prefixed token keys. No suffix convention needed — the prefix in the value is explicit:
284
+
285
+ ```tsx
286
+ const Card = tasty({
287
+ tokenProps: {
288
+ size: '$card-size',
289
+ color: '#card-accent',
290
+ },
291
+ styles: { padding: '$card-size', fill: '#card-accent' },
292
+ });
293
+
294
+ <Card size="4x" color="#purple" />
295
+ ```
296
+
297
+ #### Merge order
298
+
299
+ When all three token sources are present, values merge with increasing priority:
300
+
301
+ 1. `tokens` option in `tasty({...})` — default tokens (lowest)
302
+ 2. `tokens` prop on the component instance — runtime overrides
303
+ 3. `tokenProps`-derived values — highest priority (explicit named props win)
304
+
305
+ The `tokens` prop remains available for ad-hoc or dynamic tokens alongside `tokenProps`.
306
+
259
307
  ---
260
308
 
261
309
  ## styles prop vs style prop
@@ -458,7 +506,8 @@ See [Configuration](configuration.md) for the full `configure()` API.
458
506
  - **Use `elements` prop** to declare typed sub-components for compound components
459
507
  - **Use `styleProps`** to define what product engineers can customize
460
508
  - **Use `modProps`** to expose known modifier states as clean component props
461
- - **Use `tokens` prop** for per-instance dynamic values (progress, user color)
509
+ - **Use `tokenProps`** to expose known token keys as clean component props
510
+ - **Use `tokens` prop** for ad-hoc or dynamic per-instance token values (progress, user color)
462
511
  - **Use modifiers** (`mods` or `modProps`) for state-driven style changes instead of runtime `styles` prop changes
463
512
 
464
513
  ### Avoid
package/docs/runtime.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime API
2
2
 
3
- The React-specific `tasty()` component factory, component props, and hooks. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
3
+ The React-specific `tasty()` component factory, component props, and hooks. `tasty()` components are hook-free and compatible with React Server Components — no `'use client'` directive needed. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
4
4
 
5
5
  ---
6
6
 
@@ -154,6 +154,75 @@ For architecture guidance on when to use modifiers vs `styleProps`, see [Methodo
154
154
 
155
155
  ---
156
156
 
157
+ ## Token Props
158
+
159
+ Use `tokenProps` to expose token keys as direct component props instead of requiring the `tokens` object:
160
+
161
+ ```jsx
162
+ // Before: tokens object
163
+ <ProgressBar tokens={{ $progress: '75%', '#accent': '#purple' }} />
164
+
165
+ // After: token props
166
+ <ProgressBar progress="75%" accentColor="#purple" />
167
+ ```
168
+
169
+ ### Array form
170
+
171
+ List prop names. Names ending in `Color` map to `#` color tokens; everything else maps to `$` custom property tokens:
172
+
173
+ ```jsx
174
+ const ProgressBar = tasty({
175
+ tokenProps: ['progress', 'accentColor'] as const,
176
+ styles: { width: '$progress', fill: '#accent' },
177
+ });
178
+
179
+ <ProgressBar progress="75%" accentColor="#purple" />
180
+ // 'progress' → $progress → --progress
181
+ // 'accentColor' → #accent → --accent-color + --accent-color-oklch
182
+ ```
183
+
184
+ ### Object form
185
+
186
+ Map prop names to explicit `$`/`#`-prefixed token keys:
187
+
188
+ ```tsx
189
+ const Card = tasty({
190
+ tokenProps: {
191
+ size: '$card-size',
192
+ color: '#card-accent',
193
+ },
194
+ styles: { padding: '$card-size', fill: '#card-accent' },
195
+ });
196
+
197
+ <Card size="4x" color="#purple" />
198
+ ```
199
+
200
+ ### Merge with `tokens`
201
+
202
+ Token props and the `tokens` prop can be used together. Token props take precedence over `tokens`, which takes precedence over default `tokens` in `tasty({...})`:
203
+
204
+ ```jsx
205
+ const Bar = tasty({
206
+ tokenProps: ['progress'] as const,
207
+ tokens: { $progress: '0%' }, // default
208
+ });
209
+
210
+ <Bar tokens={{ $progress: '50%' }} progress="90%" />
211
+ // progress="90%" wins (from token prop)
212
+ ```
213
+
214
+ ### When to use `tokenProps` vs `tokens`
215
+
216
+ | Use case | Recommendation |
217
+ |---|---|
218
+ | Component has a fixed set of known token keys | `tokenProps` — cleaner API, better TypeScript autocomplete |
219
+ | Component needs arbitrary/dynamic token values | `tokens` — open-ended `Record<string, TokenValue>` |
220
+ | Both fixed and dynamic | Combine: `tokenProps` for known keys, `tokens` for ad-hoc |
221
+
222
+ For architecture guidance, see [Methodology — tokenProps](methodology.md#tokenprops).
223
+
224
+ ---
225
+
157
226
  ## Variants
158
227
 
159
228
  Define named style variations. Only CSS for variants actually used at runtime is injected:
@@ -264,11 +333,29 @@ For the mental model behind sub-elements — how they share root state context a
264
333
 
265
334
  ---
266
335
 
336
+ ## computeStyles
337
+
338
+ Hook-free, synchronous style computation. Can be used anywhere — including React Server Components, plain functions, and non-React code:
339
+
340
+ ```tsx
341
+ import { computeStyles } from '@tenphi/tasty';
342
+
343
+ const { className } = computeStyles({
344
+ padding: '2x',
345
+ fill: '#surface',
346
+ radius: '1r',
347
+ });
348
+ ```
349
+
350
+ On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS is collected via the SSR collector if one is available. This is the same function that `tasty()` components use internally.
351
+
352
+ ---
353
+
267
354
  ## Hooks
268
355
 
269
356
  ### useStyles
270
357
 
271
- Generate a className from a style object:
358
+ Generate a className from a style object. Thin wrapper around `computeStyles()` that adds React context-based SSR collector discovery for backward compatibility:
272
359
 
273
360
  ```tsx
274
361
  import { useStyles } from '@tenphi/tasty';
@@ -469,7 +556,7 @@ function useCounterStyle(
469
556
  ### Troubleshooting
470
557
 
471
558
  - Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
472
- - SSR output looks wrong: check the [SSR guide](ssr.md) because the hooks integrate with SSR collectors differently than the client-only runtime path.
559
+ - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. `computeStyles()` discovers the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
473
560
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
474
561
 
475
562
  ---
package/docs/ssr.md CHANGED
@@ -18,16 +18,16 @@ The Astro integration (`@tenphi/tasty/ssr/astro`) has no additional dependencies
18
18
 
19
19
  ## How It Works
20
20
 
21
- When the environment can execute runtime React code during server rendering, the same `tasty()` and `useStyles()` calls can run there too. In Next.js, generic React SSR, and Astro islands, Tasty simply changes where that runtime-generated CSS goes: `useStyles()` detects a `ServerStyleCollector` and collects CSS into it instead of trying to access the DOM. The collector accumulates all styles, serializes them as `<style>` tags and a cache state script in the HTML. On the client, `hydrateTastyCache()` pre-populates the injector cache so that `useStyles()` skips the rendering pipeline entirely during hydration.
21
+ `tasty()` components are hook-free and use `computeStyles()` internally a synchronous, framework-agnostic function. On the server, `computeStyles()` detects a `ServerStyleCollector` (via `AsyncLocalStorage` or an explicit option) and collects CSS into it instead of trying to access the DOM. On the client, CSS is injected synchronously into the DOM during render; the injector's content-based cache makes this idempotent. The collector accumulates all styles, serializes them as `<style>` tags and a cache state script in the HTML. On the client, `hydrateTastyCache()` pre-populates the injector cache so that `computeStyles()` skips the rendering pipeline entirely during hydration.
22
22
 
23
23
  ```
24
24
  Server Client
25
25
  ────── ──────
26
26
  tasty() renders hydrateTastyCache() pre-populates cache
27
- └─ useStyles() └─ cacheKey → className map ready
27
+ └─ computeStyles() └─ cacheKey → className map ready
28
28
  └─ collector.collect()
29
29
  tasty() renders
30
- After render: └─ useStyles()
30
+ After render: └─ computeStyles()
31
31
  <style data-tasty-ssr> └─ cache hit → skip pipeline
32
32
  <script data-tasty-cache> └─ no CSS re-injection
33
33
  ```
@@ -82,15 +82,15 @@ That's it. All `tasty()` components inside the tree automatically get SSR suppor
82
82
 
83
83
  ### How it works
84
84
 
85
- - `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load.
86
- - During SSR, `useStyles()` finds the collector via React context and pushes CSS rules to it.
87
- - `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible -- styles are injected alongside each Suspense boundary as it resolves.
85
+ - `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load. The `'use client'` boundary is required solely to access `useServerInsertedHTML` — **not** because `tasty()` components need the client.
86
+ - During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level global getter. All style computation — whether from `tasty()` components, `computeStyles()`, `useStyles()`, or other hooks like `useGlobalStyles()` discovers the collector through this single global getter. No React context is involved.
87
+ - `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible styles are injected alongside each Suspense boundary as it resolves.
88
88
  - A companion `<script>` tag transfers the `cacheKey → className` mapping to the client.
89
- - When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `useStyles()` hits the cache and skips the entire pipeline.
89
+ - When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `computeStyles()` hits the cache and skips the entire pipeline.
90
90
 
91
91
  ### Using tasty() in Server Components
92
92
 
93
- `tasty()` components use React hooks internally, so they require `'use client'`. However, this does **not** prevent them from being used in Server Component pages. In Next.js, `'use client'` components are still server-rendered on initial load. Dynamic `styleProps` like `<Grid flow="column">` work normally when a `tasty()` component is imported into a Server Component page.
93
+ `tasty()` components are hook-free and do not require `'use client'`. They can be used directly in React Server Components. Dynamic `styleProps` like `<Grid flow="column">` work normally in server components. During SSR, `computeStyles()` discovers the collector via the same global getter registered by `TastyRegistry` — no React context or client boundary needed for this path.
94
94
 
95
95
  ### Options
96
96
 
@@ -161,10 +161,10 @@ import Card from '../components/Card.tsx';
161
161
 
162
162
  ### How it works
163
163
 
164
- Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware uses `AsyncLocalStorage` to make the collector available to all `useStyles()` calls within the request.
164
+ Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware uses `AsyncLocalStorage` to make the collector available to all `computeStyles()` calls within the request.
165
165
 
166
166
  - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>`. No JavaScript is shipped for these components.
167
- - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, importing `@tenphi/tasty/ssr/astro` auto-hydrates the cache from `<script data-tasty-cache>`. The island's `useStyles()` calls hit the cache during hydration.
167
+ - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, importing `@tenphi/tasty/ssr/astro` auto-hydrates the cache from `<script data-tasty-cache>`. The island's `computeStyles()` calls hit the cache during hydration.
168
168
 
169
169
  ### Client-side hydration for islands
170
170
 
@@ -197,12 +197,12 @@ Same as Next.js -- call `configure({ nonce: '...' })` before any rendering happe
197
197
 
198
198
  ## Generic Framework Integration
199
199
 
200
- Any React-based framework can integrate using the core SSR API:
200
+ Any React-based framework can integrate using `runWithCollector`, which binds a `ServerStyleCollector` to the current async context via `AsyncLocalStorage`. All `computeStyles()` and hook calls within the render automatically discover the collector.
201
201
 
202
202
  ```tsx
203
203
  import {
204
204
  ServerStyleCollector,
205
- TastySSRContext,
205
+ runWithCollector,
206
206
  hydrateTastyCache,
207
207
  } from '@tenphi/tasty/ssr';
208
208
  import { renderToString } from 'react-dom/server';
@@ -212,10 +212,8 @@ import { hydrateRoot } from 'react-dom/client';
212
212
 
213
213
  const collector = new ServerStyleCollector();
214
214
 
215
- const html = renderToString(
216
- <TastySSRContext.Provider value={collector}>
217
- <App />
218
- </TastySSRContext.Provider>
215
+ const html = await runWithCollector(collector, () =>
216
+ renderToString(<App />)
219
217
  );
220
218
 
221
219
  const css = collector.getCSS();
@@ -251,11 +249,8 @@ For streaming with `renderToPipeableStream`, use `flushCSS()` instead of `getCSS
251
249
  ```tsx
252
250
  const collector = new ServerStyleCollector();
253
251
 
254
- const stream = renderToPipeableStream(
255
- <TastySSRContext.Provider value={collector}>
256
- <App />
257
- </TastySSRContext.Provider>,
258
- {
252
+ const stream = await runWithCollector(collector, () =>
253
+ renderToPipeableStream(<App />, {
259
254
  onShellReady() {
260
255
  // Flush styles collected so far
261
256
  const css = collector.flushCSS();
@@ -270,31 +265,10 @@ const stream = renderToPipeableStream(
270
265
  const state = collector.getCacheState();
271
266
  res.write(`<script data-tasty-cache type="application/json">${JSON.stringify(state)}</script>`);
272
267
  },
273
- }
268
+ })
274
269
  );
275
270
  ```
276
271
 
277
- ### AsyncLocalStorage (no React context)
278
-
279
- If your framework doesn't support wrapping the React tree with a provider, use `runWithCollector`:
280
-
281
- ```tsx
282
- import {
283
- ServerStyleCollector,
284
- runWithCollector,
285
- hydrateTastyCache,
286
- } from '@tenphi/tasty/ssr';
287
-
288
- const collector = new ServerStyleCollector();
289
-
290
- const html = await runWithCollector(collector, () =>
291
- renderToString(<App />)
292
- );
293
-
294
- const css = collector.getCSS();
295
- // ... inject into HTML as above
296
- ```
297
-
298
272
  ---
299
273
 
300
274
  ## API Reference
@@ -303,7 +277,7 @@ const css = collector.getCSS();
303
277
 
304
278
  | Import path | Description |
305
279
  |---|---|
306
- | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `TastySSRContext`, `runWithCollector`, `hydrateTastyCache` |
280
+ | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `runWithCollector`, `hydrateTastyCache` |
307
281
  | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
308
282
  | `@tenphi/tasty/ssr/astro` | Astro: `tastyMiddleware`, auto-hydration on import |
309
283
 
@@ -323,10 +297,6 @@ Server-safe style collector. One instance per request.
323
297
  | `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
324
298
  | `getCacheState()` | Serialize `{ entries: Record<cacheKey, className>, classCounter }` for client hydration. |
325
299
 
326
- ### `TastySSRContext`
327
-
328
- React context (`createContext<ServerStyleCollector | null>(null)`). Used by `useStyles()` to find the collector during SSR.
329
-
330
300
  ### `TastyRegistry`
331
301
 
332
302
  Next.js App Router component. Props:
@@ -350,7 +320,7 @@ Pre-populate the client injector cache. When called without arguments, reads fro
350
320
 
351
321
  ### `runWithCollector(collector, fn)`
352
322
 
353
- Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All `useStyles()` calls within `fn` (and async continuations) will find this collector.
323
+ Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All `computeStyles()` and `useStyles()` calls within `fn` (and async continuations) will find this collector.
354
324
 
355
325
  ---
356
326
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/tasty",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "A design-system-integrated styling system and DSL for concise, state-aware UI styling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -150,13 +150,13 @@
150
150
  "name": "main (import *)",
151
151
  "path": "dist/index.js",
152
152
  "import": "*",
153
- "limit": "47 kB"
153
+ "limit": "48 kB"
154
154
  },
155
155
  {
156
156
  "name": "core (import *)",
157
157
  "path": "dist/core/index.js",
158
158
  "import": "*",
159
- "limit": "45 kB"
159
+ "limit": "46 kB"
160
160
  },
161
161
  {
162
162
  "name": "static",
@@ -184,7 +184,7 @@
184
184
  "path",
185
185
  "crypto"
186
186
  ],
187
- "limit": "41 kB"
187
+ "limit": "42 kB"
188
188
  }
189
189
  ],
190
190
  "scripts": {
@@ -1,14 +0,0 @@
1
- import { getRegisteredSSRCollector } from "../ssr/ssr-collector-ref.js";
2
- //#region src/hooks/resolve-ssr-collector.ts
3
- /**
4
- * Resolve the SSR collector from React context or AsyncLocalStorage.
5
- * Returns null on the client (no collector available).
6
- */
7
- function resolveSSRCollector(reactContext) {
8
- if (reactContext) return reactContext;
9
- return getRegisteredSSRCollector();
10
- }
11
- //#endregion
12
- export { resolveSSRCollector };
13
-
14
- //# sourceMappingURL=resolve-ssr-collector.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-ssr-collector.js","names":[],"sources":["../../src/hooks/resolve-ssr-collector.ts"],"sourcesContent":["import type { ServerStyleCollector } from '../ssr/collector';\nimport { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';\n\n/**\n * Resolve the SSR collector from React context or AsyncLocalStorage.\n * Returns null on the client (no collector available).\n */\nexport function resolveSSRCollector(\n reactContext: ServerStyleCollector | null,\n): ServerStyleCollector | null {\n if (reactContext) return reactContext;\n return getRegisteredSSRCollector();\n}\n"],"mappings":";;;;;;AAOA,SAAgB,oBACd,cAC6B;AAC7B,KAAI,aAAc,QAAO;AACzB,QAAO,2BAA2B"}
@@ -1,8 +0,0 @@
1
- import { ServerStyleCollector } from "./collector.js";
2
- import * as _$react from "react";
3
-
4
- //#region src/ssr/context.d.ts
5
- declare const TastySSRContext: _$react.Context<ServerStyleCollector | null>;
6
- //#endregion
7
- export { TastySSRContext };
8
- //# sourceMappingURL=context.d.ts.map
@@ -1,13 +0,0 @@
1
- import { createContext } from "react";
2
- //#region src/ssr/context.ts
3
- /**
4
- * React context for SSR collector discovery.
5
- *
6
- * Used by Next.js TastyRegistry to provide the ServerStyleCollector
7
- * to useStyles() via React context (the streaming-compatible path).
8
- */
9
- const TastySSRContext = createContext(null);
10
- //#endregion
11
- export { TastySSRContext };
12
-
13
- //# sourceMappingURL=context.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../../src/ssr/context.ts"],"sourcesContent":["/**\n * React context for SSR collector discovery.\n *\n * Used by Next.js TastyRegistry to provide the ServerStyleCollector\n * to useStyles() via React context (the streaming-compatible path).\n */\n\nimport { createContext } from 'react';\n\nimport type { ServerStyleCollector } from './collector';\n\nexport const TastySSRContext = createContext<ServerStyleCollector | null>(null);\n"],"mappings":";;;;;;;;AAWA,MAAa,kBAAkB,cAA2C,KAAK"}