@tenphi/tasty 0.0.0-snapshot.26b4630 → 0.0.0-snapshot.2705ab9

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 (218) hide show
  1. package/README.md +315 -159
  2. package/dist/chunks/cacheKey.js +16 -8
  3. package/dist/chunks/cacheKey.js.map +1 -1
  4. package/dist/chunks/definitions.d.ts +1 -1
  5. package/dist/chunks/definitions.js +2 -3
  6. package/dist/chunks/definitions.js.map +1 -1
  7. package/dist/chunks/renderChunk.js +31 -32
  8. package/dist/chunks/renderChunk.js.map +1 -1
  9. package/dist/config.d.ts +98 -12
  10. package/dist/config.js +134 -33
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/index.d.ts +11 -10
  13. package/dist/core/index.js +8 -7
  14. package/dist/counter-style/index.js +51 -0
  15. package/dist/counter-style/index.js.map +1 -0
  16. package/dist/debug.d.ts +26 -141
  17. package/dist/debug.js +356 -635
  18. package/dist/debug.js.map +1 -1
  19. package/dist/font-face/index.js +63 -0
  20. package/dist/font-face/index.js.map +1 -0
  21. package/dist/hooks/resolve-ssr-collector.js +15 -0
  22. package/dist/hooks/resolve-ssr-collector.js.map +1 -0
  23. package/dist/hooks/useCounterStyle.d.ts +50 -0
  24. package/dist/hooks/useCounterStyle.js +47 -0
  25. package/dist/hooks/useCounterStyle.js.map +1 -0
  26. package/dist/hooks/useFontFace.d.ts +43 -0
  27. package/dist/hooks/useFontFace.js +71 -0
  28. package/dist/hooks/useFontFace.js.map +1 -0
  29. package/dist/hooks/useGlobalStyles.d.ts +3 -0
  30. package/dist/hooks/useGlobalStyles.js +24 -1
  31. package/dist/hooks/useGlobalStyles.js.map +1 -1
  32. package/dist/hooks/useKeyframes.js +14 -3
  33. package/dist/hooks/useKeyframes.js.map +1 -1
  34. package/dist/hooks/useProperty.js +32 -13
  35. package/dist/hooks/useProperty.js.map +1 -1
  36. package/dist/hooks/useRawCSS.js +9 -1
  37. package/dist/hooks/useRawCSS.js.map +1 -1
  38. package/dist/hooks/useStyles.d.ts +5 -0
  39. package/dist/hooks/useStyles.js +115 -46
  40. package/dist/hooks/useStyles.js.map +1 -1
  41. package/dist/index.d.ts +22 -11
  42. package/dist/index.js +10 -7
  43. package/dist/injector/index.d.ts +28 -2
  44. package/dist/injector/index.js +27 -1
  45. package/dist/injector/index.js.map +1 -1
  46. package/dist/injector/injector.d.ts +28 -1
  47. package/dist/injector/injector.js +93 -32
  48. package/dist/injector/injector.js.map +1 -1
  49. package/dist/injector/sheet-manager.d.ts +16 -7
  50. package/dist/injector/sheet-manager.js +31 -11
  51. package/dist/injector/sheet-manager.js.map +1 -1
  52. package/dist/injector/types.d.ts +70 -1
  53. package/dist/parser/classify.js +5 -4
  54. package/dist/parser/classify.js.map +1 -1
  55. package/dist/parser/const.js +17 -1
  56. package/dist/parser/const.js.map +1 -1
  57. package/dist/parser/parser.js +1 -1
  58. package/dist/pipeline/conditions.js +14 -8
  59. package/dist/pipeline/conditions.js.map +1 -1
  60. package/dist/pipeline/index.d.ts +3 -1
  61. package/dist/pipeline/index.js +137 -69
  62. package/dist/pipeline/index.js.map +1 -1
  63. package/dist/pipeline/materialize.js +404 -208
  64. package/dist/pipeline/materialize.js.map +1 -1
  65. package/dist/pipeline/parseStateKey.d.ts +1 -1
  66. package/dist/pipeline/parseStateKey.js +37 -20
  67. package/dist/pipeline/parseStateKey.js.map +1 -1
  68. package/dist/pipeline/simplify.js +111 -152
  69. package/dist/pipeline/simplify.js.map +1 -1
  70. package/dist/pipeline/warnings.js +18 -0
  71. package/dist/pipeline/warnings.js.map +1 -0
  72. package/dist/plugins/okhsl-plugin.js +2 -275
  73. package/dist/plugins/okhsl-plugin.js.map +1 -1
  74. package/dist/plugins/types.d.ts +9 -2
  75. package/dist/properties/index.js +82 -17
  76. package/dist/properties/index.js.map +1 -1
  77. package/dist/properties/property-type-resolver.d.ts +24 -0
  78. package/dist/properties/property-type-resolver.js +91 -0
  79. package/dist/properties/property-type-resolver.js.map +1 -0
  80. package/dist/ssr/astro.d.ts +29 -0
  81. package/dist/ssr/astro.js +65 -0
  82. package/dist/ssr/astro.js.map +1 -0
  83. package/dist/ssr/async-storage.d.ts +17 -0
  84. package/dist/ssr/async-storage.js +35 -0
  85. package/dist/ssr/async-storage.js.map +1 -0
  86. package/dist/ssr/collect-auto-properties.js +40 -0
  87. package/dist/ssr/collect-auto-properties.js.map +1 -0
  88. package/dist/ssr/collector.d.ts +102 -0
  89. package/dist/ssr/collector.js +227 -0
  90. package/dist/ssr/collector.js.map +1 -0
  91. package/dist/ssr/context.d.ts +8 -0
  92. package/dist/ssr/context.js +14 -0
  93. package/dist/ssr/context.js.map +1 -0
  94. package/dist/ssr/format-global-rules.js +22 -0
  95. package/dist/ssr/format-global-rules.js.map +1 -0
  96. package/dist/ssr/format-keyframes.js +70 -0
  97. package/dist/ssr/format-keyframes.js.map +1 -0
  98. package/dist/ssr/format-property.js +50 -0
  99. package/dist/ssr/format-property.js.map +1 -0
  100. package/dist/ssr/format-rules.js +73 -0
  101. package/dist/ssr/format-rules.js.map +1 -0
  102. package/dist/ssr/hydrate.d.ts +22 -0
  103. package/dist/ssr/hydrate.js +50 -0
  104. package/dist/ssr/hydrate.js.map +1 -0
  105. package/dist/ssr/index.d.ts +5 -0
  106. package/dist/ssr/index.js +12 -0
  107. package/dist/ssr/index.js.map +1 -0
  108. package/dist/ssr/next.d.ts +45 -0
  109. package/dist/ssr/next.js +71 -0
  110. package/dist/ssr/next.js.map +1 -0
  111. package/dist/ssr/ssr-collector-ref.js +12 -0
  112. package/dist/ssr/ssr-collector-ref.js.map +1 -0
  113. package/dist/states/index.js +12 -257
  114. package/dist/states/index.js.map +1 -1
  115. package/dist/styles/align.d.ts +1 -1
  116. package/dist/styles/align.js.map +1 -1
  117. package/dist/styles/border.d.ts +1 -1
  118. package/dist/styles/border.js +2 -2
  119. package/dist/styles/border.js.map +1 -1
  120. package/dist/styles/color.d.ts +2 -2
  121. package/dist/styles/color.js +9 -5
  122. package/dist/styles/color.js.map +1 -1
  123. package/dist/styles/createStyle.js +25 -22
  124. package/dist/styles/createStyle.js.map +1 -1
  125. package/dist/styles/fade.d.ts +1 -1
  126. package/dist/styles/fade.js.map +1 -1
  127. package/dist/styles/fill.d.ts +14 -16
  128. package/dist/styles/fill.js +6 -5
  129. package/dist/styles/fill.js.map +1 -1
  130. package/dist/styles/flow.d.ts +3 -3
  131. package/dist/styles/flow.js.map +1 -1
  132. package/dist/styles/index.js +1 -1
  133. package/dist/styles/justify.d.ts +1 -1
  134. package/dist/styles/justify.js.map +1 -1
  135. package/dist/styles/predefined.d.ts +0 -2
  136. package/dist/styles/predefined.js +1 -4
  137. package/dist/styles/predefined.js.map +1 -1
  138. package/dist/styles/preset.d.ts +5 -0
  139. package/dist/styles/preset.js +48 -47
  140. package/dist/styles/preset.js.map +1 -1
  141. package/dist/styles/radius.d.ts +1 -1
  142. package/dist/styles/radius.js.map +1 -1
  143. package/dist/styles/scrollbar.d.ts +9 -5
  144. package/dist/styles/scrollbar.js +25 -82
  145. package/dist/styles/scrollbar.js.map +1 -1
  146. package/dist/styles/shadow.d.ts +2 -2
  147. package/dist/styles/shadow.js.map +1 -1
  148. package/dist/styles/transition.d.ts +1 -1
  149. package/dist/styles/transition.js +1 -1
  150. package/dist/styles/transition.js.map +1 -1
  151. package/dist/styles/types.d.ts +70 -21
  152. package/dist/tasty.d.ts +30 -893
  153. package/dist/tasty.js +60 -19
  154. package/dist/tasty.js.map +1 -1
  155. package/dist/types.d.ts +3 -3
  156. package/dist/utils/cache-wrapper.js +4 -8
  157. package/dist/utils/cache-wrapper.js.map +1 -1
  158. package/dist/utils/color-math.d.ts +46 -0
  159. package/dist/utils/color-math.js +749 -0
  160. package/dist/utils/color-math.js.map +1 -0
  161. package/dist/utils/color-space.d.ts +5 -0
  162. package/dist/utils/color-space.js +229 -0
  163. package/dist/utils/color-space.js.map +1 -0
  164. package/dist/utils/colors.d.ts +1 -1
  165. package/dist/utils/colors.js +3 -1
  166. package/dist/utils/colors.js.map +1 -1
  167. package/dist/utils/css-types.d.ts +1 -1
  168. package/dist/utils/dotize.d.ts +1 -1
  169. package/dist/utils/has-keys.js +13 -0
  170. package/dist/utils/has-keys.js.map +1 -0
  171. package/dist/utils/mod-attrs.js +2 -2
  172. package/dist/utils/mod-attrs.js.map +1 -1
  173. package/dist/utils/process-tokens.d.ts +3 -13
  174. package/dist/utils/process-tokens.js +18 -98
  175. package/dist/utils/process-tokens.js.map +1 -1
  176. package/dist/utils/selector-transform.js +32 -0
  177. package/dist/utils/selector-transform.js.map +1 -0
  178. package/dist/utils/string.js.map +1 -1
  179. package/dist/utils/styles.d.ts +4 -88
  180. package/dist/utils/styles.js +34 -398
  181. package/dist/utils/styles.js.map +1 -1
  182. package/dist/utils/typography.d.ts +24 -13
  183. package/dist/utils/typography.js +14 -16
  184. package/dist/utils/typography.js.map +1 -1
  185. package/dist/zero/babel.d.ts +67 -8
  186. package/dist/zero/babel.js +109 -17
  187. package/dist/zero/babel.js.map +1 -1
  188. package/dist/zero/extractor.js +118 -1
  189. package/dist/zero/extractor.js.map +1 -1
  190. package/dist/zero/next.d.ts +44 -30
  191. package/dist/zero/next.js +102 -38
  192. package/dist/zero/next.js.map +1 -1
  193. package/docs/PIPELINE.md +519 -0
  194. package/docs/README.md +31 -0
  195. package/docs/adoption.md +296 -0
  196. package/docs/comparison.md +420 -0
  197. package/docs/configuration.md +326 -0
  198. package/docs/debug.md +318 -0
  199. package/docs/design-system.md +424 -0
  200. package/docs/dsl.md +673 -0
  201. package/docs/getting-started.md +217 -0
  202. package/docs/injector.md +528 -0
  203. package/docs/methodology.md +567 -0
  204. package/docs/runtime.md +485 -0
  205. package/docs/ssr.md +384 -0
  206. package/docs/styles.md +579 -0
  207. package/docs/tasty-static.md +433 -0
  208. package/package.json +98 -28
  209. package/dist/styles/styledScrollbar.d.ts +0 -47
  210. package/dist/styles/styledScrollbar.js +0 -38
  211. package/dist/styles/styledScrollbar.js.map +0 -1
  212. package/dist/tokens/typography.d.ts +0 -19
  213. package/dist/tokens/typography.js +0 -237
  214. package/dist/tokens/typography.js.map +0 -1
  215. package/dist/utils/hsl-to-rgb.js +0 -38
  216. package/dist/utils/hsl-to-rgb.js.map +0 -1
  217. package/dist/utils/okhsl-to-rgb.js +0 -296
  218. package/dist/utils/okhsl-to-rgb.js.map +0 -1
