@tenphi/tasty 0.1.0 → 0.1.3

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 (168) hide show
  1. package/README.md +27 -2
  2. package/dist/_virtual/_rolldown/runtime.js +8 -0
  3. package/dist/chunks/renderChunk.js.map +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/debug.js.map +1 -1
  6. package/dist/hooks/useKeyframes.js.map +1 -1
  7. package/dist/hooks/useRawCSS.js.map +1 -1
  8. package/dist/injector/injector.js.map +1 -1
  9. package/dist/injector/sheet-manager.js.map +1 -1
  10. package/dist/parser/index.d.ts +3 -0
  11. package/dist/parser/index.js +4 -0
  12. package/dist/parser/parser.js.map +1 -1
  13. package/dist/parser/types.d.ts +6 -1
  14. package/dist/pipeline/conditions.js.map +1 -1
  15. package/dist/pipeline/exclusive.js.map +1 -1
  16. package/dist/pipeline/index.js.map +1 -1
  17. package/dist/pipeline/materialize.js.map +1 -1
  18. package/dist/pipeline/parseStateKey.js.map +1 -1
  19. package/dist/pipeline/simplify.js.map +1 -1
  20. package/dist/styles/dimension.js.map +1 -1
  21. package/dist/styles/predefined.js.map +1 -1
  22. package/dist/styles/radius.js.map +1 -1
  23. package/dist/styles/scrollbar.js.map +1 -1
  24. package/dist/styles/shadow.js.map +1 -1
  25. package/dist/tasty.d.ts +8 -8
  26. package/dist/tasty.js.map +1 -1
  27. package/dist/utils/resolve-recipes.js.map +1 -1
  28. package/dist/utils/styles.js.map +1 -1
  29. package/dist/zero/{babel.d.mts → babel.d.ts} +6 -6
  30. package/dist/zero/{babel.mjs → babel.js} +6 -6
  31. package/dist/zero/babel.js.map +1 -0
  32. package/dist/{css-writer.d.mts → zero/css-writer.d.ts} +1 -1
  33. package/dist/{css-writer.mjs → zero/css-writer.js} +1 -1
  34. package/dist/zero/css-writer.js.map +1 -0
  35. package/dist/{extractor.d.mts → zero/extractor.d.ts} +3 -2
  36. package/dist/{extractor.mjs → zero/extractor.js} +6 -6
  37. package/dist/zero/extractor.js.map +1 -0
  38. package/dist/zero/index.d.ts +3 -0
  39. package/dist/zero/{index.mjs → index.js} +2 -2
  40. package/dist/zero/{next.d.mts → next.d.ts} +1 -1
  41. package/dist/zero/{next.mjs → next.js} +2 -2
  42. package/dist/zero/next.js.map +1 -0
  43. package/package.json +22 -23
  44. package/dist/_virtual/_rolldown/runtime.mjs +0 -7
  45. package/dist/chunks/cacheKey.mjs +0 -70
  46. package/dist/chunks/cacheKey.mjs.map +0 -1
  47. package/dist/chunks/definitions.mjs +0 -260
  48. package/dist/chunks/definitions.mjs.map +0 -1
  49. package/dist/chunks/renderChunk.mjs +0 -61
  50. package/dist/chunks/renderChunk.mjs.map +0 -1
  51. package/dist/config.mjs +0 -231
  52. package/dist/config.mjs.map +0 -1
  53. package/dist/css-writer.mjs.map +0 -1
  54. package/dist/extractor.mjs.map +0 -1
  55. package/dist/injector/injector.mjs +0 -404
  56. package/dist/injector/injector.mjs.map +0 -1
  57. package/dist/injector/sheet-manager.mjs +0 -714
  58. package/dist/injector/sheet-manager.mjs.map +0 -1
  59. package/dist/injector/types.d.mts +0 -18
  60. package/dist/keyframes/index.mjs +0 -156
  61. package/dist/keyframes/index.mjs.map +0 -1
  62. package/dist/parser/classify.mjs +0 -319
  63. package/dist/parser/classify.mjs.map +0 -1
  64. package/dist/parser/const.mjs +0 -33
  65. package/dist/parser/const.mjs.map +0 -1
  66. package/dist/parser/lru.mjs +0 -109
  67. package/dist/parser/lru.mjs.map +0 -1
  68. package/dist/parser/parser.mjs +0 -116
  69. package/dist/parser/parser.mjs.map +0 -1
  70. package/dist/parser/tokenizer.mjs +0 -69
  71. package/dist/parser/tokenizer.mjs.map +0 -1
  72. package/dist/parser/types.d.mts +0 -37
  73. package/dist/parser/types.mjs +0 -46
  74. package/dist/parser/types.mjs.map +0 -1
  75. package/dist/pipeline/conditions.mjs +0 -377
  76. package/dist/pipeline/conditions.mjs.map +0 -1
  77. package/dist/pipeline/exclusive.mjs +0 -231
  78. package/dist/pipeline/exclusive.mjs.map +0 -1
  79. package/dist/pipeline/index.mjs +0 -635
  80. package/dist/pipeline/index.mjs.map +0 -1
  81. package/dist/pipeline/materialize.mjs +0 -821
  82. package/dist/pipeline/materialize.mjs.map +0 -1
  83. package/dist/pipeline/parseStateKey.mjs +0 -418
  84. package/dist/pipeline/parseStateKey.mjs.map +0 -1
  85. package/dist/pipeline/simplify.mjs +0 -557
  86. package/dist/pipeline/simplify.mjs.map +0 -1
  87. package/dist/plugins/okhsl-plugin.mjs +0 -345
  88. package/dist/plugins/okhsl-plugin.mjs.map +0 -1
  89. package/dist/plugins/types.d.mts +0 -49
  90. package/dist/properties/index.mjs +0 -141
  91. package/dist/properties/index.mjs.map +0 -1
  92. package/dist/states/index.mjs +0 -161
  93. package/dist/states/index.mjs.map +0 -1
  94. package/dist/styles/align.mjs +0 -14
  95. package/dist/styles/align.mjs.map +0 -1
  96. package/dist/styles/border.mjs +0 -114
  97. package/dist/styles/border.mjs.map +0 -1
  98. package/dist/styles/color.mjs +0 -23
  99. package/dist/styles/color.mjs.map +0 -1
  100. package/dist/styles/createStyle.mjs +0 -77
  101. package/dist/styles/createStyle.mjs.map +0 -1
  102. package/dist/styles/dimension.mjs +0 -97
  103. package/dist/styles/dimension.mjs.map +0 -1
  104. package/dist/styles/display.mjs +0 -67
  105. package/dist/styles/display.mjs.map +0 -1
  106. package/dist/styles/fade.mjs +0 -58
  107. package/dist/styles/fade.mjs.map +0 -1
  108. package/dist/styles/fill.mjs +0 -51
  109. package/dist/styles/fill.mjs.map +0 -1
  110. package/dist/styles/flow.mjs +0 -12
  111. package/dist/styles/flow.mjs.map +0 -1
  112. package/dist/styles/gap.mjs +0 -37
  113. package/dist/styles/gap.mjs.map +0 -1
  114. package/dist/styles/height.mjs +0 -20
  115. package/dist/styles/height.mjs.map +0 -1
  116. package/dist/styles/index.mjs +0 -9
  117. package/dist/styles/index.mjs.map +0 -1
  118. package/dist/styles/inset.mjs +0 -142
  119. package/dist/styles/inset.mjs.map +0 -1
  120. package/dist/styles/justify.mjs +0 -14
  121. package/dist/styles/justify.mjs.map +0 -1
  122. package/dist/styles/margin.mjs +0 -96
  123. package/dist/styles/margin.mjs.map +0 -1
  124. package/dist/styles/outline.mjs +0 -65
  125. package/dist/styles/outline.mjs.map +0 -1
  126. package/dist/styles/padding.mjs +0 -96
  127. package/dist/styles/padding.mjs.map +0 -1
  128. package/dist/styles/predefined.mjs +0 -232
  129. package/dist/styles/predefined.mjs.map +0 -1
  130. package/dist/styles/preset.mjs +0 -126
  131. package/dist/styles/preset.mjs.map +0 -1
  132. package/dist/styles/radius.mjs +0 -51
  133. package/dist/styles/radius.mjs.map +0 -1
  134. package/dist/styles/scrollbar.mjs +0 -105
  135. package/dist/styles/scrollbar.mjs.map +0 -1
  136. package/dist/styles/shadow.mjs +0 -24
  137. package/dist/styles/shadow.mjs.map +0 -1
  138. package/dist/styles/styledScrollbar.mjs +0 -38
  139. package/dist/styles/styledScrollbar.mjs.map +0 -1
  140. package/dist/styles/transition.mjs +0 -138
  141. package/dist/styles/transition.mjs.map +0 -1
  142. package/dist/styles/types.d.mts +0 -492
  143. package/dist/styles/width.mjs +0 -20
  144. package/dist/styles/width.mjs.map +0 -1
  145. package/dist/utils/cache-wrapper.mjs +0 -26
  146. package/dist/utils/cache-wrapper.mjs.map +0 -1
  147. package/dist/utils/case-converter.mjs +0 -8
  148. package/dist/utils/case-converter.mjs.map +0 -1
  149. package/dist/utils/hsl-to-rgb.mjs +0 -38
  150. package/dist/utils/hsl-to-rgb.mjs.map +0 -1
  151. package/dist/utils/is-dev-env.mjs +0 -19
  152. package/dist/utils/is-dev-env.mjs.map +0 -1
  153. package/dist/utils/merge-styles.mjs +0 -146
  154. package/dist/utils/merge-styles.mjs.map +0 -1
  155. package/dist/utils/okhsl-to-rgb.mjs +0 -296
  156. package/dist/utils/okhsl-to-rgb.mjs.map +0 -1
  157. package/dist/utils/process-tokens.mjs +0 -28
  158. package/dist/utils/process-tokens.mjs.map +0 -1
  159. package/dist/utils/resolve-recipes.mjs +0 -143
  160. package/dist/utils/resolve-recipes.mjs.map +0 -1
  161. package/dist/utils/string.mjs +0 -8
  162. package/dist/utils/string.mjs.map +0 -1
  163. package/dist/utils/styles.d.mts +0 -18
  164. package/dist/utils/styles.mjs +0 -346
  165. package/dist/utils/styles.mjs.map +0 -1
  166. package/dist/zero/babel.mjs.map +0 -1
  167. package/dist/zero/index.d.mts +0 -3
  168. package/dist/zero/next.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Tasty Configuration Module\n *\n * Centralizes all tasty configuration, including:\n * - Style injector settings (nonce, cleanup thresholds, etc.)\n * - Global predefined states for advanced state mapping\n * - stylesGenerated flag that locks configuration after first style generation\n *\n * Configuration must be done BEFORE any styles are generated.\n * After the first `inject()` call, configuration is locked and attempts to\n * reconfigure will emit a warning and be ignored.\n */\n\nimport { StyleInjector } from './injector/injector';\nimport { clearPipelineCache, isSelector } from './pipeline';\nimport { setGlobalPredefinedStates } from './states';\nimport {\n normalizeHandlerDefinition,\n registerHandler,\n resetHandlers,\n} from './styles/predefined';\nimport { isDevEnv } from './utils/is-dev-env';\nimport {\n CUSTOM_UNITS,\n getGlobalFuncs,\n getGlobalParser,\n normalizeColorTokenValue,\n resetGlobalPredefinedTokens,\n setGlobalPredefinedTokens,\n} from './utils/styles';\n\nimport type { KeyframesSteps, PropertyDefinition } from './injector/types';\nimport type { StyleDetails, UnitHandler } from './parser/types';\nimport type { TastyPlugin } from './plugins/types';\nimport type { RecipeStyles } from './styles/types';\nimport type { StyleHandlerDefinition } from './utils/styles';\n\n/**\n * Configuration options for the Tasty style system\n */\nexport interface TastyConfig {\n /** CSP nonce for style elements */\n nonce?: string;\n /** Maximum rules per stylesheet (default: 8192) */\n maxRulesPerSheet?: number;\n /** Threshold for bulk cleanup of unused styles (default: 500) */\n unusedStylesThreshold?: number;\n /** Delay before bulk cleanup in ms, ignored if idleCleanup is true (default: 5000) */\n bulkCleanupDelay?: number;\n /** Use requestIdleCallback for cleanup when available (default: true) */\n idleCleanup?: boolean;\n /** Force text injection mode, auto-detected in test environments (default: auto) */\n forceTextInjection?: boolean;\n /** Enable development mode features: performance metrics and debug info (default: auto) */\n devMode?: boolean;\n /**\n * Ratio of unused styles to delete per bulk cleanup run (0..1).\n * Defaults to 0.5 (oldest half) to reduce risk of removing styles\n * that may be restored shortly after being marked unused.\n */\n bulkCleanupBatchRatio?: number;\n /**\n * Minimum age (in ms) a style must remain unused before eligible for deletion.\n * Helps avoid races during rapid mount/unmount cycles. Default: 10000ms.\n */\n unusedStylesMinAgeMs?: number;\n /**\n * Global predefined states for advanced state mapping.\n * These are state aliases that can be used in any component.\n * Example: { '@mobile': '@media(w < 920px)', '@dark': '@root(theme=dark)' }\n */\n states?: Record<string, string>;\n /**\n * Parser LRU cache size (default: 1000).\n * Larger values improve performance for apps with many unique style values.\n */\n parserCacheSize?: number;\n /**\n * Custom units for the style parser (merged with built-in units).\n * Units transform numeric values like `2x` → `calc(2 * var(--gap))`.\n * @example { em: 'em', vw: 'vw', custom: (n) => `${n * 10}px` }\n */\n units?: Record<string, string | UnitHandler>;\n /**\n * Custom functions for the style parser (merged with existing).\n * Functions process parsed style groups and return CSS values.\n * @example { myFunc: (groups) => groups.map(g => g.output).join(' ') }\n */\n funcs?: Record<string, (groups: StyleDetails[]) => string>;\n /**\n * Plugins that extend tasty with custom functions, units, or states.\n * Plugins are processed in order, with later plugins overriding earlier ones.\n * @example\n * ```ts\n * import { okhslPlugin } from '@tenphi/tasty';\n *\n * configure({\n * plugins: [okhslPlugin()],\n * });\n * ```\n */\n plugins?: TastyPlugin[];\n /**\n * Global keyframes definitions that can be referenced by animation names in styles.\n * Keys are animation names, values are keyframes step definitions.\n * Keyframes are only injected when actually used in styles.\n * @example\n * ```ts\n * configure({\n * keyframes: {\n * fadeIn: { from: { opacity: 0 }, to: { opacity: 1 } },\n * pulse: { '0%, 100%': { transform: 'scale(1)' }, '50%': { transform: 'scale(1.05)' } },\n * },\n * });\n * ```\n */\n keyframes?: Record<string, KeyframesSteps>;\n /**\n * Global CSS @property definitions for custom properties.\n * Keys use tasty token syntax ($name for properties, #name for colors).\n * Properties are only injected when the component using them is rendered.\n *\n * For color tokens (#name), `syntax: '<color>'` is auto-set and\n * `initialValue` defaults to `'transparent'` if not specified.\n *\n * @example\n * ```ts\n * configure({\n * properties: {\n * '$rotation': { syntax: '<angle>', initialValue: '0deg' },\n * '$scale': { syntax: '<number>', inherits: false, initialValue: 1 },\n * '#accent': { initialValue: 'purple' }, // syntax: '<color>' auto-set\n * },\n * });\n *\n * // Now use in styles - properties are registered when component renders:\n * const Spinner = tasty({\n * styles: {\n * transform: 'rotate($rotation)',\n * transition: '$$rotation 0.3s', // outputs: --rotation 0.3s\n * },\n * });\n * ```\n */\n properties?: Record<string, PropertyDefinition>;\n /**\n * Custom style handlers that transform style properties into CSS declarations.\n * Handlers replace built-in handlers for the same style name.\n * @example\n * ```ts\n * import { styleHandlers } from '@tenphi/tasty';\n *\n * configure({\n * handlers: {\n * // Override fill with custom behavior\n * fill: ({ fill }) => {\n * if (fill?.startsWith('gradient:')) {\n * return { background: fill.slice(9) };\n * }\n * return styleHandlers.fill({ fill });\n * },\n * // Add new custom style\n * elevation: ({ elevation }) => {\n * const level = parseInt(elevation) || 1;\n * return {\n * 'box-shadow': `0 ${level * 2}px ${level * 4}px rgba(0,0,0,0.1)`,\n * 'z-index': String(level * 100),\n * };\n * },\n * },\n * });\n * ```\n */\n handlers?: Record<string, StyleHandlerDefinition>;\n /**\n * Predefined tokens that are replaced during style parsing.\n * Token values are processed through the parser (like component tokens).\n * Use `$name` for custom properties and `#name` for color tokens.\n *\n * For color tokens (#name), boolean `true` is converted to `transparent`.\n *\n * @example\n * ```ts\n * configure({\n * tokens: {\n * $spacing: '2x',\n * '$card-padding': '4x',\n * '#accent': '#purple',\n * '#surface': '#white',\n * '#overlay': true, // → transparent\n * },\n * });\n *\n * // Now use in styles - tokens are replaced at parse time:\n * const Card = tasty({\n * styles: {\n * padding: '$card-padding', // → calc(4 * var(--gap))\n * fill: '#surface', // → var(--white-color)\n * },\n * });\n * ```\n */\n tokens?: Record<`$${string}`, string | number | boolean> & Record<`#${string}`, string | number | boolean>;\n /**\n * Predefined style recipes -- named style bundles that can be applied via `recipe` style property.\n * Recipe values are flat tasty styles (no sub-element keys). They may contain base styles,\n * tokens (`$name`/`#name` definitions), local states, `@keyframes`, and `@properties`.\n *\n * Components reference recipes via: `recipe: 'name1 name2'` in their styles.\n * Use `|` to separate base recipes from post recipes: `recipe: 'base1 base2 | post1'`.\n * Resolution order: `base_recipes → component styles → post_recipes`.\n *\n * Recipes cannot reference other recipes.\n *\n * @example\n * ```ts\n * configure({\n * recipes: {\n * card: { padding: '4x', fill: '#surface', radius: '1r', border: true },\n * elevated: { shadow: '2x 2x 4x #shadow' },\n * },\n * });\n *\n * // Usage in styles:\n * const Card = tasty({\n * styles: {\n * recipe: 'card elevated',\n * color: '#text', // Overrides recipe values\n * },\n * });\n * ```\n */\n recipes?: Record<string, RecipeStyles>;\n}\n\n// Warnings tracking to avoid duplicates\nconst emittedWarnings = new Set<string>();\n\nconst devMode = isDevEnv();\n\n/**\n * Emit a warning only once\n */\nfunction warnOnce(key: string, message: string): void {\n if (devMode && !emittedWarnings.has(key)) {\n emittedWarnings.add(key);\n console.warn(message);\n }\n}\n\n// ============================================================================\n// Configuration State\n// ============================================================================\n\n// Track whether styles have been generated (locks configuration)\nlet stylesGenerated = false;\n\n// Current configuration (null until first configure() or auto-configured on first use)\nlet currentConfig: TastyConfig | null = null;\n\n// Global keyframes storage (null = no keyframes configured, empty object checked via hasGlobalKeyframes)\nlet globalKeyframes: Record<string, KeyframesSteps> | null = null;\n\n// Global properties storage (null = no properties configured)\nlet globalProperties: Record<string, PropertyDefinition> | null = null;\n\n// Global recipes storage (null = no recipes configured)\nlet globalRecipes: Record<string, RecipeStyles> | null = null;\n\n/**\n * Internal properties required by tasty core features.\n * These are always injected when styles are first generated.\n * Keys use tasty token syntax (#name for colors, $name for other properties).\n *\n * For properties with CSS @property-compatible types (length, time, number, color),\n * an `initialValue` is provided so the property works even without a project-level token.\n */\nexport const INTERNAL_PROPERTIES: Record<string, PropertyDefinition> = {\n // Used by dual-fill feature to enable CSS transitions on the second fill color\n '#tasty-second-fill': {\n inherits: false,\n initialValue: 'transparent',\n },\n // Current color context variable (set by the color style handler).\n // Companion --current-color-rgb is auto-created.\n '#current': {\n inherits: true,\n initialValue: 'transparent',\n },\n // White and black are fundamental colors that need explicit initial values.\n // Companion -rgb properties are auto-created from the color initial values.\n '#white': {\n inherits: true,\n initialValue: 'rgb(255 255 255)',\n },\n '#black': {\n inherits: true,\n initialValue: 'rgb(0 0 0)',\n },\n\n // ---- Core design tokens used by style handlers ----\n // These provide sensible defaults so tasty works standalone without a design system.\n // Consuming projects (e.g. uikit) override these by defining tokens on :root.\n\n $gap: {\n syntax: '<length>',\n inherits: true,\n initialValue: '4px',\n },\n $radius: {\n syntax: '<length>',\n inherits: true,\n initialValue: '6px',\n },\n '$border-width': {\n syntax: '<length>',\n inherits: true,\n initialValue: '1px',\n },\n '$outline-width': {\n syntax: '<length>',\n inherits: true,\n initialValue: '3px',\n },\n $transition: {\n syntax: '<time>',\n inherits: true,\n initialValue: '80ms',\n },\n // Used by radius.ts for `radius=\"leaf\"` modifier\n '$sharp-radius': {\n syntax: '<length>',\n inherits: true,\n initialValue: '0px',\n },\n // Used by preset.ts for `preset=\"... strong\"`\n '$bold-font-weight': {\n syntax: '<number>',\n inherits: true,\n initialValue: '700',\n },\n};\n\n/**\n * Internal token defaults that cannot be expressed as CSS @property initial values\n * (e.g. font stacks, keyword colors). These are injected as :root CSS variables.\n * Consuming projects override them by setting their own tokens on :root.\n *\n * Keys are raw CSS custom property names (--name).\n */\nexport const INTERNAL_TOKENS: Record<string, string> = {\n '--font':\n 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, \"Apple Color Emoji\", \"Segoe UI Emoji\", sans-serif',\n '--monospace-font':\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace',\n // Default border color to the element's current text color\n '--border-color': 'currentColor',\n};\n\n// Global injector instance key\nconst GLOBAL_INJECTOR_KEY = '__TASTY_GLOBAL_INJECTOR__';\n\ninterface TastyGlobalStorage {\n [GLOBAL_INJECTOR_KEY]?: StyleInjector;\n}\n\ndeclare global {\n interface Window {\n [GLOBAL_INJECTOR_KEY]?: StyleInjector;\n }\n\n \n var __TASTY_GLOBAL_INJECTOR__: StyleInjector | undefined;\n}\n\n/**\n * Detect if we're running in a test environment\n */\nexport function isTestEnvironment(): boolean {\n // Check Node.js environment\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {\n return true;\n }\n\n // Check for test runner globals (safely)\n if (typeof global !== 'undefined') {\n const g = global as unknown as Record<string, unknown>;\n if (g.vi || g.jest || g.expect || g.describe || g.it) {\n return true;\n }\n }\n\n // Check for jsdom environment (common in tests)\n if (\n typeof window !== 'undefined' &&\n window.navigator?.userAgent?.includes('jsdom')\n ) {\n return true;\n }\n\n // Check for other test runners\n if (typeof globalThis !== 'undefined') {\n const gt = globalThis as unknown as Record<string, unknown>;\n if (gt.vitest || gt.mocha) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Create default configuration with optional test environment detection\n */\nfunction createDefaultConfig(isTest?: boolean): TastyConfig {\n return {\n maxRulesPerSheet: 8192,\n unusedStylesThreshold: 500,\n bulkCleanupDelay: 5000,\n idleCleanup: true,\n forceTextInjection: isTest ?? false,\n devMode: isDevEnv(),\n bulkCleanupBatchRatio: 0.5,\n unusedStylesMinAgeMs: 10000,\n };\n}\n\n// ============================================================================\n// stylesGenerated Flag Management\n// ============================================================================\n\n/**\n * Mark that styles have been generated (called by injector on first inject)\n * This locks the configuration - no further changes allowed.\n * Also injects internal and global properties.\n */\nexport function markStylesGenerated(): void {\n if (stylesGenerated) return; // Already marked, skip\n\n stylesGenerated = true;\n\n const injector = getGlobalInjector();\n\n // Inject internal properties required by tasty core features\n for (const [token, definition] of Object.entries(INTERNAL_PROPERTIES)) {\n injector.property(token, definition);\n }\n\n // Inject internal token defaults as :root CSS variables.\n // Use injectGlobal (not injectRawCSS) so the rule goes through the same\n // injection path as all other tasty styles (consistent in both DOM and text/test mode).\n const internalTokenEntries = Object.entries(INTERNAL_TOKENS);\n if (internalTokenEntries.length > 0) {\n const declarations = internalTokenEntries\n .map(([name, value]) => `${name}: ${value}`)\n .join('; ');\n injector.injectGlobal([{ selector: ':root', declarations }]);\n }\n\n // Inject global properties if any were configured\n // Properties are permanent and only need to be injected once\n if (globalProperties && Object.keys(globalProperties).length > 0) {\n for (const [token, definition] of Object.entries(globalProperties)) {\n injector.property(token, definition);\n }\n }\n}\n\n/**\n * Check if styles have been generated (configuration is locked)\n */\nexport function hasStylesGenerated(): boolean {\n return stylesGenerated;\n}\n\n/**\n * Reset styles generated flag (for testing only)\n */\nexport function resetStylesGenerated(): void {\n stylesGenerated = false;\n emittedWarnings.clear();\n}\n\n// ============================================================================\n// Global Keyframes Management\n// ============================================================================\n\n/**\n * Check if any global keyframes are configured.\n * Fast path: returns false if no keyframes were ever set.\n */\nexport function hasGlobalKeyframes(): boolean {\n return globalKeyframes !== null && Object.keys(globalKeyframes).length > 0;\n}\n\n/**\n * Get global keyframes configuration.\n * Returns null if no keyframes configured (fast path for zero-overhead).\n */\nexport function getGlobalKeyframes(): Record<string, KeyframesSteps> | null {\n return globalKeyframes;\n}\n\n/**\n * Set global keyframes (called from configure).\n * Internal use only.\n */\nfunction setGlobalKeyframes(keyframes: Record<string, KeyframesSteps>): void {\n if (stylesGenerated) {\n warnOnce(\n 'keyframes-after-styles',\n `[Tasty] Cannot update keyframes after styles have been generated.\\n` +\n `The new keyframes will be ignored.`,\n );\n return;\n }\n globalKeyframes = keyframes;\n}\n\n// ============================================================================\n// Global Properties Management\n// ============================================================================\n\n/**\n * Check if any global properties are configured.\n * Fast path: returns false if no properties were ever set.\n */\nexport function hasGlobalProperties(): boolean {\n return globalProperties !== null && Object.keys(globalProperties).length > 0;\n}\n\n/**\n * Get global properties configuration.\n * Returns null if no properties configured (fast path for zero-overhead).\n */\nexport function getGlobalProperties(): Record<\n string,\n PropertyDefinition\n> | null {\n return globalProperties;\n}\n\n/**\n * Set global properties (called from configure).\n * Internal use only.\n */\nfunction setGlobalProperties(\n properties: Record<string, PropertyDefinition>,\n): void {\n if (stylesGenerated) {\n warnOnce(\n 'properties-after-styles',\n `[Tasty] Cannot update properties after styles have been generated.\\n` +\n `The new properties will be ignored.`,\n );\n return;\n }\n globalProperties = properties;\n}\n\n// ============================================================================\n// Global Recipes Management\n// ============================================================================\n\n/**\n * Check if any global recipes are configured.\n * Fast path: returns false if no recipes were ever set.\n */\nexport function hasGlobalRecipes(): boolean {\n return globalRecipes !== null && Object.keys(globalRecipes).length > 0;\n}\n\n/**\n * Get global recipes configuration.\n * Returns null if no recipes configured (fast path for zero-overhead).\n */\nexport function getGlobalRecipes(): Record<string, RecipeStyles> | null {\n return globalRecipes;\n}\n\n/**\n * Set global recipes (called from configure).\n * Internal use only.\n */\nfunction setGlobalRecipes(recipes: Record<string, RecipeStyles>): void {\n if (stylesGenerated) {\n warnOnce(\n 'recipes-after-styles',\n `[Tasty] Cannot update recipes after styles have been generated.\\n` +\n `The new recipes will be ignored.`,\n );\n return;\n }\n\n // Dev-mode validation\n if (devMode) {\n for (const [name, recipeStyles] of Object.entries(recipes)) {\n for (const key of Object.keys(recipeStyles)) {\n if (isSelector(key)) {\n warnOnce(\n `recipe-selector-${name}-${key}`,\n `[Tasty] Recipe \"${name}\" contains sub-element key \"${key}\". ` +\n `Recipes must be flat styles without sub-element keys. ` +\n `Remove the sub-element key from the recipe definition.`,\n );\n }\n if (key === 'recipe') {\n warnOnce(\n `recipe-recursive-${name}`,\n `[Tasty] Recipe \"${name}\" contains a \"recipe\" key. ` +\n `Recipes cannot reference other recipes. ` +\n `Use space-separated names for composition: recipe: 'base elevated'.`,\n );\n }\n }\n }\n }\n\n globalRecipes = recipes;\n}\n\n/**\n * Check if configuration is locked (styles have been generated)\n */\nexport function isConfigLocked(): boolean {\n return stylesGenerated;\n}\n\n// ============================================================================\n// Configuration API\n// ============================================================================\n\n/**\n * Configure the Tasty style system.\n *\n * Must be called BEFORE any styles are generated (before first render that uses tasty).\n * After styles are generated, configuration is locked and calls to configure() will\n * emit a warning and be ignored.\n *\n * @example\n * ```ts\n * import { configure } from '@tenphi/tasty';\n *\n * // Configure before app renders\n * configure({\n * nonce: 'abc123',\n * states: {\n * '@mobile': '@media(w < 768px)',\n * '@dark': '@root(theme=dark)',\n * },\n * });\n * ```\n */\nexport function configure(config: Partial<TastyConfig> = {}): void {\n if (stylesGenerated) {\n warnOnce(\n 'configure-after-styles',\n `[Tasty] Cannot call configure() after styles have been generated.\\n` +\n `Configuration must be done before the first render. The configuration will be ignored.`,\n );\n return;\n }\n\n // Collect merged values from plugins first, then override with direct config\n let mergedStates: Record<string, string> = {};\n let mergedUnits: Record<string, string | UnitHandler> = {};\n let mergedFuncs: Record<string, (groups: StyleDetails[]) => string> = {};\n let mergedHandlers: Record<string, StyleHandlerDefinition> = {};\n let mergedTokens: Record<string, string | number | boolean> = {};\n let mergedRecipes: Record<string, RecipeStyles> = {};\n\n // Process plugins in order\n if (config.plugins) {\n for (const plugin of config.plugins) {\n if (plugin.states) {\n mergedStates = { ...mergedStates, ...plugin.states };\n }\n if (plugin.units) {\n mergedUnits = { ...mergedUnits, ...plugin.units };\n }\n if (plugin.funcs) {\n mergedFuncs = { ...mergedFuncs, ...plugin.funcs };\n }\n if (plugin.handlers) {\n mergedHandlers = { ...mergedHandlers, ...plugin.handlers };\n }\n if (plugin.tokens) {\n mergedTokens = { ...mergedTokens, ...plugin.tokens };\n }\n if (plugin.recipes) {\n mergedRecipes = { ...mergedRecipes, ...plugin.recipes };\n }\n }\n }\n\n // Direct config overrides plugins\n if (config.states) {\n mergedStates = { ...mergedStates, ...config.states };\n }\n if (config.units) {\n mergedUnits = { ...mergedUnits, ...config.units };\n }\n if (config.funcs) {\n mergedFuncs = { ...mergedFuncs, ...config.funcs };\n }\n if (config.handlers) {\n mergedHandlers = { ...mergedHandlers, ...config.handlers };\n }\n if (config.tokens) {\n mergedTokens = { ...mergedTokens, ...config.tokens };\n }\n if (config.recipes) {\n mergedRecipes = { ...mergedRecipes, ...config.recipes };\n }\n\n // Handle predefined states\n if (Object.keys(mergedStates).length > 0) {\n setGlobalPredefinedStates(mergedStates);\n }\n\n // Handle parser configuration (merge semantics - extend, not replace)\n const parser = getGlobalParser();\n\n if (config.parserCacheSize !== undefined) {\n parser.updateOptions({ cacheSize: config.parserCacheSize });\n }\n\n if (Object.keys(mergedUnits).length > 0) {\n // Merge with existing units\n const currentUnits = parser.getUnits() ?? CUSTOM_UNITS;\n parser.setUnits({ ...currentUnits, ...mergedUnits });\n }\n\n if (Object.keys(mergedFuncs).length > 0) {\n // Merge with existing funcs\n const currentFuncs = getGlobalFuncs();\n const finalFuncs = { ...currentFuncs, ...mergedFuncs };\n parser.setFuncs(finalFuncs);\n // Also update the global registry so customFunc() continues to work\n Object.assign(currentFuncs, mergedFuncs);\n }\n\n // Handle keyframes\n if (config.keyframes) {\n setGlobalKeyframes(config.keyframes);\n }\n\n // Handle properties\n if (config.properties) {\n setGlobalProperties(config.properties);\n }\n\n // Handle custom handlers\n if (Object.keys(mergedHandlers).length > 0) {\n for (const [name, definition] of Object.entries(mergedHandlers)) {\n const handler = normalizeHandlerDefinition(name, definition);\n registerHandler(handler);\n }\n }\n\n // Handle predefined tokens\n // Note: Tokens are processed by the classifier, not here.\n // We just store the raw values; the classifier will process them when encountered.\n if (Object.keys(mergedTokens).length > 0) {\n // Store tokens (keys are normalized to lowercase by setGlobalPredefinedTokens)\n const processedTokens: Record<string, string> = {};\n for (const [key, value] of Object.entries(mergedTokens)) {\n if (key.startsWith('#')) {\n // Color token - use shared helper for boolean handling\n const normalized = normalizeColorTokenValue(value);\n if (normalized === null) continue; // Skip false values\n processedTokens[key] = String(normalized);\n } else if (value === false) {\n // Skip false values for non-color tokens\n continue;\n } else {\n processedTokens[key] = String(value);\n }\n }\n setGlobalPredefinedTokens(processedTokens);\n }\n\n // Handle recipes\n if (Object.keys(mergedRecipes).length > 0) {\n setGlobalRecipes(mergedRecipes);\n }\n\n \n const {\n states: _states,\n parserCacheSize: _parserCacheSize,\n units: _units,\n funcs: _funcs,\n plugins: _plugins,\n keyframes: _keyframes,\n properties: _properties,\n handlers: _handlers,\n tokens: _tokens,\n recipes: _recipes,\n ...injectorConfig\n } = config;\n \n\n const fullConfig: TastyConfig = {\n ...createDefaultConfig(),\n ...currentConfig,\n ...injectorConfig,\n };\n\n // Store the config\n currentConfig = fullConfig;\n\n // Create/replace the global injector\n const storage: TastyGlobalStorage =\n typeof window !== 'undefined' ? window : globalThis;\n storage[GLOBAL_INJECTOR_KEY] = new StyleInjector(fullConfig);\n}\n\n/**\n * Get the current configuration.\n * If not configured, returns default configuration.\n */\nexport function getConfig(): TastyConfig {\n if (!currentConfig) {\n currentConfig = createDefaultConfig(isTestEnvironment());\n }\n return currentConfig;\n}\n\n/**\n * Get the global injector instance.\n * Auto-configures with defaults if not already configured.\n */\nexport function getGlobalInjector(): StyleInjector {\n const storage: TastyGlobalStorage =\n typeof window !== 'undefined' ? window : globalThis;\n\n if (!storage[GLOBAL_INJECTOR_KEY]) {\n configure();\n }\n\n return storage[GLOBAL_INJECTOR_KEY]!;\n}\n\n/**\n * Reset configuration (for testing only).\n * Clears the global injector and allows reconfiguration.\n */\nexport function resetConfig(): void {\n stylesGenerated = false;\n currentConfig = null;\n globalKeyframes = null;\n globalProperties = null;\n globalRecipes = null;\n resetGlobalPredefinedTokens();\n resetHandlers();\n clearPipelineCache();\n emittedWarnings.clear();\n\n const storage: TastyGlobalStorage =\n typeof window !== 'undefined' ? window : globalThis;\n delete storage[GLOBAL_INJECTOR_KEY];\n}\n\n// Re-export TastyConfig as StyleInjectorConfig for backward compatibility\nexport type { TastyConfig as StyleInjectorConfig };\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"css-writer.mjs","names":[],"sources":["../src/zero/css-writer.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface CSSWriterOptions {\n /** Enable source comments in output (e.g., \"from: Button.tsx\") */\n devMode?: boolean;\n}\n\ninterface CSSBlock {\n css: string;\n source?: string;\n}\n\nexport class CSSWriter {\n private cssBlocks = new Map<string, CSSBlock>();\n private outputPath: string;\n private devMode: boolean;\n\n constructor(outputPath: string, options: CSSWriterOptions = {}) {\n this.outputPath = outputPath;\n this.devMode = options.devMode ?? false;\n }\n\n /**\n * Add CSS block with deduplication key\n * @param key - Unique key for deduplication\n * @param css - CSS content\n * @param source - Optional source file path (used in devMode)\n */\n add(key: string, css: string, source?: string): void {\n this.cssBlocks.set(key, { css, source });\n }\n\n /**\n * Check if a key already exists\n */\n has(key: string): boolean {\n return this.cssBlocks.has(key);\n }\n\n /**\n * Get the number of CSS blocks\n */\n get size(): number {\n return this.cssBlocks.size;\n }\n\n /**\n * Write all collected CSS to the output file\n */\n write(): void {\n const outputDir = path.dirname(this.outputPath);\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n // Combine all CSS blocks with optional source comments\n const cssBlocks: string[] = [];\n for (const block of this.cssBlocks.values()) {\n if (this.devMode && block.source) {\n cssBlocks.push(`/* from: ${block.source} */\\n${block.css}`);\n } else {\n cssBlocks.push(block.css);\n }\n }\n const css = cssBlocks.join('\\n\\n');\n\n // Add header comment\n const header = `/* Generated by @tenphi/tasty/zero - DO NOT EDIT */\\n\\n`;\n\n fs.writeFileSync(this.outputPath, header + css);\n }\n\n /**\n * Get all CSS as string (for testing or in-memory use)\n */\n getCSS(): string {\n const cssBlocks: string[] = [];\n for (const block of this.cssBlocks.values()) {\n if (this.devMode && block.source) {\n cssBlocks.push(`/* from: ${block.source} */\\n${block.css}`);\n } else {\n cssBlocks.push(block.css);\n }\n }\n return cssBlocks.join('\\n\\n');\n }\n\n /**\n * Clear all collected CSS\n */\n clear(): void {\n this.cssBlocks.clear();\n }\n\n /**\n * Get the output path\n */\n getOutputPath(): string {\n return this.outputPath;\n }\n}\n"],"mappings":";;;;AAaA,IAAa,YAAb,MAAuB;CACrB,AAAQ,4BAAY,IAAI,KAAuB;CAC/C,AAAQ;CACR,AAAQ;CAER,YAAY,YAAoB,UAA4B,EAAE,EAAE;AAC9D,OAAK,aAAa;AAClB,OAAK,UAAU,QAAQ,WAAW;;;;;;;;CASpC,IAAI,KAAa,KAAa,QAAuB;AACnD,OAAK,UAAU,IAAI,KAAK;GAAE;GAAK;GAAQ,CAAC;;;;;CAM1C,IAAI,KAAsB;AACxB,SAAO,KAAK,UAAU,IAAI,IAAI;;;;;CAMhC,IAAI,OAAe;AACjB,SAAO,KAAK,UAAU;;;;;CAMxB,QAAc;EACZ,MAAM,YAAY,KAAK,QAAQ,KAAK,WAAW;AAG/C,MAAI,CAAC,GAAG,WAAW,UAAU,CAC3B,IAAG,UAAU,WAAW,EAAE,WAAW,MAAM,CAAC;EAI9C,MAAM,YAAsB,EAAE;AAC9B,OAAK,MAAM,SAAS,KAAK,UAAU,QAAQ,CACzC,KAAI,KAAK,WAAW,MAAM,OACxB,WAAU,KAAK,YAAY,MAAM,OAAO,OAAO,MAAM,MAAM;MAE3D,WAAU,KAAK,MAAM,IAAI;EAG7B,MAAM,MAAM,UAAU,KAAK,OAAO;AAKlC,KAAG,cAAc,KAAK,YAFP,4DAE4B,IAAI;;;;;CAMjD,SAAiB;EACf,MAAM,YAAsB,EAAE;AAC9B,OAAK,MAAM,SAAS,KAAK,UAAU,QAAQ,CACzC,KAAI,KAAK,WAAW,MAAM,OACxB,WAAU,KAAK,YAAY,MAAM,OAAO,OAAO,MAAM,MAAM;MAE3D,WAAU,KAAK,MAAM,IAAI;AAG7B,SAAO,UAAU,KAAK,OAAO;;;;;CAM/B,QAAc;AACZ,OAAK,UAAU,OAAO;;;;;CAMxB,gBAAwB;AACtB,SAAO,KAAK"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"extractor.mjs","names":[],"sources":["../src/zero/extractor.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\nimport {\n categorizeStyleKeys,\n generateChunkCacheKey,\n renderStylesForChunk,\n} from '../chunks';\nimport type { KeyframesSteps } from '../injector/types';\nimport {\n extractAnimationNamesFromStyles,\n extractLocalKeyframes,\n filterUsedKeyframes,\n hasLocalKeyframes,\n mergeKeyframes,\n} from '../keyframes';\nimport type { StyleResult } from '../pipeline';\nimport { renderStyles } from '../pipeline';\nimport type { Styles } from '../styles/types';\n\nexport interface ExtractedChunk {\n className: string;\n css: string;\n}\n\nexport interface ExtractedSelector {\n selector: string;\n css: string;\n}\n\nexport interface ExtractedKeyframes {\n name: string;\n css: string;\n}\n\nexport interface KeyframesExtractionResult {\n /** Keyframes to inject (deduplicated by content) */\n keyframes: ExtractedKeyframes[];\n /** Map from original animation name to canonical name (for replacement) */\n nameMap: Map<string, string>;\n}\n\n/**\n * Generate a deterministic className from a cache key using content hash.\n * This ensures the same styles always produce the same className,\n * regardless of build order or incremental compilation.\n */\nfunction generateClassName(cacheKey: string): string {\n const hash = createHash('md5').update(cacheKey).digest('hex').slice(0, 6);\n return `ts${hash}`; // 'ts' prefix for \"tasty-static\" to distinguish from runtime 't' classes\n}\n\n/**\n * Extract styles using chunking (for className mode).\n * Returns multiple classes, one per chunk.\n */\nexport function extractStylesWithChunks(styles: Styles): ExtractedChunk[] {\n const chunks: ExtractedChunk[] = [];\n\n // Categorize style keys into chunks\n const chunkMap = categorizeStyleKeys(styles as Record<string, unknown>);\n\n for (const [chunkName, chunkStyleKeys] of chunkMap) {\n if (chunkStyleKeys.length === 0) continue;\n\n // Generate cache key for this chunk (used for className hash)\n const cacheKey = generateChunkCacheKey(styles, chunkName, chunkStyleKeys);\n\n // Render styles for this chunk\n const renderResult = renderStylesForChunk(\n styles,\n chunkName,\n chunkStyleKeys,\n );\n\n if (renderResult.rules.length === 0) continue;\n\n // Generate deterministic className from content hash\n const className = generateClassName(cacheKey);\n const selector = `.${className}.${className}`;\n\n // Format CSS\n const css = formatRulesToCSS(renderResult.rules, selector);\n\n chunks.push({ className, css });\n }\n\n return chunks;\n}\n\n/**\n * Extract styles for a specific selector (for global/selector mode).\n * Returns a single CSS block.\n */\nexport function extractStylesForSelector(\n selector: string,\n styles: Styles,\n): ExtractedSelector {\n // renderStyles with selector returns StyleResult[] with selectors already applied\n const rules = renderStyles(styles, selector);\n // Format without re-prefixing - rules already have the full selector\n const css = formatRulesDirectly(rules);\n\n return { selector, css };\n}\n\n/**\n * Format StyleResult[] to CSS string.\n * Prefixes each rule's selector with the base selector.\n * Used for chunked styles where rules have relative selectors.\n */\nfunction formatRulesToCSS(rules: StyleResult[], baseSelector: string): string {\n return rules\n .map((rule) => {\n // Handle selector as array (OR conditions) or string\n // Note: renderStyles without className joins array selectors with '|||' placeholder\n const selectorParts = Array.isArray(rule.selector)\n ? rule.selector\n : rule.selector\n ? rule.selector.split('|||')\n : [''];\n\n // Prefix each selector part with the base selector\n const fullSelector = selectorParts\n .map((part) => {\n // Build selector: [rootPrefix] baseSelector[part]\n let selector: string;\n\n // If part is empty, just use base selector\n if (!part) {\n selector = baseSelector;\n } else if (part.startsWith(':') || part.startsWith('[')) {\n // If part starts with a pseudo-class or pseudo-element, append to base\n selector = `${baseSelector}${part}`;\n } else if (\n part.startsWith('>') ||\n part.startsWith('+') ||\n part.startsWith('~')\n ) {\n // If part starts with >, +, ~ combinator, append with space\n selector = `${baseSelector}${part}`;\n } else {\n // Otherwise, combine base with part\n selector = `${baseSelector}${part}`;\n }\n\n // Prepend rootPrefix if present (for @root() states)\n if (rule.rootPrefix) {\n selector = `${rule.rootPrefix} ${selector}`;\n }\n\n return selector;\n })\n .join(', ');\n\n let css = `${fullSelector} { ${rule.declarations} }`;\n\n // Wrap in at-rules (in reverse order for proper nesting)\n if (rule.atRules && rule.atRules.length > 0) {\n for (const atRule of [...rule.atRules].reverse()) {\n css = `${atRule} {\\n ${css}\\n}`;\n }\n }\n\n return css;\n })\n .join('\\n\\n');\n}\n\n/**\n * Format StyleResult[] to CSS string directly without prefixing.\n * Used for global styles where rules already have the full selector.\n */\nfunction formatRulesDirectly(rules: StyleResult[]): string {\n return rules\n .map((rule) => {\n // Prepend rootPrefix if present (for @root() states)\n const selector = rule.rootPrefix\n ? `${rule.rootPrefix} ${rule.selector}`\n : rule.selector;\n\n let css = `${selector} { ${rule.declarations} }`;\n\n // Wrap in at-rules (in reverse order for proper nesting)\n if (rule.atRules && rule.atRules.length > 0) {\n for (const atRule of [...rule.atRules].reverse()) {\n css = `${atRule} {\\n ${css}\\n}`;\n }\n }\n\n return css;\n })\n .join('\\n\\n');\n}\n\n// Note: With hash-based className generation, counter management functions\n// are no longer needed. ClassNames are deterministic based on content.\n\n/**\n * Generate a deterministic keyframes name from content hash.\n * This ensures the same keyframes content always produces the same name,\n * enabling automatic deduplication across elements and files.\n */\nfunction generateKeyframesName(steps: KeyframesSteps): string {\n const content = JSON.stringify(steps);\n const hash = createHash('md5').update(content).digest('hex').slice(0, 6);\n return `kf${hash}`; // 'kf' prefix for \"keyframes\"\n}\n\n/**\n * Extract keyframes that are used in styles.\n * Merges local @keyframes with global keyframes, filters to only used ones.\n * Generates hash-based names from content for automatic deduplication.\n *\n * @param styles - The styles object (may contain @keyframes and animation properties)\n * @param globalKeyframes - Optional global keyframes from config\n * @returns Keyframes to inject and name mapping for replacement\n */\nexport function extractKeyframesFromStyles(\n styles: Styles,\n globalKeyframes?: Record<string, KeyframesSteps> | null,\n): KeyframesExtractionResult {\n const emptyResult: KeyframesExtractionResult = {\n keyframes: [],\n nameMap: new Map(),\n };\n\n // Extract animation names from styles\n const usedNames = extractAnimationNamesFromStyles(styles);\n if (usedNames.size === 0) return emptyResult;\n\n // Merge local and global keyframes\n const local = hasLocalKeyframes(styles)\n ? extractLocalKeyframes(styles)\n : null;\n const allKeyframes = mergeKeyframes(local, globalKeyframes ?? null);\n\n // Filter to only used keyframes\n const usedKeyframes = filterUsedKeyframes(allKeyframes, usedNames);\n if (!usedKeyframes) return emptyResult;\n\n // Generate hash-based names and collect unique keyframes\n const seenHashes = new Set<string>();\n const nameMap = new Map<string, string>();\n const keyframesToEmit: ExtractedKeyframes[] = [];\n\n for (const [originalName, steps] of Object.entries(usedKeyframes)) {\n const hashedName = generateKeyframesName(steps);\n\n // Always map original name to hashed name (for CSS replacement)\n nameMap.set(originalName, hashedName);\n\n // Only emit each unique keyframe once\n if (!seenHashes.has(hashedName)) {\n seenHashes.add(hashedName);\n const css = keyframesToCSS(hashedName, steps);\n keyframesToEmit.push({ name: hashedName, css });\n }\n }\n\n return { keyframes: keyframesToEmit, nameMap };\n}\n\n/**\n * Convert keyframes steps to CSS string.\n */\nfunction keyframesToCSS(name: string, steps: KeyframesSteps): string {\n const stepRules: string[] = [];\n\n for (const [key, value] of Object.entries(steps)) {\n if (typeof value === 'string') {\n // Raw CSS string\n stepRules.push(`${key} { ${value.trim()} }`);\n } else if (value && typeof value === 'object') {\n // Style map - convert to CSS declarations\n const declarations = Object.entries(value)\n .map(([prop, val]) => {\n const cssProperty = camelToKebab(prop);\n return `${cssProperty}: ${val}`;\n })\n .join('; ');\n stepRules.push(`${key} { ${declarations} }`);\n }\n }\n\n return `@keyframes ${name} { ${stepRules.join(' ')} }`;\n}\n\n/**\n * Convert camelCase to kebab-case.\n */\nfunction camelToKebab(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);\n}\n"],"mappings":"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"}
@@ -1,404 +0,0 @@
1
- import { parseStyle } from "../utils/styles.mjs";
2
- import { colorInitialValueToRgb, getEffectiveDefinition, normalizePropertyDefinition } from "../properties/index.mjs";
3
- import { isDevEnv } from "../utils/is-dev-env.mjs";
4
- import { SheetManager } from "./sheet-manager.mjs";
5
-
6
- //#region src/injector/injector.ts
7
- /**
8
- * Generate sequential class name with format t{number}
9
- */
10
- function generateClassName(counter) {
11
- return `t${counter}`;
12
- }
13
- var StyleInjector = class {
14
- sheetManager;
15
- config;
16
- cleanupScheduled = false;
17
- globalRuleCounter = 0;
18
- /** @internal — exposed for debug utilities only */
19
- get _sheetManager() {
20
- return this.sheetManager;
21
- }
22
- constructor(config = {}) {
23
- this.config = config;
24
- this.sheetManager = new SheetManager(config);
25
- }
26
- /**
27
- * Allocate a className for a cacheKey without injecting styles yet.
28
- * This allows separating className allocation (render phase) from style injection (insertion phase).
29
- */
30
- allocateClassName(cacheKey, options) {
31
- const root = options?.root || document;
32
- const registry = this.sheetManager.getRegistry(root);
33
- if (registry.cacheKeyToClassName.has(cacheKey)) return {
34
- className: registry.cacheKeyToClassName.get(cacheKey),
35
- isNewAllocation: false
36
- };
37
- const className = generateClassName(registry.classCounter++);
38
- const placeholderRuleInfo = {
39
- className,
40
- ruleIndex: -1,
41
- sheetIndex: -1
42
- };
43
- registry.rules.set(className, placeholderRuleInfo);
44
- registry.cacheKeyToClassName.set(cacheKey, className);
45
- return {
46
- className,
47
- isNewAllocation: true
48
- };
49
- }
50
- /**
51
- * Inject styles from StyleResult objects
52
- */
53
- inject(rules, options) {
54
- const root = options?.root || document;
55
- const registry = this.sheetManager.getRegistry(root);
56
- if (rules.length === 0) return {
57
- className: "",
58
- dispose: () => {}
59
- };
60
- const cacheKey = options?.cacheKey;
61
- let className;
62
- let isPreAllocated = false;
63
- if (cacheKey && registry.cacheKeyToClassName.has(cacheKey)) {
64
- className = registry.cacheKeyToClassName.get(cacheKey);
65
- const existingRuleInfo = registry.rules.get(className);
66
- isPreAllocated = existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1;
67
- if (!isPreAllocated) {
68
- const currentRefCount = registry.refCounts.get(className) || 0;
69
- registry.refCounts.set(className, currentRefCount + 1);
70
- if (registry.metrics) registry.metrics.hits++;
71
- return {
72
- className,
73
- dispose: () => this.dispose(className, registry)
74
- };
75
- }
76
- } else className = generateClassName(registry.classCounter++);
77
- const rulesToInsert = rules.map((rule) => {
78
- let newSelector = rule.selector;
79
- if (rule.needsClassName) {
80
- const selectorParts = newSelector ? newSelector.split("|||") : [""];
81
- const classPrefix = `.${className}.${className}`;
82
- newSelector = selectorParts.map((part) => {
83
- const classSelector = part ? `${classPrefix}${part}` : classPrefix;
84
- if (rule.rootPrefix) return `${rule.rootPrefix} ${classSelector}`;
85
- return classSelector;
86
- }).join(", ");
87
- }
88
- return {
89
- ...rule,
90
- selector: newSelector,
91
- needsClassName: void 0,
92
- rootPrefix: void 0
93
- };
94
- });
95
- const colorPropRegex = /^\s*(--[a-z0-9-]+-color)\s*:/i;
96
- for (const rule of rulesToInsert) {
97
- if (!rule.declarations) continue;
98
- const parts = rule.declarations.split(/;+\s*/);
99
- for (const part of parts) {
100
- if (!part) continue;
101
- const match = colorPropRegex.exec(part);
102
- if (match) {
103
- const propName = match[1];
104
- if (!this.isPropertyDefined(propName, { root })) this.property(propName, {
105
- syntax: "<color>",
106
- initialValue: "transparent",
107
- root
108
- });
109
- }
110
- }
111
- }
112
- const ruleInfo = this.sheetManager.insertRule(registry, rulesToInsert, className, root);
113
- if (!ruleInfo) {
114
- if (registry.metrics) registry.metrics.misses++;
115
- return {
116
- className,
117
- dispose: () => {}
118
- };
119
- }
120
- registry.refCounts.set(className, 1);
121
- if (isPreAllocated) registry.rules.set(className, ruleInfo);
122
- else {
123
- registry.rules.set(className, ruleInfo);
124
- if (cacheKey) registry.cacheKeyToClassName.set(cacheKey, className);
125
- }
126
- if (registry.metrics) {
127
- registry.metrics.totalInsertions++;
128
- registry.metrics.misses++;
129
- }
130
- return {
131
- className,
132
- dispose: () => this.dispose(className, registry)
133
- };
134
- }
135
- /**
136
- * Inject global styles (rules without a generated tasty class selector)
137
- * This ensures we don't reserve a tasty class name (t{number}) for global rules,
138
- * which could otherwise collide with element-level styles and break lookups.
139
- */
140
- injectGlobal(rules, options) {
141
- const root = options?.root || document;
142
- const registry = this.sheetManager.getRegistry(root);
143
- if (!rules || rules.length === 0) return { dispose: () => {} };
144
- const key = `global:${this.globalRuleCounter++}`;
145
- const info = this.sheetManager.insertGlobalRule(registry, rules, key, root);
146
- if (registry.metrics) registry.metrics.totalInsertions++;
147
- return { dispose: () => {
148
- if (info) this.sheetManager.deleteGlobalRule(registry, key);
149
- } };
150
- }
151
- /**
152
- * Inject raw CSS text directly without parsing
153
- * This is a low-overhead alternative to createGlobalStyle for raw CSS
154
- * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking
155
- */
156
- injectRawCSS(css, options) {
157
- const root = options?.root || document;
158
- return this.sheetManager.injectRawCSS(css, root);
159
- }
160
- /**
161
- * Get raw CSS text for SSR
162
- */
163
- getRawCSSText(options) {
164
- const root = options?.root || document;
165
- return this.sheetManager.getRawCSSText(root);
166
- }
167
- /**
168
- * Dispose of a className
169
- */
170
- dispose(className, registry) {
171
- const currentRefCount = registry.refCounts.get(className);
172
- if (currentRefCount == null || currentRefCount <= 0) return;
173
- const newRefCount = currentRefCount - 1;
174
- registry.refCounts.set(className, newRefCount);
175
- if (newRefCount === 0) {
176
- if (registry.metrics) registry.metrics.totalUnused++;
177
- this.sheetManager.checkCleanupNeeded(registry);
178
- }
179
- }
180
- /**
181
- * Force bulk cleanup of unused styles
182
- */
183
- cleanup(root) {
184
- const registry = this.sheetManager.getRegistry(root || document);
185
- this.sheetManager.forceCleanup(registry);
186
- }
187
- /**
188
- * Get CSS text from all sheets (for SSR)
189
- */
190
- getCssText(options) {
191
- const root = options?.root || document;
192
- const registry = this.sheetManager.getRegistry(root);
193
- return this.sheetManager.getCssText(registry);
194
- }
195
- /**
196
- * Get CSS only for the provided tasty classNames (e.g., ["t0","t3"])
197
- */
198
- getCssTextForClasses(classNames, options) {
199
- const root = options?.root || document;
200
- const registry = this.sheetManager.getRegistry(root);
201
- const cssChunks = [];
202
- for (const cls of classNames) {
203
- const info = registry.rules.get(cls);
204
- if (info) {
205
- const styleSheet = registry.sheets[info.sheetIndex]?.sheet?.sheet;
206
- if (styleSheet) {
207
- const start = Math.max(0, info.ruleIndex);
208
- const end = Math.min(styleSheet.cssRules.length - 1, info.endRuleIndex ?? info.ruleIndex);
209
- if (start >= 0 && end >= start && start < styleSheet.cssRules.length) for (let i = start; i <= end; i++) {
210
- const rule = styleSheet.cssRules[i];
211
- if (rule) cssChunks.push(rule.cssText);
212
- }
213
- } else if (info.cssText && info.cssText.length) cssChunks.push(...info.cssText);
214
- }
215
- }
216
- return cssChunks.join("\n");
217
- }
218
- /**
219
- * Get cache performance metrics
220
- */
221
- getMetrics(options) {
222
- const root = options?.root || document;
223
- const registry = this.sheetManager.getRegistry(root);
224
- return this.sheetManager.getMetrics(registry);
225
- }
226
- /**
227
- * Reset cache performance metrics
228
- */
229
- resetMetrics(options) {
230
- const root = options?.root || document;
231
- const registry = this.sheetManager.getRegistry(root);
232
- this.sheetManager.resetMetrics(registry);
233
- }
234
- /**
235
- * Define a CSS @property custom property.
236
- *
237
- * Accepts tasty token syntax for the property name:
238
- * - `$name` → defines `--name`
239
- * - `#name` → defines `--name-color` (auto-sets syntax: '<color>', defaults initialValue: 'transparent')
240
- * - `--name` → defines `--name` (legacy format)
241
- *
242
- * Example:
243
- * @property --rotation { syntax: "<angle>"; inherits: false; initial-value: 45deg; }
244
- *
245
- * Note: No caching or dispose — this defines a global property.
246
- *
247
- * If the same property is registered with different options, a warning is emitted
248
- * but the original definition is preserved (CSS @property cannot be redefined).
249
- */
250
- property(name, options) {
251
- const root = options?.root || document;
252
- const registry = this.sheetManager.getRegistry(root);
253
- const effectiveResult = getEffectiveDefinition(name, {
254
- syntax: options?.syntax,
255
- inherits: options?.inherits,
256
- initialValue: options?.initialValue
257
- });
258
- if (!effectiveResult.isValid) {
259
- if (isDevEnv()) console.warn(`[Tasty] property(): ${effectiveResult.error}. Got: "${name}"`);
260
- return;
261
- }
262
- const cssName = effectiveResult.cssName;
263
- const definition = effectiveResult.definition;
264
- const normalizedDef = normalizePropertyDefinition(definition);
265
- const existingDef = registry.injectedProperties.get(cssName);
266
- if (existingDef !== void 0) {
267
- if (existingDef !== normalizedDef) {
268
- if (isDevEnv()) console.warn(`[Tasty] @property ${cssName} was already defined with a different declaration. The new declaration will be ignored. Original: ${existingDef}, New: ${normalizedDef}`);
269
- }
270
- return;
271
- }
272
- const parts = [];
273
- if (definition.syntax != null) {
274
- let syntax = String(definition.syntax).trim();
275
- if (!/^['"]/u.test(syntax)) syntax = `"${syntax}"`;
276
- parts.push(`syntax: ${syntax};`);
277
- }
278
- const inherits = definition.inherits ?? true;
279
- parts.push(`inherits: ${inherits ? "true" : "false"};`);
280
- if (definition.initialValue != null) {
281
- let initialValueStr;
282
- if (typeof definition.initialValue === "number") initialValueStr = String(definition.initialValue);
283
- else initialValueStr = parseStyle(definition.initialValue).output;
284
- parts.push(`initial-value: ${initialValueStr};`);
285
- }
286
- const declarations = parts.join(" ").trim();
287
- const rule = {
288
- selector: `@property ${cssName}`,
289
- declarations
290
- };
291
- if (!this.sheetManager.insertGlobalRule(registry, [rule], `property:${name}`, root)) return;
292
- registry.injectedProperties.set(cssName, normalizedDef);
293
- if (effectiveResult.isColor) {
294
- const rgbCssName = `${cssName}-rgb`;
295
- if (!this.isPropertyDefined(rgbCssName, { root })) this.property(rgbCssName, {
296
- syntax: "<number>+",
297
- inherits: definition.inherits,
298
- initialValue: colorInitialValueToRgb(definition.initialValue),
299
- root
300
- });
301
- }
302
- }
303
- /**
304
- * Check whether a given @property name was already injected by this injector.
305
- *
306
- * Accepts tasty token syntax:
307
- * - `$name` → checks `--name`
308
- * - `#name` → checks `--name-color`
309
- * - `--name` → checks `--name` (legacy format)
310
- */
311
- isPropertyDefined(name, options) {
312
- const root = options?.root || document;
313
- const registry = this.sheetManager.getRegistry(root);
314
- const effectiveResult = getEffectiveDefinition(name, {});
315
- if (!effectiveResult.isValid) return false;
316
- return registry.injectedProperties.has(effectiveResult.cssName);
317
- }
318
- /**
319
- * Inject keyframes and return object with toString() and dispose()
320
- *
321
- * Keyframes are cached by content (steps). If the same content is injected
322
- * multiple times with different provided names, the first injected name is reused.
323
- *
324
- * If the same name is provided with different content (collision), a unique
325
- * name is generated to avoid overwriting the existing keyframes.
326
- */
327
- keyframes(steps, nameOrOptions) {
328
- const isStringName = typeof nameOrOptions === "string";
329
- const providedName = isStringName ? nameOrOptions : nameOrOptions?.name;
330
- const root = isStringName ? document : nameOrOptions?.root || document;
331
- const registry = this.sheetManager.getRegistry(root);
332
- if (Object.keys(steps).length === 0) return {
333
- toString: () => "",
334
- dispose: () => {}
335
- };
336
- const contentHash = JSON.stringify(steps);
337
- const existing = registry.keyframesCache.get(contentHash);
338
- if (existing) {
339
- existing.refCount++;
340
- return {
341
- toString: () => existing.name,
342
- dispose: () => this.disposeKeyframes(contentHash, registry)
343
- };
344
- }
345
- let actualName;
346
- if (providedName) {
347
- const existingContentForName = registry.keyframesNameToContent.get(providedName);
348
- if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-k${registry.keyframesCounter++}`;
349
- else {
350
- actualName = providedName;
351
- registry.keyframesNameToContent.set(providedName, contentHash);
352
- }
353
- } else actualName = `k${registry.keyframesCounter++}`;
354
- const info = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
355
- if (!info) return {
356
- toString: () => "",
357
- dispose: () => {}
358
- };
359
- registry.keyframesCache.set(contentHash, {
360
- name: actualName,
361
- refCount: 1,
362
- info
363
- });
364
- if (registry.metrics) {
365
- registry.metrics.totalInsertions++;
366
- registry.metrics.misses++;
367
- }
368
- return {
369
- toString: () => actualName,
370
- dispose: () => this.disposeKeyframes(contentHash, registry)
371
- };
372
- }
373
- /**
374
- * Dispose keyframes
375
- */
376
- disposeKeyframes(contentHash, registry) {
377
- const entry = registry.keyframesCache.get(contentHash);
378
- if (!entry) return;
379
- entry.refCount--;
380
- if (entry.refCount <= 0) {
381
- this.sheetManager.deleteKeyframes(registry, entry.info);
382
- registry.keyframesCache.delete(contentHash);
383
- for (const [name, hash] of registry.keyframesNameToContent.entries()) if (hash === contentHash) {
384
- registry.keyframesNameToContent.delete(name);
385
- break;
386
- }
387
- if (registry.metrics) {
388
- registry.metrics.totalUnused++;
389
- registry.metrics.stylesCleanedUp++;
390
- }
391
- }
392
- }
393
- /**
394
- * Destroy all resources for a root
395
- */
396
- destroy(root) {
397
- const targetRoot = root || document;
398
- this.sheetManager.cleanup(targetRoot);
399
- }
400
- };
401
-
402
- //#endregion
403
- export { StyleInjector };
404
- //# sourceMappingURL=injector.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"injector.mjs","names":[],"sources":["../../src/injector/injector.ts"],"sourcesContent":["/**\n * Style injector that works with structured style objects\n * Eliminates CSS string parsing for better performance\n */\n\nimport type { StyleResult } from '../pipeline';\nimport {\n colorInitialValueToRgb,\n getEffectiveDefinition,\n normalizePropertyDefinition,\n} from '../properties';\nimport { isDevEnv } from '../utils/is-dev-env';\nimport type { StyleValue } from '../utils/styles';\nimport { parseStyle } from '../utils/styles';\n\nimport { SheetManager } from './sheet-manager';\nimport type {\n CacheMetrics,\n GlobalInjectResult,\n InjectResult,\n KeyframesResult,\n KeyframesSteps,\n PropertyDefinition,\n RawCSSResult,\n RootRegistry,\n StyleInjectorConfig,\n StyleRule,\n} from './types';\n\n/**\n * Generate sequential class name with format t{number}\n */\nfunction generateClassName(counter: number): string {\n return `t${counter}`;\n}\n\nexport class StyleInjector {\n private sheetManager: SheetManager;\n private config: StyleInjectorConfig;\n private cleanupScheduled = false;\n private globalRuleCounter = 0;\n\n /** @internal — exposed for debug utilities only */\n get _sheetManager(): SheetManager {\n return this.sheetManager;\n }\n\n constructor(config: StyleInjectorConfig = {}) {\n this.config = config;\n this.sheetManager = new SheetManager(config);\n }\n\n /**\n * Allocate a className for a cacheKey without injecting styles yet.\n * This allows separating className allocation (render phase) from style injection (insertion phase).\n */\n allocateClassName(\n cacheKey: string,\n options?: { root?: Document | ShadowRoot },\n ): { className: string; isNewAllocation: boolean } {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n // Check if we can reuse existing className for this cache key\n if (registry.cacheKeyToClassName.has(cacheKey)) {\n const className = registry.cacheKeyToClassName.get(cacheKey)!;\n return {\n className,\n isNewAllocation: false,\n };\n }\n\n // Generate new className and reserve it\n const className = generateClassName(registry.classCounter++);\n\n // Create placeholder RuleInfo to reserve the className\n const placeholderRuleInfo = {\n className,\n ruleIndex: -1, // Placeholder - will be set during actual injection\n sheetIndex: -1, // Placeholder - will be set during actual injection\n };\n\n // Store RuleInfo only once by className, and map cacheKey separately\n registry.rules.set(className, placeholderRuleInfo);\n registry.cacheKeyToClassName.set(cacheKey, className);\n\n return {\n className,\n isNewAllocation: true,\n };\n }\n\n /**\n * Inject styles from StyleResult objects\n */\n inject(\n rules: StyleResult[],\n options?: { root?: Document | ShadowRoot; cacheKey?: string },\n ): InjectResult {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n if (rules.length === 0) {\n return {\n className: '',\n dispose: () => { /* noop */ },\n };\n }\n\n // Rules are now in StyleRule format directly\n\n // Check if we can reuse based on cache key\n const cacheKey = options?.cacheKey;\n let className: string;\n let isPreAllocated = false;\n\n if (cacheKey && registry.cacheKeyToClassName.has(cacheKey)) {\n // Reuse existing class for this cache key\n className = registry.cacheKeyToClassName.get(cacheKey)!;\n const existingRuleInfo = registry.rules.get(className)!;\n\n // Check if this is a placeholder (pre-allocated but not yet injected)\n isPreAllocated =\n existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1;\n\n if (!isPreAllocated) {\n // Already injected - just increment refCount\n const currentRefCount = registry.refCounts.get(className) || 0;\n registry.refCounts.set(className, currentRefCount + 1);\n\n // Update metrics\n if (registry.metrics) {\n registry.metrics.hits++;\n }\n\n return {\n className,\n dispose: () => this.dispose(className, registry),\n };\n }\n } else {\n // Generate new className\n className = generateClassName(registry.classCounter++);\n }\n\n // Process rules: handle needsClassName flag and apply specificity\n const rulesToInsert = rules.map((rule) => {\n let newSelector = rule.selector;\n\n // If rule needs className prepended\n if (rule.needsClassName) {\n // Handle multiple selectors (separated by ||| for OR conditions)\n const selectorParts = newSelector ? newSelector.split('|||') : [''];\n\n const classPrefix = `.${className}.${className}`;\n\n newSelector = selectorParts\n .map((part) => {\n const classSelector = part ? `${classPrefix}${part}` : classPrefix;\n\n // If there's a root prefix, add it before the class selector\n if (rule.rootPrefix) {\n return `${rule.rootPrefix} ${classSelector}`;\n }\n return classSelector;\n })\n .join(', ');\n }\n\n return {\n ...rule,\n selector: newSelector,\n needsClassName: undefined, // Remove the flag after processing\n rootPrefix: undefined, // Remove rootPrefix after processing\n };\n });\n\n // Before inserting, auto-register @property for any color custom properties being defined.\n // Fast parse: split declarations by ';' and match \"--*-color:\"\n // Do this only when we actually insert (i.e., no cache hit above)\n const colorPropRegex = /^\\s*(--[a-z0-9-]+-color)\\s*:/i;\n for (const rule of rulesToInsert) {\n // Skip if no declarations\n if (!rule.declarations) continue;\n const parts = rule.declarations.split(/;+\\s*/);\n for (const part of parts) {\n if (!part) continue;\n const match = colorPropRegex.exec(part);\n if (match) {\n const propName = match[1];\n // Register @property only if not already defined for this root\n if (!this.isPropertyDefined(propName, { root })) {\n this.property(propName, {\n syntax: '<color>',\n initialValue: 'transparent',\n root,\n });\n }\n }\n }\n }\n\n // Insert rules using existing sheet manager\n const ruleInfo = this.sheetManager.insertRule(\n registry,\n rulesToInsert,\n className,\n root,\n );\n\n if (!ruleInfo) {\n // Update metrics\n if (registry.metrics) {\n registry.metrics.misses++;\n }\n\n return {\n className,\n dispose: () => { /* noop */ },\n };\n }\n\n // Store in registry\n registry.refCounts.set(className, 1);\n\n if (isPreAllocated) {\n // Update the existing placeholder entry with real rule info\n registry.rules.set(className, ruleInfo);\n // cacheKey mapping already exists from allocation\n } else {\n // Store new entries\n registry.rules.set(className, ruleInfo);\n if (cacheKey) {\n registry.cacheKeyToClassName.set(cacheKey, className);\n }\n }\n\n // Update metrics\n if (registry.metrics) {\n registry.metrics.totalInsertions++;\n registry.metrics.misses++;\n }\n\n return {\n className,\n dispose: () => this.dispose(className, registry),\n };\n }\n\n /**\n * Inject global styles (rules without a generated tasty class selector)\n * This ensures we don't reserve a tasty class name (t{number}) for global rules,\n * which could otherwise collide with element-level styles and break lookups.\n */\n injectGlobal(\n rules: StyleResult[],\n options?: { root?: Document | ShadowRoot },\n ): GlobalInjectResult {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n if (!rules || rules.length === 0) {\n return { dispose: () => { /* noop */ } };\n }\n\n // Use a non-tasty identifier to avoid any collisions with .t{number} classes\n const key = `global:${this.globalRuleCounter++}`;\n\n const info = this.sheetManager.insertGlobalRule(\n registry,\n rules as unknown as StyleRule[],\n key,\n root,\n );\n\n if (registry.metrics) {\n registry.metrics.totalInsertions++;\n }\n\n return {\n dispose: () => {\n if (info) this.sheetManager.deleteGlobalRule(registry, key);\n },\n };\n }\n\n /**\n * Inject raw CSS text directly without parsing\n * This is a low-overhead alternative to createGlobalStyle for raw CSS\n * The CSS is inserted into a separate style element to avoid conflicts with tasty's chunking\n */\n injectRawCSS(\n css: string,\n options?: { root?: Document | ShadowRoot },\n ): RawCSSResult {\n const root = options?.root || document;\n return this.sheetManager.injectRawCSS(css, root);\n }\n\n /**\n * Get raw CSS text for SSR\n */\n getRawCSSText(options?: { root?: Document | ShadowRoot }): string {\n const root = options?.root || document;\n return this.sheetManager.getRawCSSText(root);\n }\n\n /**\n * Dispose of a className\n */\n private dispose(className: string, registry: RootRegistry): void {\n const currentRefCount = registry.refCounts.get(className);\n // Guard against stale double-dispose or mismatched lifecycle\n if (currentRefCount == null || currentRefCount <= 0) {\n return;\n }\n\n const newRefCount = currentRefCount - 1;\n registry.refCounts.set(className, newRefCount);\n\n if (newRefCount === 0) {\n // Update metrics\n if (registry.metrics) {\n registry.metrics.totalUnused++;\n }\n\n // Check if cleanup should be scheduled\n this.sheetManager.checkCleanupNeeded(registry);\n }\n }\n\n /**\n * Force bulk cleanup of unused styles\n */\n cleanup(root?: Document | ShadowRoot): void {\n const registry = this.sheetManager.getRegistry(root || document);\n // Clean up ALL unused rules regardless of batch ratio\n this.sheetManager.forceCleanup(registry);\n }\n\n /**\n * Get CSS text from all sheets (for SSR)\n */\n getCssText(options?: { root?: Document | ShadowRoot }): string {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n return this.sheetManager.getCssText(registry);\n }\n\n /**\n * Get CSS only for the provided tasty classNames (e.g., [\"t0\",\"t3\"])\n */\n getCssTextForClasses(\n classNames: Iterable<string>,\n options?: { root?: Document | ShadowRoot },\n ): string {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n const cssChunks: string[] = [];\n for (const cls of classNames) {\n const info = registry.rules.get(cls);\n if (info) {\n // Always prefer reading from the live stylesheet, since indices can change\n const sheet = registry.sheets[info.sheetIndex];\n const styleSheet = sheet?.sheet?.sheet;\n if (styleSheet) {\n const start = Math.max(0, info.ruleIndex);\n const end = Math.min(\n styleSheet.cssRules.length - 1,\n (info.endRuleIndex as number) ?? info.ruleIndex,\n );\n // Additional validation: ensure indices are valid and in correct order\n if (\n start >= 0 &&\n end >= start &&\n start < styleSheet.cssRules.length\n ) {\n for (let i = start; i <= end; i++) {\n const rule = styleSheet.cssRules[i] as CSSRule | undefined;\n if (rule) cssChunks.push(rule.cssText);\n }\n }\n } else if (info.cssText && info.cssText.length) {\n // Fallback in environments without CSSOM access\n cssChunks.push(...info.cssText);\n }\n }\n }\n return cssChunks.join('\\n');\n }\n\n /**\n * Get cache performance metrics\n */\n getMetrics(options?: { root?: Document | ShadowRoot }): CacheMetrics | null {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n return this.sheetManager.getMetrics(registry);\n }\n\n /**\n * Reset cache performance metrics\n */\n resetMetrics(options?: { root?: Document | ShadowRoot }): void {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n this.sheetManager.resetMetrics(registry);\n }\n\n /**\n * Define a CSS @property custom property.\n *\n * Accepts tasty token syntax for the property name:\n * - `$name` → defines `--name`\n * - `#name` → defines `--name-color` (auto-sets syntax: '<color>', defaults initialValue: 'transparent')\n * - `--name` → defines `--name` (legacy format)\n *\n * Example:\n * @property --rotation { syntax: \"<angle>\"; inherits: false; initial-value: 45deg; }\n *\n * Note: No caching or dispose — this defines a global property.\n *\n * If the same property is registered with different options, a warning is emitted\n * but the original definition is preserved (CSS @property cannot be redefined).\n */\n property(\n name: string,\n options?: PropertyDefinition & {\n root?: Document | ShadowRoot;\n },\n ): void {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n // Parse the token and get effective definition\n // This handles $name, #name, --name formats and auto-sets syntax for colors\n const userDefinition: PropertyDefinition = {\n syntax: options?.syntax,\n inherits: options?.inherits,\n initialValue: options?.initialValue,\n };\n\n const effectiveResult = getEffectiveDefinition(name, userDefinition);\n\n if (!effectiveResult.isValid) {\n if (isDevEnv()) {\n console.warn(\n `[Tasty] property(): ${effectiveResult.error}. Got: \"${name}\"`,\n );\n }\n return;\n }\n\n const cssName = effectiveResult.cssName;\n const definition = effectiveResult.definition;\n\n // Normalize the definition for comparison\n const normalizedDef = normalizePropertyDefinition(definition);\n\n // Check if already defined\n const existingDef = registry.injectedProperties.get(cssName);\n if (existingDef !== undefined) {\n // Property already exists - check if definitions match\n if (existingDef !== normalizedDef) {\n // Different definition - warn but don't replace (CSS @property can't be redefined)\n if (isDevEnv()) {\n console.warn(\n `[Tasty] @property ${cssName} was already defined with a different declaration. ` +\n `The new declaration will be ignored. ` +\n `Original: ${existingDef}, New: ${normalizedDef}`,\n );\n }\n }\n // Either exact match or warned - skip injection\n return;\n }\n\n const parts: string[] = [];\n\n if (definition.syntax != null) {\n let syntax = String(definition.syntax).trim();\n if (!/^['\"]/u.test(syntax)) syntax = `\"${syntax}\"`;\n parts.push(`syntax: ${syntax};`);\n }\n\n // inherits is required by the CSS @property spec - default to true\n const inherits = definition.inherits ?? true;\n parts.push(`inherits: ${inherits ? 'true' : 'false'};`);\n\n if (definition.initialValue != null) {\n let initialValueStr: string;\n if (typeof definition.initialValue === 'number') {\n initialValueStr = String(definition.initialValue);\n } else {\n // Process via tasty parser to resolve custom units/functions\n initialValueStr = parseStyle(definition.initialValue as StyleValue).output;\n }\n parts.push(`initial-value: ${initialValueStr};`);\n }\n\n const declarations = parts.join(' ').trim();\n\n const rule: StyleRule = {\n selector: `@property ${cssName}`,\n declarations,\n } as StyleRule;\n\n // Insert as a global rule; only mark injected when insertion succeeds\n const info = this.sheetManager.insertGlobalRule(\n registry,\n [rule],\n `property:${name}`,\n root,\n );\n\n if (!info) {\n return;\n }\n\n // Track that this property was injected with its normalized definition\n registry.injectedProperties.set(cssName, normalizedDef);\n\n // Auto-register companion -rgb property for color properties.\n // E.g. --white-color gets a companion --white-color-rgb with syntax: '<number>+'\n if (effectiveResult.isColor) {\n const rgbCssName = `${cssName}-rgb`;\n if (!this.isPropertyDefined(rgbCssName, { root })) {\n this.property(rgbCssName, {\n syntax: '<number>+',\n inherits: definition.inherits,\n initialValue: colorInitialValueToRgb(definition.initialValue),\n root,\n });\n }\n }\n }\n\n /**\n * Check whether a given @property name was already injected by this injector.\n *\n * Accepts tasty token syntax:\n * - `$name` → checks `--name`\n * - `#name` → checks `--name-color`\n * - `--name` → checks `--name` (legacy format)\n */\n isPropertyDefined(\n name: string,\n options?: { root?: Document | ShadowRoot },\n ): boolean {\n const root = options?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n // Parse the token to get the CSS property name\n const effectiveResult = getEffectiveDefinition(name, {});\n if (!effectiveResult.isValid) {\n return false;\n }\n\n return registry.injectedProperties.has(effectiveResult.cssName);\n }\n\n /**\n * Inject keyframes and return object with toString() and dispose()\n *\n * Keyframes are cached by content (steps). If the same content is injected\n * multiple times with different provided names, the first injected name is reused.\n *\n * If the same name is provided with different content (collision), a unique\n * name is generated to avoid overwriting the existing keyframes.\n */\n keyframes(\n steps: KeyframesSteps,\n nameOrOptions?: string | { root?: Document | ShadowRoot; name?: string },\n ): KeyframesResult {\n // Parse parameters\n const isStringName = typeof nameOrOptions === 'string';\n const providedName = isStringName ? nameOrOptions : nameOrOptions?.name;\n const root = isStringName ? document : nameOrOptions?.root || document;\n const registry = this.sheetManager.getRegistry(root);\n\n if (Object.keys(steps).length === 0) {\n return {\n toString: () => '',\n dispose: () => { /* noop */ },\n };\n }\n\n // Generate content-based cache key (independent of provided name)\n const contentHash = JSON.stringify(steps);\n\n // Check if this exact content is already cached\n const existing = registry.keyframesCache.get(contentHash);\n if (existing) {\n existing.refCount++;\n return {\n toString: () => existing.name,\n dispose: () => this.disposeKeyframes(contentHash, registry),\n };\n }\n\n // Determine the actual name to use\n let actualName: string;\n\n if (providedName) {\n // Check if this name is already used with different content\n const existingContentForName =\n registry.keyframesNameToContent.get(providedName);\n\n if (existingContentForName && existingContentForName !== contentHash) {\n // Name collision: same name, different content\n // Generate a unique name to avoid overwriting\n actualName = `${providedName}-k${registry.keyframesCounter++}`;\n } else {\n // Name is available or used with same content\n actualName = providedName;\n // Track this name -> content mapping\n registry.keyframesNameToContent.set(providedName, contentHash);\n }\n } else {\n // No name provided, generate one\n actualName = `k${registry.keyframesCounter++}`;\n }\n\n // Insert keyframes\n const info = this.sheetManager.insertKeyframes(\n registry,\n steps,\n actualName,\n root,\n );\n if (!info) {\n return {\n toString: () => '',\n dispose: () => { /* noop */ },\n };\n }\n\n // Cache the result by content hash\n registry.keyframesCache.set(contentHash, {\n name: actualName,\n refCount: 1,\n info,\n });\n\n // Update metrics\n if (registry.metrics) {\n registry.metrics.totalInsertions++;\n registry.metrics.misses++;\n }\n\n return {\n toString: () => actualName,\n dispose: () => this.disposeKeyframes(contentHash, registry),\n };\n }\n\n /**\n * Dispose keyframes\n */\n private disposeKeyframes(contentHash: string, registry: RootRegistry): void {\n const entry = registry.keyframesCache.get(contentHash);\n if (!entry) return;\n\n entry.refCount--;\n if (entry.refCount <= 0) {\n // Dispose immediately - keyframes are global and safe to clean up right away\n this.sheetManager.deleteKeyframes(registry, entry.info);\n registry.keyframesCache.delete(contentHash);\n\n // Clean up name-to-content mapping if this name was tracked\n // Find and remove the mapping for this content hash\n for (const [name, hash] of registry.keyframesNameToContent.entries()) {\n if (hash === contentHash) {\n registry.keyframesNameToContent.delete(name);\n break;\n }\n }\n\n // Update metrics\n if (registry.metrics) {\n registry.metrics.totalUnused++;\n registry.metrics.stylesCleanedUp++;\n }\n }\n }\n\n /**\n * Destroy all resources for a root\n */\n destroy(root?: Document | ShadowRoot): void {\n const targetRoot = root || document;\n this.sheetManager.cleanup(targetRoot);\n }\n}\n"],"mappings":"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"}