@@ -1,13 +1,21 @@
1
1
  import { renderStyles } from "../pipeline/index.js";
2
+ import { getConfig } from "../config.js";
2
3
  import { injectGlobal } from "../injector/index.js";
3
4
  import { resolveRecipes } from "../utils/resolve-recipes.js";
4
- import { useInsertionEffect, useMemo, useRef } from "react";
5
+ import { collectAutoInferredProperties } from "../ssr/collect-auto-properties.js";
6
+ import { resolveSSRCollector } from "./resolve-ssr-collector.js";
7
+ import { TastySSRContext } from "../ssr/context.js";
8
+ import { formatGlobalRules } from "../ssr/format-global-rules.js";
9
+ import { useContext, useInsertionEffect, useMemo, useRef } from "react";
5
10
 
6
11
  //#region src/hooks/useGlobalStyles.ts
7
12
  /**
8
13
  * Hook to inject global styles for a given selector.
9
14
  * Useful for styling elements by selector without generating classNames.
10
15
  *
16
+ * SSR-aware: when a ServerStyleCollector is available, CSS is collected
17
+ * during the render phase instead of being injected into the DOM.
18
+ *
11
19
  * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')
12
20
  * @param styles - Tasty styles object
13
21
  *
@@ -25,6 +33,7 @@ import { useInsertionEffect, useMemo, useRef } from "react";
25
33
  * ```
26
34
  */
27
35
  function useGlobalStyles(selector, styles) {
36
+ const ssrCollector = resolveSSRCollector(useContext(TastySSRContext));
28
37
  const disposeRef = useRef(null);
29
38
  const resolvedStyles = useMemo(() => {
30
39
  if (!styles) return styles;
@@ -38,6 +47,20 @@ function useGlobalStyles(selector, styles) {
38
47
  }
39
48
  return renderStyles(resolvedStyles, selector);
40
49
  }, [resolvedStyles, selector]);
50
+ useMemo(() => {
51
+ if (!ssrCollector || styleResults.length === 0) return;
52
+ ssrCollector.collectInternals();
53
+ const css = formatGlobalRules(styleResults);
54
+ if (css) {
55
+ const key = `global:${selector}:${css.length}:${css.slice(0, 64)}`;
56
+ ssrCollector.collectGlobalStyles(key, css);
57
+ }
58
+ if (getConfig().autoPropertyTypes !== false) collectAutoInferredProperties(styleResults, ssrCollector, resolvedStyles);
59
+ }, [
60
+ ssrCollector,
61
+ styleResults,
62
+ selector
63
+ ]);
41
64
  useInsertionEffect(() => {
42
65
  disposeRef.current?.();
43
66
  if (styleResults.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalStyles.js","names":[],"sources":["../../src/hooks/useGlobalStyles.ts"],"sourcesContent":["import { useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { injectGlobal } from '../injector';\nimport type { StyleResult } from '../pipeline';\nimport { renderStyles } from '../pipeline';\nimport type { Styles } from '../styles/types';\nimport { resolveRecipes } from '../utils/resolve-recipes';\n\n/**\n * Hook to inject global styles for a given selector.\n * Useful for styling elements by selector without generating classNames.\n *\n * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')\n * @param styles - Tasty styles object\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * useGlobalStyles('.card', {\n * padding: '2x',\n * radius: '1r',\n * fill: '#white',\n * });\n *\n * return <div className=\"card\">Content</div>;\n * }\n * ```\n */\nexport function useGlobalStyles(selector: string, styles?: Styles): void {\n const disposeRef = useRef<(() => void) | null>(null);\n\n // Resolve recipes before rendering (zero overhead if no recipes configured)\n const resolvedStyles = useMemo(() => {\n if (!styles) return styles;\n return resolveRecipes(styles);\n }, [styles]);\n\n // Render styles with the provided selector\n // Note: renderStyles overload with selector string returns StyleResult[] directly\n const styleResults = useMemo((): StyleResult[] => {\n if (!resolvedStyles) return [];\n\n // Validate selector - empty string would cause renderStyles to return RenderResult instead of StyleResult[]\n if (!selector) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Tasty] useGlobalStyles: selector is required and cannot be empty. ' +\n 'Styles will not be injected.',\n );\n }\n return [];\n }\n\n const result = renderStyles(resolvedStyles, selector);\n // When a non-empty selector is provided, renderStyles returns StyleResult[]\n return result as StyleResult[];\n }, [resolvedStyles, selector]);\n\n // Inject as global styles\n useInsertionEffect(() => {\n disposeRef.current?.();\n\n if (styleResults.length > 0) {\n const { dispose } = injectGlobal(styleResults);\n disposeRef.current = dispose;\n } else {\n disposeRef.current = null;\n }\n\n return () => {\n disposeRef.current?.();\n disposeRef.current = null;\n };\n }, [styleResults]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBAAgB,UAAkB,QAAuB;CACvE,MAAM,aAAa,OAA4B,KAAK;CAGpD,MAAM,iBAAiB,cAAc;AACnC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,eAAe,OAAO;IAC5B,CAAC,OAAO,CAAC;CAIZ,MAAM,eAAe,cAA6B;AAChD,MAAI,CAAC,eAAgB,QAAO,EAAE;AAG9B,MAAI,CAAC,UAAU;AAEX,WAAQ,KACN,kGAED;AAEH,UAAO,EAAE;;AAKX,SAFe,aAAa,gBAAgB,SAAS;IAGpD,CAAC,gBAAgB,SAAS,CAAC;AAG9B,0BAAyB;AACvB,aAAW,WAAW;AAEtB,MAAI,aAAa,SAAS,GAAG;GAC3B,MAAM,EAAE,YAAY,aAAa,aAAa;AAC9C,cAAW,UAAU;QAErB,YAAW,UAAU;AAGvB,eAAa;AACX,cAAW,WAAW;AACtB,cAAW,UAAU;;IAEtB,CAAC,aAAa,CAAC"}
1
+ {"version":3,"file":"useGlobalStyles.js","names":[],"sources":["../../src/hooks/useGlobalStyles.ts"],"sourcesContent":["import { useContext, useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { getConfig } from '../config';\nimport { injectGlobal } from '../injector';\nimport type { StyleResult } from '../pipeline';\nimport { renderStyles } from '../pipeline';\nimport { collectAutoInferredProperties } from '../ssr/collect-auto-properties';\nimport { resolveSSRCollector } from './resolve-ssr-collector';\nimport { TastySSRContext } from '../ssr/context';\nimport { formatGlobalRules } from '../ssr/format-global-rules';\nimport type { Styles } from '../styles/types';\nimport { resolveRecipes } from '../utils/resolve-recipes';\n\n/**\n * Hook to inject global styles for a given selector.\n * Useful for styling elements by selector without generating classNames.\n *\n * SSR-aware: when a ServerStyleCollector is available, CSS is collected\n * during the render phase instead of being injected into the DOM.\n *\n * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')\n * @param styles - Tasty styles object\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * useGlobalStyles('.card', {\n * padding: '2x',\n * radius: '1r',\n * fill: '#white',\n * });\n *\n * return <div className=\"card\">Content</div>;\n * }\n * ```\n */\nexport function useGlobalStyles(selector: string, styles?: Styles): void {\n const ssrContextValue = useContext(TastySSRContext);\n const ssrCollector = resolveSSRCollector(ssrContextValue);\n\n const disposeRef = useRef<(() => void) | null>(null);\n\n // Resolve recipes before rendering (zero overhead if no recipes configured)\n const resolvedStyles = useMemo(() => {\n if (!styles) return styles;\n return resolveRecipes(styles);\n }, [styles]);\n\n // Render styles with the provided selector\n // Note: renderStyles overload with selector string returns StyleResult[] directly\n const styleResults = useMemo((): StyleResult[] => {\n if (!resolvedStyles) return [];\n\n if (!selector) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Tasty] useGlobalStyles: selector is required and cannot be empty. ' +\n 'Styles will not be injected.',\n );\n }\n return [];\n }\n\n const result = renderStyles(resolvedStyles, selector);\n return result as StyleResult[];\n }, [resolvedStyles, selector]);\n\n // SSR path: collect CSS during render\n useMemo(() => {\n if (!ssrCollector || styleResults.length === 0) return;\n\n ssrCollector.collectInternals();\n\n const css = formatGlobalRules(styleResults);\n if (css) {\n const key = `global:${selector}:${css.length}:${css.slice(0, 64)}`;\n ssrCollector.collectGlobalStyles(key, css);\n }\n\n if (getConfig().autoPropertyTypes !== false) {\n collectAutoInferredProperties(styleResults, ssrCollector, resolvedStyles);\n }\n }, [ssrCollector, styleResults, selector]);\n\n // Client path: inject via DOM\n useInsertionEffect(() => {\n disposeRef.current?.();\n\n if (styleResults.length > 0) {\n const { dispose } = injectGlobal(styleResults);\n disposeRef.current = dispose;\n } else {\n disposeRef.current = null;\n }\n\n return () => {\n disposeRef.current?.();\n disposeRef.current = null;\n };\n }, [styleResults]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,gBAAgB,UAAkB,QAAuB;CAEvE,MAAM,eAAe,oBADG,WAAW,gBAAgB,CACM;CAEzD,MAAM,aAAa,OAA4B,KAAK;CAGpD,MAAM,iBAAiB,cAAc;AACnC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,eAAe,OAAO;IAC5B,CAAC,OAAO,CAAC;CAIZ,MAAM,eAAe,cAA6B;AAChD,MAAI,CAAC,eAAgB,QAAO,EAAE;AAE9B,MAAI,CAAC,UAAU;AAEX,WAAQ,KACN,kGAED;AAEH,UAAO,EAAE;;AAIX,SADe,aAAa,gBAAgB,SAAS;IAEpD,CAAC,gBAAgB,SAAS,CAAC;AAG9B,eAAc;AACZ,MAAI,CAAC,gBAAgB,aAAa,WAAW,EAAG;AAEhD,eAAa,kBAAkB;EAE/B,MAAM,MAAM,kBAAkB,aAAa;AAC3C,MAAI,KAAK;GACP,MAAM,MAAM,UAAU,SAAS,GAAG,IAAI,OAAO,GAAG,IAAI,MAAM,GAAG,GAAG;AAChE,gBAAa,oBAAoB,KAAK,IAAI;;AAG5C,MAAI,WAAW,CAAC,sBAAsB,MACpC,+BAA8B,cAAc,cAAc,eAAe;IAE1E;EAAC;EAAc;EAAc;EAAS,CAAC;AAG1C,0BAAyB;AACvB,aAAW,WAAW;AAEtB,MAAI,aAAa,SAAS,GAAG;GAC3B,MAAM,EAAE,YAAY,aAAa,aAAa;AAC9C,cAAW,UAAU;QAErB,YAAW,UAAU;AAGvB,eAAa;AACX,cAAW,WAAW;AACtB,cAAW,UAAU;;IAEtB,CAAC,aAAa,CAAC"}
@@ -1,8 +1,12 @@
1
1
  import { keyframes } from "../injector/index.js";
2
- import { useInsertionEffect, useMemo, useRef } from "react";
2
+ import { resolveSSRCollector } from "./resolve-ssr-collector.js";
3
+ import { TastySSRContext } from "../ssr/context.js";
4
+ import { formatKeyframesCSS } from "../ssr/format-keyframes.js";
5
+ import { useContext, useInsertionEffect, useMemo, useRef } from "react";
3
6
 
4
7
  //#region src/hooks/useKeyframes.ts
5
8
  function useKeyframes(stepsOrFactory, depsOrOptions, options) {
9
+ const ssrCollector = resolveSSRCollector(useContext(TastySSRContext));
6
10
  const isFactory = typeof stepsOrFactory === "function";
7
11
  const deps = isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : void 0;
8
12
  const opts = isFactory ? options : depsOrOptions;
@@ -14,9 +18,15 @@ function useKeyframes(stepsOrFactory, depsOrOptions, options) {
14
18
  const renderResultRef = useRef(null);
15
19
  const effectResultRef = useRef(null);
16
20
  const name = useMemo(() => {
21
+ if (!stepsData) return "";
22
+ if (ssrCollector) {
23
+ const actualName = ssrCollector.allocateKeyframeName(opts?.name);
24
+ const css = formatKeyframesCSS(actualName, stepsData);
25
+ ssrCollector.collectKeyframes(actualName, css);
26
+ return actualName;
27
+ }
17
28
  renderResultRef.current?.dispose();
18
29
  renderResultRef.current = null;
19
- if (!stepsData) return "";
20
30
  const result = keyframes(stepsData, {
21
31
  name: opts?.name,
22
32
  root: opts?.root
@@ -26,7 +36,8 @@ function useKeyframes(stepsOrFactory, depsOrOptions, options) {
26
36
  }, [
27
37
  stepsData,
28
38
  opts?.name,
29
- opts?.root
39
+ opts?.root,
40
+ ssrCollector
30
41
  ]);
31
42
  useInsertionEffect(() => {
32
43
  effectResultRef.current?.dispose();
@@ -1 +1 @@
1
- {"version":3,"file":"useKeyframes.js","names":[],"sources":["../../src/hooks/useKeyframes.ts"],"sourcesContent":["import { useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { keyframes } from '../injector';\nimport type { KeyframesResult, KeyframesSteps } from '../injector/types';\n\ninterface UseKeyframesOptions {\n name?: string;\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to inject CSS @keyframes and return the generated animation name.\n * Handles keyframes injection with proper cleanup on unmount or dependency changes.\n *\n * @example Basic usage - steps object is the dependency\n * ```tsx\n * function MyComponent() {\n * const bounce = useKeyframes({\n * '0%': { transform: 'scale(1)' },\n * '50%': { transform: 'scale(1.1)' },\n * '100%': { transform: 'scale(1)' },\n * });\n *\n * return <div style={{ animation: `${bounce} 1s infinite` }}>Bouncing</div>;\n * }\n * ```\n *\n * @example With custom name\n * ```tsx\n * function MyComponent() {\n * const fadeIn = useKeyframes(\n * { from: { opacity: 0 }, to: { opacity: 1 } },\n * { name: 'fadeIn' }\n * );\n *\n * return <div style={{ animation: `${fadeIn} 0.3s ease-out` }}>Fading in</div>;\n * }\n * ```\n *\n * @example Factory function with dependencies\n * ```tsx\n * function MyComponent({ scale }: { scale: number }) {\n * const pulse = useKeyframes(\n * () => ({\n * '0%': { transform: 'scale(1)' },\n * '100%': { transform: `scale(${scale})` },\n * }),\n * [scale]\n * );\n *\n * return <div style={{ animation: `${pulse} 1s infinite` }}>Pulsing</div>;\n * }\n * ```\n */\n\n// Overload 1: Static steps object\nexport function useKeyframes(\n steps: KeyframesSteps,\n options?: UseKeyframesOptions,\n): string;\n\n// Overload 2: Factory function with dependencies\nexport function useKeyframes(\n factory: () => KeyframesSteps,\n deps: readonly unknown[],\n options?: UseKeyframesOptions,\n): string;\n\n// Implementation\nexport function useKeyframes(\n stepsOrFactory: KeyframesSteps | (() => KeyframesSteps),\n depsOrOptions?: readonly unknown[] | UseKeyframesOptions,\n options?: UseKeyframesOptions,\n): string {\n // Detect which overload is being used\n const isFactory = typeof stepsOrFactory === 'function';\n\n // Parse arguments based on overload\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseKeyframesOptions | undefined);\n\n // Memoize the keyframes steps to get a stable reference\n const stepsData = useMemo(\n () => {\n const steps = isFactory\n ? (stepsOrFactory as () => KeyframesSteps)()\n : (stepsOrFactory as KeyframesSteps);\n\n if (!steps || Object.keys(steps).length === 0) {\n return null;\n }\n\n return steps;\n },\n\n isFactory ? (deps ?? []) : [stepsOrFactory],\n );\n\n // Store keyframes results for cleanup - we need to track both the render-time\n // injection (for the name) and the effect-time injection (for Strict Mode safety)\n const renderResultRef = useRef<KeyframesResult | null>(null);\n const effectResultRef = useRef<KeyframesResult | null>(null);\n\n // Inject keyframes during render to ensure the animation name is available\n // immediately. The keyframes() function uses reference counting internally,\n // so multiple calls with the same content are deduplicated.\n const name = useMemo(() => {\n // Dispose previous render-time result if deps changed\n renderResultRef.current?.dispose();\n renderResultRef.current = null;\n\n if (!stepsData) {\n return '';\n }\n\n // Inject keyframes synchronously\n const result = keyframes(stepsData, {\n name: opts?.name,\n root: opts?.root,\n });\n\n renderResultRef.current = result;\n\n return result.toString();\n }, [stepsData, opts?.name, opts?.root]);\n\n // Handle injection and cleanup in useInsertionEffect to properly support\n // React 18+ Strict Mode double-invocation (mount → unmount → mount).\n // The effect setup re-injects the keyframes if cleanup was called, ensuring\n // the CSS exists after Strict Mode remounts.\n useInsertionEffect(() => {\n // Dispose previous effect-time result\n effectResultRef.current?.dispose();\n effectResultRef.current = null;\n\n // Re-inject keyframes. This ensures the CSS exists after Strict Mode cleanup.\n // The keyframes() function uses reference counting, so this is idempotent\n // if the CSS wasn't disposed.\n if (stepsData) {\n const result = keyframes(stepsData, {\n name: opts?.name,\n root: opts?.root,\n });\n effectResultRef.current = result;\n }\n\n // Cleanup on unmount or when dependencies change.\n // Dispose both the effect-time and render-time results to properly\n // decrement the reference count.\n return () => {\n effectResultRef.current?.dispose();\n effectResultRef.current = null;\n renderResultRef.current?.dispose();\n renderResultRef.current = null;\n };\n }, [stepsData, opts?.name, opts?.root]);\n\n return name;\n}\n"],"mappings":";;;;AAqEA,SAAgB,aACd,gBACA,eACA,SACQ;CAER,MAAM,YAAY,OAAO,mBAAmB;CAG5C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB;CAC9D,MAAM,OAAO,YACT,UACC;CAGL,MAAM,YAAY,cACV;EACJ,MAAM,QAAQ,YACT,gBAAyC,GACzC;AAEL,MAAI,CAAC,SAAS,OAAO,KAAK,MAAM,CAAC,WAAW,EAC1C,QAAO;AAGT,SAAO;IAGT,YAAa,QAAQ,EAAE,GAAI,CAAC,eAAe,CAC5C;CAID,MAAM,kBAAkB,OAA+B,KAAK;CAC5D,MAAM,kBAAkB,OAA+B,KAAK;CAK5D,MAAM,OAAO,cAAc;AAEzB,kBAAgB,SAAS,SAAS;AAClC,kBAAgB,UAAU;AAE1B,MAAI,CAAC,UACH,QAAO;EAIT,MAAM,SAAS,UAAU,WAAW;GAClC,MAAM,MAAM;GACZ,MAAM,MAAM;GACb,CAAC;AAEF,kBAAgB,UAAU;AAE1B,SAAO,OAAO,UAAU;IACvB;EAAC;EAAW,MAAM;EAAM,MAAM;EAAK,CAAC;AAMvC,0BAAyB;AAEvB,kBAAgB,SAAS,SAAS;AAClC,kBAAgB,UAAU;AAK1B,MAAI,UAKF,iBAAgB,UAJD,UAAU,WAAW;GAClC,MAAM,MAAM;GACZ,MAAM,MAAM;GACb,CAAC;AAOJ,eAAa;AACX,mBAAgB,SAAS,SAAS;AAClC,mBAAgB,UAAU;AAC1B,mBAAgB,SAAS,SAAS;AAClC,mBAAgB,UAAU;;IAE3B;EAAC;EAAW,MAAM;EAAM,MAAM;EAAK,CAAC;AAEvC,QAAO"}
1
+ {"version":3,"file":"useKeyframes.js","names":[],"sources":["../../src/hooks/useKeyframes.ts"],"sourcesContent":["import { useContext, useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { keyframes } from '../injector';\nimport type { KeyframesResult, KeyframesSteps } from '../injector/types';\nimport { resolveSSRCollector } from './resolve-ssr-collector';\nimport { TastySSRContext } from '../ssr/context';\nimport { formatKeyframesCSS } from '../ssr/format-keyframes';\n\ninterface UseKeyframesOptions {\n name?: string;\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to inject CSS @keyframes and return the generated animation name.\n * Handles keyframes injection with proper cleanup on unmount or dependency changes.\n *\n * @example Basic usage - steps object is the dependency\n * ```tsx\n * function MyComponent() {\n * const bounce = useKeyframes({\n * '0%': { transform: 'scale(1)' },\n * '50%': { transform: 'scale(1.1)' },\n * '100%': { transform: 'scale(1)' },\n * });\n *\n * return <div style={{ animation: `${bounce} 1s infinite` }}>Bouncing</div>;\n * }\n * ```\n *\n * @example With custom name\n * ```tsx\n * function MyComponent() {\n * const fadeIn = useKeyframes(\n * { from: { opacity: 0 }, to: { opacity: 1 } },\n * { name: 'fadeIn' }\n * );\n *\n * return <div style={{ animation: `${fadeIn} 0.3s ease-out` }}>Fading in</div>;\n * }\n * ```\n *\n * @example Factory function with dependencies\n * ```tsx\n * function MyComponent({ scale }: { scale: number }) {\n * const pulse = useKeyframes(\n * () => ({\n * '0%': { transform: 'scale(1)' },\n * '100%': { transform: `scale(${scale})` },\n * }),\n * [scale]\n * );\n *\n * return <div style={{ animation: `${pulse} 1s infinite` }}>Pulsing</div>;\n * }\n * ```\n */\n\n// Overload 1: Static steps object\nexport function useKeyframes(\n steps: KeyframesSteps,\n options?: UseKeyframesOptions,\n): string;\n\n// Overload 2: Factory function with dependencies\nexport function useKeyframes(\n factory: () => KeyframesSteps,\n deps: readonly unknown[],\n options?: UseKeyframesOptions,\n): string;\n\n// Implementation\nexport function useKeyframes(\n stepsOrFactory: KeyframesSteps | (() => KeyframesSteps),\n depsOrOptions?: readonly unknown[] | UseKeyframesOptions,\n options?: UseKeyframesOptions,\n): string {\n const ssrContextValue = useContext(TastySSRContext);\n const ssrCollector = resolveSSRCollector(ssrContextValue);\n\n // Detect which overload is being used\n const isFactory = typeof stepsOrFactory === 'function';\n\n // Parse arguments based on overload\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseKeyframesOptions | undefined);\n\n // Memoize the keyframes steps to get a stable reference\n const stepsData = useMemo(\n () => {\n const steps = isFactory\n ? (stepsOrFactory as () => KeyframesSteps)()\n : (stepsOrFactory as KeyframesSteps);\n\n if (!steps || Object.keys(steps).length === 0) {\n return null;\n }\n\n return steps;\n },\n\n isFactory ? (deps ?? []) : [stepsOrFactory],\n );\n\n // Store keyframes results for cleanup (client only)\n const renderResultRef = useRef<KeyframesResult | null>(null);\n const effectResultRef = useRef<KeyframesResult | null>(null);\n\n const name = useMemo(() => {\n if (!stepsData) {\n return '';\n }\n\n // SSR path: format and collect, return name without DOM injection\n if (ssrCollector) {\n const actualName = ssrCollector.allocateKeyframeName(opts?.name);\n const css = formatKeyframesCSS(actualName, stepsData);\n ssrCollector.collectKeyframes(actualName, css);\n return actualName;\n }\n\n // Client path: inject keyframes synchronously for immediate name availability\n renderResultRef.current?.dispose();\n renderResultRef.current = null;\n\n const result = keyframes(stepsData, {\n name: opts?.name,\n root: opts?.root,\n });\n\n renderResultRef.current = result;\n\n return result.toString();\n }, [stepsData, opts?.name, opts?.root, ssrCollector]);\n\n // Client path: handle Strict Mode double-invocation and cleanup\n useInsertionEffect(() => {\n effectResultRef.current?.dispose();\n effectResultRef.current = null;\n\n if (stepsData) {\n const result = keyframes(stepsData, {\n name: opts?.name,\n root: opts?.root,\n });\n effectResultRef.current = result;\n }\n\n return () => {\n effectResultRef.current?.dispose();\n effectResultRef.current = null;\n renderResultRef.current?.dispose();\n renderResultRef.current = null;\n };\n }, [stepsData, opts?.name, opts?.root]);\n\n return name;\n}\n"],"mappings":";;;;;;;AAwEA,SAAgB,aACd,gBACA,eACA,SACQ;CAER,MAAM,eAAe,oBADG,WAAW,gBAAgB,CACM;CAGzD,MAAM,YAAY,OAAO,mBAAmB;CAG5C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB;CAC9D,MAAM,OAAO,YACT,UACC;CAGL,MAAM,YAAY,cACV;EACJ,MAAM,QAAQ,YACT,gBAAyC,GACzC;AAEL,MAAI,CAAC,SAAS,OAAO,KAAK,MAAM,CAAC,WAAW,EAC1C,QAAO;AAGT,SAAO;IAGT,YAAa,QAAQ,EAAE,GAAI,CAAC,eAAe,CAC5C;CAGD,MAAM,kBAAkB,OAA+B,KAAK;CAC5D,MAAM,kBAAkB,OAA+B,KAAK;CAE5D,MAAM,OAAO,cAAc;AACzB,MAAI,CAAC,UACH,QAAO;AAIT,MAAI,cAAc;GAChB,MAAM,aAAa,aAAa,qBAAqB,MAAM,KAAK;GAChE,MAAM,MAAM,mBAAmB,YAAY,UAAU;AACrD,gBAAa,iBAAiB,YAAY,IAAI;AAC9C,UAAO;;AAIT,kBAAgB,SAAS,SAAS;AAClC,kBAAgB,UAAU;EAE1B,MAAM,SAAS,UAAU,WAAW;GAClC,MAAM,MAAM;GACZ,MAAM,MAAM;GACb,CAAC;AAEF,kBAAgB,UAAU;AAE1B,SAAO,OAAO,UAAU;IACvB;EAAC;EAAW,MAAM;EAAM,MAAM;EAAM;EAAa,CAAC;AAGrD,0BAAyB;AACvB,kBAAgB,SAAS,SAAS;AAClC,kBAAgB,UAAU;AAE1B,MAAI,UAKF,iBAAgB,UAJD,UAAU,WAAW;GAClC,MAAM,MAAM;GACZ,MAAM,MAAM;GACb,CAAC;AAIJ,eAAa;AACX,mBAAgB,SAAS,SAAS;AAClC,mBAAgB,UAAU;AAC1B,mBAAgB,SAAS,SAAS;AAClC,mBAAgB,UAAU;;IAE3B;EAAC;EAAW,MAAM;EAAM,MAAM;EAAK,CAAC;AAEvC,QAAO"}
@@ -1,5 +1,8 @@
1
1
  import { getGlobalInjector } from "../config.js";
2
- import { useInsertionEffect, useMemo } from "react";
2
+ import { formatPropertyCSS } from "../ssr/format-property.js";
3
+ import { resolveSSRCollector } from "./resolve-ssr-collector.js";
4
+ import { TastySSRContext } from "../ssr/context.js";
5
+ import { useContext, useInsertionEffect, useMemo } from "react";
3
6
 
4
7
  //#region src/hooks/useProperty.ts
5
8
  /**
@@ -55,6 +58,33 @@ import { useInsertionEffect, useMemo } from "react";
55
58
  * ```
56
59
  */
57
60
  function useProperty(name, options) {
61
+ const ssrCollector = resolveSSRCollector(useContext(TastySSRContext));
62
+ const optionsKey = useMemo(() => {
63
+ if (!options) return "";
64
+ return JSON.stringify({
65
+ syntax: options.syntax,
66
+ inherits: options.inherits,
67
+ initialValue: options.initialValue
68
+ });
69
+ }, [
70
+ options?.syntax,
71
+ options?.inherits,
72
+ options?.initialValue
73
+ ]);
74
+ useMemo(() => {
75
+ if (!ssrCollector || !name) return;
76
+ ssrCollector.collectInternals();
77
+ const css = formatPropertyCSS(name, {
78
+ syntax: options?.syntax,
79
+ inherits: options?.inherits,
80
+ initialValue: options?.initialValue
81
+ });
82
+ if (css) ssrCollector.collectProperty(name, css);
83
+ }, [
84
+ ssrCollector,
85
+ name,
86
+ optionsKey
87
+ ]);
58
88
  useInsertionEffect(() => {
59
89
  if (!name) {
60
90
  console.warn(`[Tasty] useProperty: property name is required`);
@@ -70,18 +100,7 @@ function useProperty(name, options) {
70
100
  });
71
101
  }, [
72
102
  name,
73
- useMemo(() => {
74
- if (!options) return "";
75
- return JSON.stringify({
76
- syntax: options.syntax,
77
- inherits: options.inherits,
78
- initialValue: options.initialValue
79
- });
80
- }, [
81
- options?.syntax,
82
- options?.inherits,
83
- options?.initialValue
84
- ]),
103
+ optionsKey,
85
104
  options?.root
86
105
  ]);
87
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useProperty.js","names":[],"sources":["../../src/hooks/useProperty.ts"],"sourcesContent":["import { useInsertionEffect, useMemo } from 'react';\n\nimport { getGlobalInjector } from '../config';\n\nexport interface UsePropertyOptions {\n /**\n * CSS syntax string for the property (e.g., '<color>', '<length>', '<angle>').\n * For color tokens (#name), this is auto-set to '<color>' and cannot be overridden.\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax\n */\n syntax?: string;\n /**\n * Whether the property inherits from parent elements\n * @default true\n */\n inherits?: boolean;\n /**\n * Initial value for the property.\n * For color tokens (#name), this defaults to 'transparent' if not specified.\n */\n initialValue?: string | number;\n /**\n * Shadow root or document to inject into\n */\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to register a CSS @property custom property.\n * This enables advanced features like animating custom properties.\n *\n * Note: @property rules are global and persistent once defined.\n * The hook ensures the property is only registered once per root.\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 * @param name - The property token ($name, #name) or CSS property name (--name)\n * @param options - Property configuration\n *\n * @example Basic property with token syntax\n * ```tsx\n * function Spinner() {\n * useProperty('$rotation', {\n * syntax: '<angle>',\n * inherits: false,\n * initialValue: '0deg',\n * });\n *\n * return <div className=\"spinner\" />;\n * }\n * ```\n *\n * @example Color property with token syntax (auto-sets syntax)\n * ```tsx\n * function MyComponent() {\n * useProperty('#theme', {\n * initialValue: 'red', // syntax: '<color>' is auto-set\n * });\n *\n * // Now --theme-color can be animated with CSS transitions\n * return <div style={{ '--theme-color': 'blue' } as React.CSSProperties}>Colored</div>;\n * }\n * ```\n *\n * @example Legacy format (still supported)\n * ```tsx\n * function ResizableBox() {\n * useProperty('--box-size', {\n * syntax: '<length>',\n * initialValue: '100px',\n * });\n *\n * return <div style={{ width: 'var(--box-size)' }} />;\n * }\n * ```\n */\nexport function useProperty(name: string, options?: UsePropertyOptions): void {\n // Memoize the options to create a stable dependency\n const optionsKey = useMemo(() => {\n if (!options) return '';\n return JSON.stringify({\n syntax: options.syntax,\n inherits: options.inherits,\n initialValue: options.initialValue,\n });\n }, [options?.syntax, options?.inherits, options?.initialValue]);\n\n useInsertionEffect(() => {\n if (!name) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(`[Tasty] useProperty: property name is required`);\n }\n return;\n }\n\n const injector = getGlobalInjector();\n\n // Check if already defined (properties are persistent)\n // The injector handles token parsing internally\n if (injector.isPropertyDefined(name, { root: options?.root })) {\n return;\n }\n\n // Register the property\n // The injector handles $name, #name, --name parsing and auto-sets\n // syntax for color tokens\n injector.property(name, {\n syntax: options?.syntax,\n inherits: options?.inherits,\n initialValue: options?.initialValue,\n root: options?.root,\n });\n\n // No cleanup - @property rules are global and persistent\n // Re-registering is a no-op anyway due to the isPropertyDefined check\n }, [name, optionsKey, options?.root]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EA,SAAgB,YAAY,MAAc,SAAoC;AAW5E,0BAAyB;AACvB,MAAI,CAAC,MAAM;AAEP,WAAQ,KAAK,iDAAiD;AAEhE;;EAGF,MAAM,WAAW,mBAAmB;AAIpC,MAAI,SAAS,kBAAkB,MAAM,EAAE,MAAM,SAAS,MAAM,CAAC,CAC3D;AAMF,WAAS,SAAS,MAAM;GACtB,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,cAAc,SAAS;GACvB,MAAM,SAAS;GAChB,CAAC;IAID;EAAC;EArCe,cAAc;AAC/B,OAAI,CAAC,QAAS,QAAO;AACrB,UAAO,KAAK,UAAU;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,cAAc,QAAQ;IACvB,CAAC;KACD;GAAC,SAAS;GAAQ,SAAS;GAAU,SAAS;GAAa,CAAC;EA8BzC,SAAS;EAAK,CAAC"}
1
+ {"version":3,"file":"useProperty.js","names":[],"sources":["../../src/hooks/useProperty.ts"],"sourcesContent":["import { useContext, useInsertionEffect, useMemo } from 'react';\n\nimport { getGlobalInjector } from '../config';\nimport { resolveSSRCollector } from './resolve-ssr-collector';\nimport { TastySSRContext } from '../ssr/context';\nimport { formatPropertyCSS } from '../ssr/format-property';\n\nexport interface UsePropertyOptions {\n /**\n * CSS syntax string for the property (e.g., '<color>', '<length>', '<angle>').\n * For color tokens (#name), this is auto-set to '<color>' and cannot be overridden.\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax\n */\n syntax?: string;\n /**\n * Whether the property inherits from parent elements\n * @default true\n */\n inherits?: boolean;\n /**\n * Initial value for the property.\n * For color tokens (#name), this defaults to 'transparent' if not specified.\n */\n initialValue?: string | number;\n /**\n * Shadow root or document to inject into\n */\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to register a CSS @property custom property.\n * This enables advanced features like animating custom properties.\n *\n * Note: @property rules are global and persistent once defined.\n * The hook ensures the property is only registered once per root.\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 * @param name - The property token ($name, #name) or CSS property name (--name)\n * @param options - Property configuration\n *\n * @example Basic property with token syntax\n * ```tsx\n * function Spinner() {\n * useProperty('$rotation', {\n * syntax: '<angle>',\n * inherits: false,\n * initialValue: '0deg',\n * });\n *\n * return <div className=\"spinner\" />;\n * }\n * ```\n *\n * @example Color property with token syntax (auto-sets syntax)\n * ```tsx\n * function MyComponent() {\n * useProperty('#theme', {\n * initialValue: 'red', // syntax: '<color>' is auto-set\n * });\n *\n * // Now --theme-color can be animated with CSS transitions\n * return <div style={{ '--theme-color': 'blue' } as React.CSSProperties}>Colored</div>;\n * }\n * ```\n *\n * @example Legacy format (still supported)\n * ```tsx\n * function ResizableBox() {\n * useProperty('--box-size', {\n * syntax: '<length>',\n * initialValue: '100px',\n * });\n *\n * return <div style={{ width: 'var(--box-size)' }} />;\n * }\n * ```\n */\nexport function useProperty(name: string, options?: UsePropertyOptions): void {\n const ssrContextValue = useContext(TastySSRContext);\n const ssrCollector = resolveSSRCollector(ssrContextValue);\n\n // Memoize the options to create a stable dependency\n const optionsKey = useMemo(() => {\n if (!options) return '';\n return JSON.stringify({\n syntax: options.syntax,\n inherits: options.inherits,\n initialValue: options.initialValue,\n });\n }, [options?.syntax, options?.inherits, options?.initialValue]);\n\n // SSR path: collect @property CSS during render\n useMemo(() => {\n if (!ssrCollector || !name) return;\n\n ssrCollector.collectInternals();\n\n const css = formatPropertyCSS(name, {\n syntax: options?.syntax,\n inherits: options?.inherits,\n initialValue: options?.initialValue,\n });\n if (css) {\n ssrCollector.collectProperty(name, css);\n }\n }, [ssrCollector, name, optionsKey]);\n\n // Client path: inject via DOM\n useInsertionEffect(() => {\n if (!name) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(`[Tasty] useProperty: property name is required`);\n }\n return;\n }\n\n const injector = getGlobalInjector();\n\n if (injector.isPropertyDefined(name, { root: options?.root })) {\n return;\n }\n\n injector.property(name, {\n syntax: options?.syntax,\n inherits: options?.inherits,\n initialValue: options?.initialValue,\n root: options?.root,\n });\n }, [name, optionsKey, options?.root]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,YAAY,MAAc,SAAoC;CAE5E,MAAM,eAAe,oBADG,WAAW,gBAAgB,CACM;CAGzD,MAAM,aAAa,cAAc;AAC/B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,KAAK,UAAU;GACpB,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,cAAc,QAAQ;GACvB,CAAC;IACD;EAAC,SAAS;EAAQ,SAAS;EAAU,SAAS;EAAa,CAAC;AAG/D,eAAc;AACZ,MAAI,CAAC,gBAAgB,CAAC,KAAM;AAE5B,eAAa,kBAAkB;EAE/B,MAAM,MAAM,kBAAkB,MAAM;GAClC,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,cAAc,SAAS;GACxB,CAAC;AACF,MAAI,IACF,cAAa,gBAAgB,MAAM,IAAI;IAExC;EAAC;EAAc;EAAM;EAAW,CAAC;AAGpC,0BAAyB;AACvB,MAAI,CAAC,MAAM;AAEP,WAAQ,KAAK,iDAAiD;AAEhE;;EAGF,MAAM,WAAW,mBAAmB;AAEpC,MAAI,SAAS,kBAAkB,MAAM,EAAE,MAAM,SAAS,MAAM,CAAC,CAC3D;AAGF,WAAS,SAAS,MAAM;GACtB,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,cAAc,SAAS;GACvB,MAAM,SAAS;GAChB,CAAC;IACD;EAAC;EAAM;EAAY,SAAS;EAAK,CAAC"}
@@ -1,12 +1,20 @@
1
1
  import { injectRawCSS } from "../injector/index.js";
2
- import { useInsertionEffect, useMemo, useRef } from "react";
2
+ import { resolveSSRCollector } from "./resolve-ssr-collector.js";
3
+ import { TastySSRContext } from "../ssr/context.js";
4
+ import { useContext, useInsertionEffect, useMemo, useRef } from "react";
3
5
 
4
6
  //#region src/hooks/useRawCSS.ts
5
7
  function useRawCSS(cssOrFactory, depsOrOptions, options) {
8
+ const ssrCollector = resolveSSRCollector(useContext(TastySSRContext));
6
9
  const isFactory = typeof cssOrFactory === "function";
7
10
  const deps = isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : void 0;
8
11
  const opts = isFactory ? options : depsOrOptions;
9
12
  const css = useMemo(() => isFactory ? cssOrFactory() : cssOrFactory, isFactory ? deps ?? [] : [cssOrFactory]);
13
+ useMemo(() => {
14
+ if (!ssrCollector || !css.trim()) return;
15
+ const key = `raw:${css.length}:${css.slice(0, 64)}`;
16
+ ssrCollector.collectRawCSS(key, css);
17
+ }, [ssrCollector, css]);
10
18
  const disposeRef = useRef(null);
11
19
  useInsertionEffect(() => {
12
20
  disposeRef.current?.();
@@ -1 +1 @@
1
- {"version":3,"file":"useRawCSS.js","names":[],"sources":["../../src/hooks/useRawCSS.ts"],"sourcesContent":["import { useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { injectRawCSS } from '../injector';\n\ninterface UseRawCSSOptions {\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to inject raw CSS text directly without parsing.\n * This is a low-overhead alternative for injecting global CSS that doesn't need tasty processing.\n *\n * The CSS is inserted into a separate style element (data-tasty-raw) to avoid conflicts\n * with tasty's chunked style sheets.\n *\n * @example Static CSS string\n * ```tsx\n * function GlobalStyles() {\n * useRawCSS(`\n * body {\n * margin: 0;\n * padding: 0;\n * font-family: sans-serif;\n * }\n * `);\n *\n * return null;\n * }\n * ```\n *\n * @example Factory function with dependencies (like useMemo)\n * ```tsx\n * function ThemeStyles({ theme }: { theme: 'light' | 'dark' }) {\n * useRawCSS(() => `\n * :root {\n * --bg-color: ${theme === 'dark' ? '#1a1a1a' : '#ffffff'};\n * --text-color: ${theme === 'dark' ? '#ffffff' : '#1a1a1a'};\n * }\n * `, [theme]);\n *\n * return null;\n * }\n * ```\n *\n * @example With options\n * ```tsx\n * function ShadowStyles({ shadowRoot }) {\n * useRawCSS(() => `.scoped { color: red; }`, [], { root: shadowRoot });\n * return null;\n * }\n * ```\n */\n\n// Overload 1: Static CSS string\nexport function useRawCSS(css: string, options?: UseRawCSSOptions): void;\n\n// Overload 2: Factory function with dependencies\nexport function useRawCSS(\n factory: () => string,\n deps: readonly unknown[],\n options?: UseRawCSSOptions,\n): void;\n\n// Implementation\nexport function useRawCSS(\n cssOrFactory: string | (() => string),\n depsOrOptions?: readonly unknown[] | UseRawCSSOptions,\n options?: UseRawCSSOptions,\n): void {\n // Detect which overload is being used\n const isFactory = typeof cssOrFactory === 'function';\n\n // Parse arguments based on overload\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseRawCSSOptions | undefined);\n\n // Memoize CSS - for factory functions, use provided deps; for strings, use the string itself\n const css = useMemo(\n () =>\n isFactory ? (cssOrFactory as () => string)() : (cssOrFactory as string),\n\n isFactory ? (deps ?? []) : [cssOrFactory],\n );\n\n const disposeRef = useRef<(() => void) | null>(null);\n\n useInsertionEffect(() => {\n // Dispose previous injection if any\n disposeRef.current?.();\n\n if (!css.trim()) {\n disposeRef.current = null;\n return;\n }\n\n // Inject new CSS\n const { dispose } = injectRawCSS(css, opts);\n disposeRef.current = dispose;\n\n // Cleanup on unmount or when css changes\n return () => {\n disposeRef.current?.();\n disposeRef.current = null;\n };\n }, [css, opts?.root]);\n}\n"],"mappings":";;;;AAgEA,SAAgB,UACd,cACA,eACA,SACM;CAEN,MAAM,YAAY,OAAO,iBAAiB;CAG1C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB;CAC9D,MAAM,OAAO,YACT,UACC;CAGL,MAAM,MAAM,cAER,YAAa,cAA+B,GAAI,cAElD,YAAa,QAAQ,EAAE,GAAI,CAAC,aAAa,CAC1C;CAED,MAAM,aAAa,OAA4B,KAAK;AAEpD,0BAAyB;AAEvB,aAAW,WAAW;AAEtB,MAAI,CAAC,IAAI,MAAM,EAAE;AACf,cAAW,UAAU;AACrB;;EAIF,MAAM,EAAE,YAAY,aAAa,KAAK,KAAK;AAC3C,aAAW,UAAU;AAGrB,eAAa;AACX,cAAW,WAAW;AACtB,cAAW,UAAU;;IAEtB,CAAC,KAAK,MAAM,KAAK,CAAC"}
1
+ {"version":3,"file":"useRawCSS.js","names":[],"sources":["../../src/hooks/useRawCSS.ts"],"sourcesContent":["import { useContext, useInsertionEffect, useMemo, useRef } from 'react';\n\nimport { injectRawCSS } from '../injector';\nimport { resolveSSRCollector } from './resolve-ssr-collector';\nimport { TastySSRContext } from '../ssr/context';\n\ninterface UseRawCSSOptions {\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to inject raw CSS text directly without parsing.\n * This is a low-overhead alternative for injecting global CSS that doesn't need tasty processing.\n *\n * The CSS is inserted into a separate style element (data-tasty-raw) to avoid conflicts\n * with tasty's chunked style sheets.\n *\n * @example Static CSS string\n * ```tsx\n * function GlobalStyles() {\n * useRawCSS(`\n * body {\n * margin: 0;\n * padding: 0;\n * font-family: sans-serif;\n * }\n * `);\n *\n * return null;\n * }\n * ```\n *\n * @example Factory function with dependencies (like useMemo)\n * ```tsx\n * function ThemeStyles({ theme }: { theme: 'light' | 'dark' }) {\n * useRawCSS(() => `\n * :root {\n * --bg-color: ${theme === 'dark' ? '#1a1a1a' : '#ffffff'};\n * --text-color: ${theme === 'dark' ? '#ffffff' : '#1a1a1a'};\n * }\n * `, [theme]);\n *\n * return null;\n * }\n * ```\n *\n * @example With options\n * ```tsx\n * function ShadowStyles({ shadowRoot }) {\n * useRawCSS(() => `.scoped { color: red; }`, [], { root: shadowRoot });\n * return null;\n * }\n * ```\n */\n\n// Overload 1: Static CSS string\nexport function useRawCSS(css: string, options?: UseRawCSSOptions): void;\n\n// Overload 2: Factory function with dependencies\nexport function useRawCSS(\n factory: () => string,\n deps: readonly unknown[],\n options?: UseRawCSSOptions,\n): void;\n\n// Implementation\nexport function useRawCSS(\n cssOrFactory: string | (() => string),\n depsOrOptions?: readonly unknown[] | UseRawCSSOptions,\n options?: UseRawCSSOptions,\n): void {\n const ssrContextValue = useContext(TastySSRContext);\n const ssrCollector = resolveSSRCollector(ssrContextValue);\n\n // Detect which overload is being used\n const isFactory = typeof cssOrFactory === 'function';\n\n // Parse arguments based on overload\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseRawCSSOptions | undefined);\n\n // Memoize CSS - for factory functions, use provided deps; for strings, use the string itself\n const css = useMemo(\n () =>\n isFactory ? (cssOrFactory as () => string)() : (cssOrFactory as string),\n\n isFactory ? (deps ?? []) : [cssOrFactory],\n );\n\n // SSR path: collect raw CSS during render\n useMemo(() => {\n if (!ssrCollector || !css.trim()) return;\n\n const key = `raw:${css.length}:${css.slice(0, 64)}`;\n ssrCollector.collectRawCSS(key, css);\n }, [ssrCollector, css]);\n\n const disposeRef = useRef<(() => void) | null>(null);\n\n // Client path: inject via DOM\n useInsertionEffect(() => {\n disposeRef.current?.();\n\n if (!css.trim()) {\n disposeRef.current = null;\n return;\n }\n\n const { dispose } = injectRawCSS(css, opts);\n disposeRef.current = dispose;\n\n return () => {\n disposeRef.current?.();\n disposeRef.current = null;\n };\n }, [css, opts?.root]);\n}\n"],"mappings":";;;;;;AAkEA,SAAgB,UACd,cACA,eACA,SACM;CAEN,MAAM,eAAe,oBADG,WAAW,gBAAgB,CACM;CAGzD,MAAM,YAAY,OAAO,iBAAiB;CAG1C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB;CAC9D,MAAM,OAAO,YACT,UACC;CAGL,MAAM,MAAM,cAER,YAAa,cAA+B,GAAI,cAElD,YAAa,QAAQ,EAAE,GAAI,CAAC,aAAa,CAC1C;AAGD,eAAc;AACZ,MAAI,CAAC,gBAAgB,CAAC,IAAI,MAAM,CAAE;EAElC,MAAM,MAAM,OAAO,IAAI,OAAO,GAAG,IAAI,MAAM,GAAG,GAAG;AACjD,eAAa,cAAc,KAAK,IAAI;IACnC,CAAC,cAAc,IAAI,CAAC;CAEvB,MAAM,aAAa,OAA4B,KAAK;AAGpD,0BAAyB;AACvB,aAAW,WAAW;AAEtB,MAAI,CAAC,IAAI,MAAM,EAAE;AACf,cAAW,UAAU;AACrB;;EAGF,MAAM,EAAE,YAAY,aAAa,KAAK,KAAK;AAC3C,aAAW,UAAU;AAErB,eAAa;AACX,cAAW,WAAW;AACtB,cAAW,UAAU;;IAEtB,CAAC,KAAK,MAAM,KAAK,CAAC"}
@@ -18,6 +18,11 @@ interface UseStylesResult {
18
18
  * Hook to generate CSS classes from Tasty styles.
19
19
  * Handles style rendering, className allocation, and CSS injection.
20
20
  *
21
+ * SSR-aware: when a ServerStyleCollector is available (via React context
22
+ * or AsyncLocalStorage), CSS is collected during the render phase instead
23
+ * of being injected into the DOM. useInsertionEffect does not run on the
24
+ * server, so the collector path is the only active path during SSR.
25
+ *
21
26
  * Uses chunking to split styles into logical groups for better caching
22
27
  * and CSS reuse across components.
23
28
  *
@@ -1,35 +1,38 @@
1
- import { stringifyStyles } from "../utils/styles.js";
2
1
  import { extractLocalProperties, hasLocalProperties } from "../properties/index.js";
3
- import { getGlobalKeyframes, hasGlobalKeyframes } from "../config.js";
4
- import { CHUNK_NAMES, categorizeStyleKeys } from "../chunks/definitions.js";
2
+ import { stringifyStyles } from "../utils/styles.js";
3
+ import { extractLocalFontFace, fontFaceContentHash, formatFontFaceRule, hasLocalFontFace } from "../font-face/index.js";
4
+ import { extractLocalCounterStyle, formatCounterStyleRule, hasLocalCounterStyle } from "../counter-style/index.js";
5
+ import { getConfig, getGlobalKeyframes, hasGlobalKeyframes } from "../config.js";
6
+ import { categorizeStyleKeys } from "../chunks/definitions.js";
5
7
  import { generateChunkCacheKey } from "../chunks/cacheKey.js";
6
8
  import { renderStylesForChunk } from "../chunks/renderChunk.js";
7
- import { allocateClassName, inject, keyframes, property } from "../injector/index.js";
9
+ import { allocateClassName, counterStyle, fontFace, inject, keyframes, property } from "../injector/index.js";
8
10
  import { resolveRecipes } from "../utils/resolve-recipes.js";
9
11
  import { extractAnimationNamesFromStyles, extractLocalKeyframes, filterUsedKeyframes, hasLocalKeyframes, mergeKeyframes, replaceAnimationNames } from "../keyframes/index.js";
10
- import { useInsertionEffect, useMemo, useRef } from "react";
12
+ import { formatPropertyCSS } from "../ssr/format-property.js";
13
+ import { collectAutoInferredProperties } from "../ssr/collect-auto-properties.js";
14
+ import { resolveSSRCollector } from "./resolve-ssr-collector.js";
15
+ import { TastySSRContext } from "../ssr/context.js";
16
+ import { formatKeyframesCSS } from "../ssr/format-keyframes.js";
17
+ import { hasKeys } from "../utils/has-keys.js";
18
+ import { useContext, useInsertionEffect, useMemo, useRef } from "react";
11
19
 
12
20
  //#region src/hooks/useStyles.ts
13
21
  /**
14
- * Check if styles contain @starting-style rules.
15
- *
16
- * @starting-style CSS cannot be applied via multiple class names because
17
- * of cascade - later rules override earlier ones. When @starting is detected,
18
- * we combine top-level styles into a single chunk but keep sub-element styles
19
- * in their own chunk for better caching.
20
- */
21
- function containsStartingStyle(styleKey) {
22
- return styleKey.includes("@starting");
23
- }
24
- /**
25
22
  * Render, cache-key, and allocate a className for a single chunk.
26
23
  * Returns a ProcessedChunk, or null if the chunk produces no CSS rules.
24
+ *
25
+ * Always runs the pipeline and calls allocateClassName. The inject()
26
+ * call in useInsertionEffect handles all edge cases: placeholders from
27
+ * abandoned concurrent renders, hydration hits (ruleIndex -2), and
28
+ * runtime cache hits (already injected). The pipeline's own LRU cache
29
+ * makes repeated calls for identical styles cheap.
27
30
  */
28
31
  function processChunk(styles, chunkName, styleKeys) {
29
32
  if (styleKeys.length === 0) return null;
30
- const renderResult = renderStylesForChunk(styles, chunkName, styleKeys);
31
- if (renderResult.rules.length === 0) return null;
32
33
  const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys);
34
+ const renderResult = renderStylesForChunk(styles, chunkName, styleKeys, cacheKey);
35
+ if (renderResult.rules.length === 0) return null;
33
36
  const { className } = allocateClassName(cacheKey);
34
37
  return {
35
38
  name: chunkName,
@@ -40,26 +43,6 @@ function processChunk(styles, chunkName, styleKeys) {
40
43
  };
41
44
  }
42
45
  /**
43
- * Merge chunk map entries for @starting-style partial chunking.
44
- *
45
- * All non-subcomponent chunks are merged into a single COMBINED entry,
46
- * while SUBCOMPONENTS stays separate. This preserves CSS cascade for
47
- * @starting-style while still allowing sub-element styles to cache independently.
48
- */
49
- function mergeChunksForStartingStyle(chunkMap) {
50
- const merged = /* @__PURE__ */ new Map();
51
- const combinedKeys = [];
52
- for (const [chunkName, keys] of chunkMap) if (chunkName === CHUNK_NAMES.SUBCOMPONENTS) merged.set(CHUNK_NAMES.SUBCOMPONENTS, keys);
53
- else combinedKeys.push(...keys);
54
- if (combinedKeys.length > 0) {
55
- const result = /* @__PURE__ */ new Map();
56
- result.set(CHUNK_NAMES.COMBINED, combinedKeys);
57
- for (const [k, v] of merged) result.set(k, v);
58
- return result;
59
- }
60
- return merged;
61
- }
62
- /**
63
46
  * Get keyframes that are actually used in styles.
64
47
  * Returns null if no keyframes are used (fast path for zero overhead).
65
48
  *
@@ -77,9 +60,44 @@ function getUsedKeyframes(styles) {
77
60
  return filterUsedKeyframes(mergeKeyframes(hasLocal ? extractLocalKeyframes(styles) : null, hasGlobal ? getGlobalKeyframes() : null), usedNames);
78
61
  }
79
62
  /**
63
+ * Process a chunk on the SSR path: allocate via collector, render, collect CSS.
64
+ * Returns null if the chunk produces no CSS rules.
65
+ */
66
+ function processChunkSSR(collector, styles, chunkName, styleKeys) {
67
+ if (styleKeys.length === 0) return null;
68
+ const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys);
69
+ const { className, isNewAllocation } = collector.allocateClassName(cacheKey);
70
+ if (isNewAllocation) {
71
+ const renderResult = renderStylesForChunk(styles, chunkName, styleKeys);
72
+ if (renderResult.rules.length > 0) {
73
+ collector.collectChunk(cacheKey, className, renderResult.rules);
74
+ return {
75
+ name: chunkName,
76
+ styleKeys,
77
+ cacheKey,
78
+ renderResult,
79
+ className
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+ return {
85
+ name: chunkName,
86
+ styleKeys,
87
+ cacheKey,
88
+ renderResult: { rules: [] },
89
+ className
90
+ };
91
+ }
92
+ /**
80
93
  * Hook to generate CSS classes from Tasty styles.
81
94
  * Handles style rendering, className allocation, and CSS injection.
82
95
  *
96
+ * SSR-aware: when a ServerStyleCollector is available (via React context
97
+ * or AsyncLocalStorage), CSS is collected during the render phase instead
98
+ * of being injected into the DOM. useInsertionEffect does not run on the
99
+ * server, so the collector path is the only active path during SSR.
100
+ *
83
101
  * Uses chunking to split styles into logical groups for better caching
84
102
  * and CSS reuse across components.
85
103
  *
@@ -97,6 +115,7 @@ function getUsedKeyframes(styles) {
97
115
  * ```
98
116
  */
99
117
  function useStyles(styles) {
118
+ const ssrCollector = resolveSSRCollector(useContext(TastySSRContext));
100
119
  const disposeRef = useRef([]);
101
120
  const stylesRef = useRef({
102
121
  key: "",
@@ -107,7 +126,7 @@ function useStyles(styles) {
107
126
  return resolveRecipes(styles);
108
127
  }, [styles]);
109
128
  const styleKey = useMemo(() => {
110
- if (!resolvedStyles || Object.keys(resolvedStyles).length === 0) return "";
129
+ if (!resolvedStyles || !hasKeys(resolvedStyles)) return "";
111
130
  return stringifyStyles(resolvedStyles);
112
131
  }, [resolvedStyles]);
113
132
  if (stylesRef.current.key !== styleKey) stylesRef.current = {
@@ -117,10 +136,49 @@ function useStyles(styles) {
117
136
  const processedChunks = useMemo(() => {
118
137
  const currentStyles = stylesRef.current.styles;
119
138
  if (!styleKey || !currentStyles) return [];
120
- let chunkMap = categorizeStyleKeys(currentStyles);
121
- if (containsStartingStyle(styleKey)) chunkMap = mergeChunksForStartingStyle(chunkMap);
139
+ const chunkMap = categorizeStyleKeys(currentStyles);
122
140
  const chunks = [];
123
- for (const [chunkName, chunkStyleKeys] of chunkMap) {
141
+ if (ssrCollector) {
142
+ ssrCollector.collectInternals();
143
+ for (const [chunkName, chunkStyleKeys] of chunkMap) {
144
+ const chunk = processChunkSSR(ssrCollector, currentStyles, chunkName, chunkStyleKeys);
145
+ if (chunk) chunks.push(chunk);
146
+ }
147
+ const usedKeyframes = getUsedKeyframes(currentStyles);
148
+ if (usedKeyframes) for (const [name, steps] of Object.entries(usedKeyframes)) {
149
+ const css = formatKeyframesCSS(name, steps);
150
+ ssrCollector.collectKeyframes(name, css);
151
+ }
152
+ if (hasLocalProperties(currentStyles)) {
153
+ const localProperties = extractLocalProperties(currentStyles);
154
+ if (localProperties) for (const [token, definition] of Object.entries(localProperties)) {
155
+ const css = formatPropertyCSS(token, definition);
156
+ if (css) ssrCollector.collectProperty(token, css);
157
+ }
158
+ }
159
+ if (hasLocalFontFace(currentStyles)) {
160
+ const localFontFace = extractLocalFontFace(currentStyles);
161
+ if (localFontFace) for (const [family, input] of Object.entries(localFontFace)) {
162
+ const descriptors = Array.isArray(input) ? input : [input];
163
+ for (const desc of descriptors) {
164
+ const hash = fontFaceContentHash(family, desc);
165
+ const css = formatFontFaceRule(family, desc);
166
+ ssrCollector.collectFontFace(hash, css);
167
+ }
168
+ }
169
+ }
170
+ if (hasLocalCounterStyle(currentStyles)) {
171
+ const localCounterStyle = extractLocalCounterStyle(currentStyles);
172
+ if (localCounterStyle) for (const [name, descriptors] of Object.entries(localCounterStyle)) {
173
+ const css = formatCounterStyleRule(name, descriptors);
174
+ ssrCollector.collectCounterStyle(name, css);
175
+ }
176
+ }
177
+ if (getConfig().autoPropertyTypes !== false) {
178
+ const allRules = chunks.flatMap((c) => c.renderResult.rules);
179
+ if (allRules.length > 0) collectAutoInferredProperties(allRules, ssrCollector, currentStyles);
180
+ }
181
+ } else for (const [chunkName, chunkStyleKeys] of chunkMap) {
124
182
  const chunk = processChunk(currentStyles, chunkName, chunkStyleKeys);
125
183
  if (chunk) chunks.push(chunk);
126
184
  }
@@ -131,6 +189,21 @@ function useStyles(styles) {
131
189
  disposeRef.current = [];
132
190
  if (processedChunks.length === 0) return;
133
191
  const currentStyles = stylesRef.current.styles;
192
+ if (currentStyles && hasLocalProperties(currentStyles)) {
193
+ const localProperties = extractLocalProperties(currentStyles);
194
+ if (localProperties) for (const [token, definition] of Object.entries(localProperties)) property(token, definition);
195
+ }
196
+ if (currentStyles && hasLocalFontFace(currentStyles)) {
197
+ const localFontFace = extractLocalFontFace(currentStyles);
198
+ if (localFontFace) for (const [family, input] of Object.entries(localFontFace)) {
199
+ const descriptors = Array.isArray(input) ? input : [input];
200
+ for (const desc of descriptors) fontFace(family, desc);
201
+ }
202
+ }
203
+ if (currentStyles && hasLocalCounterStyle(currentStyles)) {
204
+ const localCounterStyle = extractLocalCounterStyle(currentStyles);
205
+ if (localCounterStyle) for (const [name, descriptors] of Object.entries(localCounterStyle)) counterStyle(name, descriptors);
206
+ }
134
207
  const usedKeyframes = currentStyles ? getUsedKeyframes(currentStyles) : null;
135
208
  let nameMap = null;
136
209
  if (usedKeyframes) {
@@ -143,10 +216,6 @@ function useStyles(styles) {
143
216
  }
144
217
  if (nameMap.size === 0) nameMap = null;
145
218
  }
146
- if (currentStyles && hasLocalProperties(currentStyles)) {
147
- const localProperties = extractLocalProperties(currentStyles);
148
- if (localProperties) for (const [token, definition] of Object.entries(localProperties)) property(token, definition);
149
- }
150
219
  for (const chunk of processedChunks) if (chunk.renderResult.rules.length > 0) {
151
220
  const { dispose } = inject(nameMap ? chunk.renderResult.rules.map((rule) => ({
152
221
  ...rule,
@@ -1 +1 @@
1
- {"version":3,"file":"useStyles.js","names":[],"sources":["../../src/hooks/useStyles.ts"],"sourcesContent":["import { useInsertionEffect, useMemo, useRef } from 'react';\n\nimport {\n categorizeStyleKeys,\n CHUNK_NAMES,\n generateChunkCacheKey,\n renderStylesForChunk,\n} from '../chunks';\nimport { getGlobalKeyframes, hasGlobalKeyframes } from '../config';\nimport { allocateClassName, inject, keyframes, property } from '../injector';\nimport type { KeyframesSteps } from '../injector/types';\nimport {\n extractAnimationNamesFromStyles,\n extractLocalKeyframes,\n filterUsedKeyframes,\n hasLocalKeyframes,\n mergeKeyframes,\n replaceAnimationNames,\n} from '../keyframes';\nimport type { RenderResult } from '../pipeline';\nimport { extractLocalProperties, hasLocalProperties } from '../properties';\nimport type { Styles } from '../styles/types';\nimport { resolveRecipes } from '../utils/resolve-recipes';\nimport { stringifyStyles } from '../utils/styles';\n\n/**\n * Check if styles contain @starting-style rules.\n *\n * @starting-style CSS cannot be applied via multiple class names because\n * of cascade - later rules override earlier ones. When @starting is detected,\n * we combine top-level styles into a single chunk but keep sub-element styles\n * in their own chunk for better caching.\n */\nfunction containsStartingStyle(styleKey: string): boolean {\n return styleKey.includes('@starting');\n}\n\n/**\n * Tasty styles object to generate CSS classes for.\n * Can be undefined or empty object for no styles.\n */\nexport type UseStylesOptions = Styles | undefined;\n\nexport interface UseStylesResult {\n /**\n * Generated className(s) to apply to the element.\n * Can be empty string if no styles are provided.\n * With chunking enabled, may contain multiple space-separated class names.\n */\n className: string;\n}\n\n/**\n * Information about a processed chunk\n */\ninterface ProcessedChunk {\n name: string;\n styleKeys: string[];\n cacheKey: string;\n renderResult: RenderResult;\n className: string;\n}\n\n/**\n * Render, cache-key, and allocate a className for a single chunk.\n * Returns a ProcessedChunk, or null if the chunk produces no CSS rules.\n */\nfunction processChunk(\n styles: Styles,\n chunkName: string,\n styleKeys: string[],\n): ProcessedChunk | null {\n if (styleKeys.length === 0) return null;\n\n const renderResult = renderStylesForChunk(styles, chunkName, styleKeys);\n if (renderResult.rules.length === 0) return null;\n\n const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys);\n const { className } = allocateClassName(cacheKey);\n\n return { name: chunkName, styleKeys, cacheKey, renderResult, className };\n}\n\n/**\n * Merge chunk map entries for @starting-style partial chunking.\n *\n * All non-subcomponent chunks are merged into a single COMBINED entry,\n * while SUBCOMPONENTS stays separate. This preserves CSS cascade for\n * @starting-style while still allowing sub-element styles to cache independently.\n */\nfunction mergeChunksForStartingStyle(\n chunkMap: Map<string, string[]>,\n): Map<string, string[]> {\n const merged = new Map<string, string[]>();\n const combinedKeys: string[] = [];\n\n for (const [chunkName, keys] of chunkMap) {\n if (chunkName === CHUNK_NAMES.SUBCOMPONENTS) {\n merged.set(CHUNK_NAMES.SUBCOMPONENTS, keys);\n } else {\n combinedKeys.push(...keys);\n }\n }\n\n if (combinedKeys.length > 0) {\n // Insert COMBINED first so it appears before SUBCOMPONENTS\n const result = new Map<string, string[]>();\n result.set(CHUNK_NAMES.COMBINED, combinedKeys);\n for (const [k, v] of merged) {\n result.set(k, v);\n }\n return result;\n }\n\n return merged;\n}\n\n/**\n * Get keyframes that are actually used in styles.\n * Returns null if no keyframes are used (fast path for zero overhead).\n *\n * Optimization order:\n * 1. Check if any keyframes are defined (local or global) - if not, return null\n * 2. Extract animation names from styles - if none, return null\n * 3. Merge and filter keyframes to only used ones\n */\nfunction getUsedKeyframes(\n styles: Styles,\n): Record<string, KeyframesSteps> | null {\n // Fast path: no keyframes defined anywhere\n const hasLocal = hasLocalKeyframes(styles);\n const hasGlobal = hasGlobalKeyframes();\n if (!hasLocal && !hasGlobal) return null;\n\n // Extract animation names from styles (not from rendered CSS - faster)\n const usedNames = extractAnimationNamesFromStyles(styles);\n if (usedNames.size === 0) return null;\n\n // Merge local and global keyframes\n const local = hasLocal ? extractLocalKeyframes(styles) : null;\n const global = hasGlobal ? getGlobalKeyframes() : null;\n const allKeyframes = mergeKeyframes(local, global);\n\n // Filter to only used keyframes\n return filterUsedKeyframes(allKeyframes, usedNames);\n}\n\n/**\n * Hook to generate CSS classes from Tasty styles.\n * Handles style rendering, className allocation, and CSS injection.\n *\n * Uses chunking to split styles into logical groups for better caching\n * and CSS reuse across components.\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { className } = useStyles({\n * padding: '2x',\n * fill: '#purple',\n * radius: '1r',\n * });\n *\n * return <div className={className}>Styled content</div>;\n * }\n * ```\n */\nexport function useStyles(styles: UseStylesOptions): UseStylesResult {\n // Array of dispose functions for each chunk\n const disposeRef = useRef<(() => void)[]>([]);\n\n // Store styles by their stringified key to avoid recomputing when only reference changes\n const stylesRef = useRef<{ key: string; styles: Styles | undefined }>({\n key: '',\n styles: undefined,\n });\n\n // Resolve recipes before any processing (zero overhead if no recipes configured)\n const resolvedStyles = useMemo(() => {\n if (!styles) return styles;\n return resolveRecipes(styles);\n }, [styles]);\n\n // Compute style key - this is a primitive string that captures style content\n const styleKey = useMemo(() => {\n if (!resolvedStyles || Object.keys(resolvedStyles).length === 0) {\n return '';\n }\n return stringifyStyles(resolvedStyles);\n }, [resolvedStyles]);\n\n // Update ref when styleKey changes (content actually changed)\n if (stylesRef.current.key !== styleKey) {\n stylesRef.current = { key: styleKey, styles: resolvedStyles };\n }\n\n // Process chunks: categorize, generate cache keys, render, and allocate classNames\n // Only depends on styleKey (primitive), not styles object reference\n const processedChunks: ProcessedChunk[] = useMemo(() => {\n const currentStyles = stylesRef.current.styles;\n if (!styleKey || !currentStyles) {\n return [];\n }\n\n // Categorize style keys into chunks\n let chunkMap = categorizeStyleKeys(\n currentStyles as Record<string, unknown>,\n );\n\n // Partial chunking for styles containing @starting-style rules.\n // @starting-style CSS cannot work with multiple class names due to cascade,\n // so we merge all top-level chunks into one but keep sub-element styles separate.\n if (containsStartingStyle(styleKey)) {\n chunkMap = mergeChunksForStartingStyle(chunkMap);\n }\n\n // Process each chunk: render → cache key → allocate className\n const chunks: ProcessedChunk[] = [];\n\n for (const [chunkName, chunkStyleKeys] of chunkMap) {\n const chunk = processChunk(currentStyles, chunkName, chunkStyleKeys);\n if (chunk) {\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }, [styleKey]);\n\n // Inject styles in insertion effect (avoids render phase side effects)\n useInsertionEffect(() => {\n // Cleanup all previous disposals\n disposeRef.current.forEach((dispose) => dispose?.());\n disposeRef.current = [];\n\n // Fast path: no chunks to inject\n if (processedChunks.length === 0) {\n return;\n }\n\n const currentStyles = stylesRef.current.styles;\n\n // Get keyframes that are actually used (returns null if none - zero overhead)\n const usedKeyframes = currentStyles\n ? getUsedKeyframes(currentStyles)\n : null;\n\n // Inject keyframes and build name map (only if we have keyframes)\n let nameMap: Map<string, string> | null = null;\n\n if (usedKeyframes) {\n nameMap = new Map();\n for (const [name, steps] of Object.entries(usedKeyframes)) {\n const result = keyframes(steps, { name });\n const injectedName = result.toString();\n // Only add to map if name differs (optimization for replacement check)\n if (injectedName !== name) {\n nameMap.set(name, injectedName);\n }\n disposeRef.current.push(result.dispose);\n }\n // Clear map if no replacements needed\n if (nameMap.size === 0) {\n nameMap = null;\n }\n }\n\n // Register local @properties if defined (no dispose needed - properties are permanent)\n // Token formats: $name → --name, #name → --name-color (with auto syntax: '<color>')\n // The injector.property() handles token parsing and auto-settings internally\n // Note: Global properties are injected once when styles are first generated (see markStylesGenerated)\n if (currentStyles && hasLocalProperties(currentStyles)) {\n const localProperties = extractLocalProperties(currentStyles);\n if (localProperties) {\n for (const [token, definition] of Object.entries(localProperties)) {\n // Pass the token directly - injector handles parsing\n property(token, definition);\n }\n }\n }\n\n // Inject each chunk\n for (const chunk of processedChunks) {\n if (chunk.renderResult.rules.length > 0) {\n // Replace animation names only if needed\n const rulesToInject = nameMap\n ? chunk.renderResult.rules.map((rule) => ({\n ...rule,\n declarations: replaceAnimationNames(rule.declarations, nameMap!),\n }))\n : chunk.renderResult.rules;\n\n const { dispose } = inject(rulesToInject, {\n cacheKey: chunk.cacheKey,\n });\n disposeRef.current.push(dispose);\n }\n }\n\n return () => {\n disposeRef.current.forEach((dispose) => dispose?.());\n disposeRef.current = [];\n };\n }, [processedChunks]);\n\n // Combine all chunk classNames\n const className = useMemo(() => {\n return processedChunks.map((chunk) => chunk.className).join(' ');\n }, [processedChunks]);\n\n return {\n className,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useStyles.js","names":[],"sources":["../../src/hooks/useStyles.ts"],"sourcesContent":["import { useContext, useInsertionEffect, useMemo, useRef } from 'react';\n\nimport {\n categorizeStyleKeys,\n generateChunkCacheKey,\n renderStylesForChunk,\n} from '../chunks';\nimport { getConfig, getGlobalKeyframes, hasGlobalKeyframes } from '../config';\nimport {\n allocateClassName,\n counterStyle,\n fontFace,\n inject,\n keyframes,\n property,\n} from '../injector';\nimport type { FontFaceDescriptors, KeyframesSteps } from '../injector/types';\nimport {\n extractLocalCounterStyle,\n formatCounterStyleRule,\n hasLocalCounterStyle,\n} from '../counter-style';\nimport {\n extractLocalFontFace,\n fontFaceContentHash,\n formatFontFaceRule,\n hasLocalFontFace,\n} from '../font-face';\nimport {\n extractAnimationNamesFromStyles,\n extractLocalKeyframes,\n filterUsedKeyframes,\n hasLocalKeyframes,\n mergeKeyframes,\n replaceAnimationNames,\n} from '../keyframes';\nimport type { RenderResult } from '../pipeline';\nimport { extractLocalProperties, hasLocalProperties } from '../properties';\nimport { collectAutoInferredProperties } from '../ssr/collect-auto-properties';\nimport type { ServerStyleCollector } from '../ssr/collector';\nimport { resolveSSRCollector } from './resolve-ssr-collector';\nimport { TastySSRContext } from '../ssr/context';\nimport { formatKeyframesCSS } from '../ssr/format-keyframes';\nimport { formatPropertyCSS } from '../ssr/format-property';\nimport type { Styles } from '../styles/types';\nimport { hasKeys } from '../utils/has-keys';\nimport { resolveRecipes } from '../utils/resolve-recipes';\nimport { stringifyStyles } from '../utils/styles';\n\n/**\n * Tasty styles object to generate CSS classes for.\n * Can be undefined or empty object for no styles.\n */\nexport type UseStylesOptions = Styles | undefined;\n\nexport interface UseStylesResult {\n /**\n * Generated className(s) to apply to the element.\n * Can be empty string if no styles are provided.\n * With chunking enabled, may contain multiple space-separated class names.\n */\n className: string;\n}\n\n/**\n * Information about a processed chunk\n */\ninterface ProcessedChunk {\n name: string;\n styleKeys: string[];\n cacheKey: string;\n renderResult: RenderResult;\n className: string;\n}\n\n/**\n * Render, cache-key, and allocate a className for a single chunk.\n * Returns a ProcessedChunk, or null if the chunk produces no CSS rules.\n *\n * Always runs the pipeline and calls allocateClassName. The inject()\n * call in useInsertionEffect handles all edge cases: placeholders from\n * abandoned concurrent renders, hydration hits (ruleIndex -2), and\n * runtime cache hits (already injected). The pipeline's own LRU cache\n * makes repeated calls for identical styles cheap.\n */\nfunction processChunk(\n styles: Styles,\n chunkName: string,\n styleKeys: string[],\n): ProcessedChunk | null {\n if (styleKeys.length === 0) return null;\n\n const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys);\n const renderResult = renderStylesForChunk(\n styles,\n chunkName,\n styleKeys,\n cacheKey,\n );\n if (renderResult.rules.length === 0) return null;\n\n const { className } = allocateClassName(cacheKey);\n\n return { name: chunkName, styleKeys, cacheKey, renderResult, className };\n}\n\n/**\n * Get keyframes that are actually used in styles.\n * Returns null if no keyframes are used (fast path for zero overhead).\n *\n * Optimization order:\n * 1. Check if any keyframes are defined (local or global) - if not, return null\n * 2. Extract animation names from styles - if none, return null\n * 3. Merge and filter keyframes to only used ones\n */\nfunction getUsedKeyframes(\n styles: Styles,\n): Record<string, KeyframesSteps> | null {\n // Fast path: no keyframes defined anywhere\n const hasLocal = hasLocalKeyframes(styles);\n const hasGlobal = hasGlobalKeyframes();\n if (!hasLocal && !hasGlobal) return null;\n\n // Extract animation names from styles (not from rendered CSS - faster)\n const usedNames = extractAnimationNamesFromStyles(styles);\n if (usedNames.size === 0) return null;\n\n // Merge local and global keyframes\n const local = hasLocal ? extractLocalKeyframes(styles) : null;\n const global = hasGlobal ? getGlobalKeyframes() : null;\n const allKeyframes = mergeKeyframes(local, global);\n\n // Filter to only used keyframes\n return filterUsedKeyframes(allKeyframes, usedNames);\n}\n\n/**\n * Process a chunk on the SSR path: allocate via collector, render, collect CSS.\n * Returns null if the chunk produces no CSS rules.\n */\nfunction processChunkSSR(\n collector: ServerStyleCollector,\n styles: Styles,\n chunkName: string,\n styleKeys: string[],\n): ProcessedChunk | null {\n if (styleKeys.length === 0) return null;\n\n const cacheKey = generateChunkCacheKey(styles, chunkName, styleKeys);\n const { className, isNewAllocation } = collector.allocateClassName(cacheKey);\n\n if (isNewAllocation) {\n const renderResult = renderStylesForChunk(styles, chunkName, styleKeys);\n if (renderResult.rules.length > 0) {\n collector.collectChunk(cacheKey, className, renderResult.rules);\n return {\n name: chunkName,\n styleKeys,\n cacheKey,\n renderResult,\n className,\n };\n }\n return null;\n }\n\n return {\n name: chunkName,\n styleKeys,\n cacheKey,\n renderResult: { rules: [] },\n className,\n };\n}\n\n/**\n * Hook to generate CSS classes from Tasty styles.\n * Handles style rendering, className allocation, and CSS injection.\n *\n * SSR-aware: when a ServerStyleCollector is available (via React context\n * or AsyncLocalStorage), CSS is collected during the render phase instead\n * of being injected into the DOM. useInsertionEffect does not run on the\n * server, so the collector path is the only active path during SSR.\n *\n * Uses chunking to split styles into logical groups for better caching\n * and CSS reuse across components.\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { className } = useStyles({\n * padding: '2x',\n * fill: '#purple',\n * radius: '1r',\n * });\n *\n * return <div className={className}>Styled content</div>;\n * }\n * ```\n */\nexport function useStyles(styles: UseStylesOptions): UseStylesResult {\n const ssrContextValue = useContext(TastySSRContext);\n const ssrCollector = resolveSSRCollector(ssrContextValue);\n\n // Array of dispose functions for each chunk\n const disposeRef = useRef<(() => void)[]>([]);\n\n // Store styles by their stringified key to avoid recomputing when only reference changes\n const stylesRef = useRef<{ key: string; styles: Styles | undefined }>({\n key: '',\n styles: undefined,\n });\n\n // Resolve recipes before any processing (zero overhead if no recipes configured)\n const resolvedStyles = useMemo(() => {\n if (!styles) return styles;\n return resolveRecipes(styles);\n }, [styles]);\n\n // Compute style key - this is a primitive string that captures style content\n const styleKey = useMemo(() => {\n if (!resolvedStyles || !hasKeys(resolvedStyles)) {\n return '';\n }\n return stringifyStyles(resolvedStyles);\n }, [resolvedStyles]);\n\n // Update ref when styleKey changes (content actually changed)\n if (stylesRef.current.key !== styleKey) {\n stylesRef.current = { key: styleKey, styles: resolvedStyles };\n }\n\n // Process chunks: categorize, generate cache keys, render, and allocate classNames\n // Only depends on styleKey (primitive), not styles object reference\n const processedChunks: ProcessedChunk[] = useMemo(() => {\n const currentStyles = stylesRef.current.styles;\n if (!styleKey || !currentStyles) {\n return [];\n }\n\n // Categorize style keys into chunks\n const chunkMap = categorizeStyleKeys(\n currentStyles as Record<string, unknown>,\n );\n\n const chunks: ProcessedChunk[] = [];\n\n if (ssrCollector) {\n // Ensure internal @property and :root token CSS is included\n ssrCollector.collectInternals();\n\n // SERVER PATH: allocate via collector, collect CSS\n for (const [chunkName, chunkStyleKeys] of chunkMap) {\n const chunk = processChunkSSR(\n ssrCollector,\n currentStyles,\n chunkName,\n chunkStyleKeys,\n );\n if (chunk) chunks.push(chunk);\n }\n\n // Collect keyframes on the server\n const usedKeyframes = getUsedKeyframes(currentStyles);\n if (usedKeyframes) {\n for (const [name, steps] of Object.entries(usedKeyframes)) {\n const css = formatKeyframesCSS(name, steps);\n ssrCollector.collectKeyframes(name, css);\n }\n }\n\n // Collect explicit @property rules on the server\n if (hasLocalProperties(currentStyles)) {\n const localProperties = extractLocalProperties(currentStyles);\n if (localProperties) {\n for (const [token, definition] of Object.entries(localProperties)) {\n const css = formatPropertyCSS(token, definition);\n if (css) {\n ssrCollector.collectProperty(token, css);\n }\n }\n }\n }\n\n // Collect @font-face rules on the server\n if (hasLocalFontFace(currentStyles)) {\n const localFontFace = extractLocalFontFace(currentStyles);\n if (localFontFace) {\n for (const [family, input] of Object.entries(localFontFace)) {\n const descriptors: FontFaceDescriptors[] = Array.isArray(input)\n ? input\n : [input];\n for (const desc of descriptors) {\n const hash = fontFaceContentHash(family, desc);\n const css = formatFontFaceRule(family, desc);\n ssrCollector.collectFontFace(hash, css);\n }\n }\n }\n }\n\n // Collect @counter-style rules on the server\n if (hasLocalCounterStyle(currentStyles)) {\n const localCounterStyle = extractLocalCounterStyle(currentStyles);\n if (localCounterStyle) {\n for (const [name, descriptors] of Object.entries(localCounterStyle)) {\n const css = formatCounterStyleRule(name, descriptors);\n ssrCollector.collectCounterStyle(name, css);\n }\n }\n }\n\n // Auto-infer @property types from rendered declarations (SSR).\n // On the client this happens inside StyleInjector.inject(), which\n // runs in useInsertionEffect and therefore never executes on the server.\n if (getConfig().autoPropertyTypes !== false) {\n const allRules = chunks.flatMap((c) => c.renderResult.rules);\n if (allRules.length > 0) {\n collectAutoInferredProperties(allRules, ssrCollector, currentStyles);\n }\n }\n } else {\n // CLIENT PATH: unchanged behavior\n for (const [chunkName, chunkStyleKeys] of chunkMap) {\n const chunk = processChunk(currentStyles, chunkName, chunkStyleKeys);\n if (chunk) chunks.push(chunk);\n }\n }\n\n return chunks;\n }, [styleKey]);\n\n // Inject styles in insertion effect (avoids render phase side effects).\n // Does NOT run on the server — the SSR path above handles collection.\n useInsertionEffect(() => {\n // Cleanup all previous disposals\n disposeRef.current.forEach((dispose) => dispose?.());\n disposeRef.current = [];\n\n // Fast path: no chunks to inject\n if (processedChunks.length === 0) {\n return;\n }\n\n const currentStyles = stylesRef.current.styles;\n\n // Register explicit @properties first so they take precedence over auto-inference\n // that happens during keyframe and style chunk injection.\n // Token formats: $name → --name, #name → --name-color (with auto syntax: '<color>')\n // Note: Global properties are injected once when styles are first generated (see markStylesGenerated)\n if (currentStyles && hasLocalProperties(currentStyles)) {\n const localProperties = extractLocalProperties(currentStyles);\n if (localProperties) {\n for (const [token, definition] of Object.entries(localProperties)) {\n property(token, definition);\n }\n }\n }\n\n // Inject @font-face rules (permanent, no cleanup needed)\n if (currentStyles && hasLocalFontFace(currentStyles)) {\n const localFontFace = extractLocalFontFace(currentStyles);\n if (localFontFace) {\n for (const [family, input] of Object.entries(localFontFace)) {\n const descriptors: FontFaceDescriptors[] = Array.isArray(input)\n ? input\n : [input];\n for (const desc of descriptors) {\n fontFace(family, desc);\n }\n }\n }\n }\n\n // Inject @counter-style rules (permanent, no cleanup needed)\n if (currentStyles && hasLocalCounterStyle(currentStyles)) {\n const localCounterStyle = extractLocalCounterStyle(currentStyles);\n if (localCounterStyle) {\n for (const [name, descriptors] of Object.entries(localCounterStyle)) {\n counterStyle(name, descriptors);\n }\n }\n }\n\n // Get keyframes that are actually used (returns null if none - zero overhead)\n const usedKeyframes = currentStyles\n ? getUsedKeyframes(currentStyles)\n : null;\n\n // Inject keyframes and build name map (only if we have keyframes)\n let nameMap: Map<string, string> | null = null;\n\n if (usedKeyframes) {\n nameMap = new Map();\n for (const [name, steps] of Object.entries(usedKeyframes)) {\n const result = keyframes(steps, { name });\n const injectedName = result.toString();\n // Only add to map if name differs (optimization for replacement check)\n if (injectedName !== name) {\n nameMap.set(name, injectedName);\n }\n disposeRef.current.push(result.dispose);\n }\n // Clear map if no replacements needed\n if (nameMap.size === 0) {\n nameMap = null;\n }\n }\n\n // Inject each chunk\n for (const chunk of processedChunks) {\n if (chunk.renderResult.rules.length > 0) {\n // Replace animation names only if needed\n const rulesToInject = nameMap\n ? chunk.renderResult.rules.map((rule) => ({\n ...rule,\n declarations: replaceAnimationNames(rule.declarations, nameMap!),\n }))\n : chunk.renderResult.rules;\n\n const { dispose } = inject(rulesToInject, {\n cacheKey: chunk.cacheKey,\n });\n disposeRef.current.push(dispose);\n }\n }\n\n return () => {\n disposeRef.current.forEach((dispose) => dispose?.());\n disposeRef.current = [];\n };\n }, [processedChunks]);\n\n // Combine all chunk classNames\n const className = useMemo(() => {\n return processedChunks.map((chunk) => chunk.className).join(' ');\n }, [processedChunks]);\n\n return {\n className,\n };\n}\n"],"mappings":"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"}