@tenphi/tasty 1.4.2 → 1.5.1

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 (71) hide show
  1. package/README.md +8 -7
  2. package/dist/compute-styles.js +6 -28
  3. package/dist/compute-styles.js.map +1 -1
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +2 -2
  8. package/dist/hooks/useCounterStyle.d.ts +3 -17
  9. package/dist/hooks/useCounterStyle.js +54 -35
  10. package/dist/hooks/useCounterStyle.js.map +1 -1
  11. package/dist/hooks/useFontFace.d.ts +3 -1
  12. package/dist/hooks/useFontFace.js +21 -24
  13. package/dist/hooks/useFontFace.js.map +1 -1
  14. package/dist/hooks/useGlobalStyles.d.ts +18 -2
  15. package/dist/hooks/useGlobalStyles.js +51 -40
  16. package/dist/hooks/useGlobalStyles.js.map +1 -1
  17. package/dist/hooks/useKeyframes.d.ts +4 -2
  18. package/dist/hooks/useKeyframes.js +41 -50
  19. package/dist/hooks/useKeyframes.js.map +1 -1
  20. package/dist/hooks/useProperty.d.ts +4 -2
  21. package/dist/hooks/useProperty.js +29 -41
  22. package/dist/hooks/useProperty.js.map +1 -1
  23. package/dist/hooks/useRawCSS.d.ts +13 -44
  24. package/dist/hooks/useRawCSS.js +90 -21
  25. package/dist/hooks/useRawCSS.js.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +2 -2
  28. package/dist/injector/index.d.ts +5 -10
  29. package/dist/injector/index.js +5 -12
  30. package/dist/injector/index.js.map +1 -1
  31. package/dist/injector/injector.d.ts +11 -13
  32. package/dist/injector/injector.js +50 -73
  33. package/dist/injector/injector.js.map +1 -1
  34. package/dist/injector/sheet-manager.js +2 -1
  35. package/dist/injector/sheet-manager.js.map +1 -1
  36. package/dist/injector/types.d.ts +21 -28
  37. package/dist/rsc-cache.js +81 -0
  38. package/dist/rsc-cache.js.map +1 -0
  39. package/dist/ssr/astro-client.d.ts +1 -0
  40. package/dist/ssr/astro-client.js +24 -0
  41. package/dist/ssr/astro-client.js.map +1 -0
  42. package/dist/ssr/astro-middleware.d.ts +15 -0
  43. package/dist/ssr/astro-middleware.js +19 -0
  44. package/dist/ssr/astro-middleware.js.map +1 -0
  45. package/dist/ssr/astro.d.ts +85 -8
  46. package/dist/ssr/astro.js +110 -20
  47. package/dist/ssr/astro.js.map +1 -1
  48. package/dist/ssr/async-storage.js +14 -4
  49. package/dist/ssr/async-storage.js.map +1 -1
  50. package/dist/ssr/collect-auto-properties.js +28 -9
  51. package/dist/ssr/collect-auto-properties.js.map +1 -1
  52. package/dist/ssr/index.d.ts +1 -1
  53. package/dist/ssr/ssr-collector-ref.js +4 -3
  54. package/dist/ssr/ssr-collector-ref.js.map +1 -1
  55. package/dist/tasty.d.ts +1 -1
  56. package/dist/utils/deps-equal.js +15 -0
  57. package/dist/utils/deps-equal.js.map +1 -0
  58. package/dist/utils/hash.js +14 -0
  59. package/dist/utils/hash.js.map +1 -0
  60. package/docs/adoption.md +1 -1
  61. package/docs/comparison.md +1 -1
  62. package/docs/configuration.md +1 -1
  63. package/docs/design-system.md +1 -1
  64. package/docs/dsl.md +21 -6
  65. package/docs/getting-started.md +1 -1
  66. package/docs/injector.md +25 -24
  67. package/docs/methodology.md +1 -1
  68. package/docs/runtime.md +12 -31
  69. package/docs/ssr.md +117 -36
  70. package/docs/tasty-static.md +1 -1
  71. package/package.json +8 -2
@@ -1,42 +1,61 @@
1
1
  import { formatCounterStyleRule } from "../counter-style/index.js";
2
2
  import { getGlobalInjector } from "../config.js";
3
- import { getRegisteredSSRCollector } from "../ssr/ssr-collector-ref.js";
4
- import { useInsertionEffect, useMemo } from "react";
3
+ import { getStyleTarget, pushRSCCSS } from "../rsc-cache.js";
5
4
  //#region src/hooks/useCounterStyle.ts
6
5
  let clientCounterStyleCounter = 0;
7
- function useCounterStyle(descriptorsOrFactory, depsOrOptions, options) {
8
- const ssrCollector = getRegisteredSSRCollector();
9
- const isFactory = typeof descriptorsOrFactory === "function";
10
- const deps = isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : void 0;
11
- const opts = isFactory ? options : depsOrOptions;
12
- const inputKey = useMemo(() => isFactory ? null : JSON.stringify(descriptorsOrFactory), [isFactory ? null : descriptorsOrFactory]);
13
- const descriptorsData = useMemo(() => {
14
- const descriptors = isFactory ? descriptorsOrFactory() : descriptorsOrFactory;
15
- if (!descriptors || !descriptors.system) return null;
16
- return descriptors;
17
- }, isFactory ? deps ?? [] : [inputKey]);
18
- const name = useMemo(() => {
19
- if (!descriptorsData) return "";
20
- if (ssrCollector) {
21
- const actualName = ssrCollector.allocateCounterStyleName(opts?.name);
22
- const css = formatCounterStyleRule(actualName, descriptorsData);
23
- ssrCollector.collectCounterStyle(actualName, css);
24
- return actualName;
25
- }
26
- return opts?.name ?? `cs${clientCounterStyleCounter++}`;
27
- }, [
28
- descriptorsData,
29
- opts?.name,
30
- ssrCollector
31
- ]);
32
- useInsertionEffect(() => {
33
- if (!descriptorsData || !name) return;
34
- getGlobalInjector().counterStyle(name, descriptorsData, { root: opts?.root });
35
- }, [
36
- descriptorsData,
37
- name,
38
- opts?.root
39
- ]);
6
+ const clientContentToName = /* @__PURE__ */ new Map();
7
+ /**
8
+ * Inject a CSS @counter-style rule and return the generated name.
9
+ * Permanent no cleanup on unmount. Deduplicates by name.
10
+ *
11
+ * Works in all environments: client, SSR with collector, and React Server Components.
12
+ *
13
+ * @example Basic usage
14
+ * ```tsx
15
+ * function EmojiList() {
16
+ * const styleName = useCounterStyle({
17
+ * system: 'cyclic',
18
+ * symbols: '"👍"',
19
+ * suffix: '" "',
20
+ * }, { name: 'thumbs' });
21
+ *
22
+ * return (
23
+ * <ol style={{ listStyleType: styleName }}>
24
+ * <li>First</li>
25
+ * <li>Second</li>
26
+ * </ol>
27
+ * );
28
+ * }
29
+ * ```
30
+ *
31
+ */
32
+ function useCounterStyle(descriptors, options) {
33
+ if (!descriptors || !descriptors.system) return "";
34
+ const target = getStyleTarget();
35
+ if (target.mode === "ssr") {
36
+ const actualName = target.collector.allocateCounterStyleName(options?.name);
37
+ const css = formatCounterStyleRule(actualName, descriptors);
38
+ target.collector.collectCounterStyle(actualName, css);
39
+ return actualName;
40
+ }
41
+ if (target.mode === "rsc") {
42
+ const serializedContent = JSON.stringify(descriptors);
43
+ const key = `__cs:${options?.name ?? ""}:${serializedContent}`;
44
+ const existingName = target.cache.generatedNames.get(key);
45
+ if (existingName) return existingName;
46
+ const actualName = options?.name ?? `cs${target.cache.counterStyleCounter++}`;
47
+ const css = formatCounterStyleRule(actualName, descriptors);
48
+ pushRSCCSS(target.cache, key, css);
49
+ target.cache.generatedNames.set(key, actualName);
50
+ return actualName;
51
+ }
52
+ const serializedContent = JSON.stringify(descriptors);
53
+ const cacheKey = `${options?.name ?? ""}:${serializedContent}`;
54
+ const existingName = clientContentToName.get(cacheKey);
55
+ if (existingName) return existingName;
56
+ const name = options?.name ?? `cs${clientCounterStyleCounter++}`;
57
+ clientContentToName.set(cacheKey, name);
58
+ getGlobalInjector().counterStyle(name, descriptors, { root: options?.root });
40
59
  return name;
41
60
  }
42
61
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useCounterStyle.js","names":[],"sources":["../../src/hooks/useCounterStyle.ts"],"sourcesContent":["import { useInsertionEffect, useMemo } from 'react';\n\nimport { getGlobalInjector } from '../config';\nimport { formatCounterStyleRule } from '../counter-style';\nimport type { CounterStyleDescriptors } from '../injector/types';\nimport { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';\n\ninterface UseCounterStyleOptions {\n name?: string;\n root?: Document | ShadowRoot;\n}\n\nlet clientCounterStyleCounter = 0;\n\n/**\n * Hook to inject a CSS @counter-style rule and return the generated name.\n * Permanent — no cleanup on unmount. Deduplicates by name.\n *\n * @example Basic usage\n * ```tsx\n * function EmojiList() {\n * const styleName = useCounterStyle({\n * system: 'cyclic',\n * symbols: '\"👍\"',\n * suffix: '\" \"',\n * }, { name: 'thumbs' });\n *\n * return (\n * <ol style={{ listStyleType: styleName }}>\n * <li>First</li>\n * <li>Second</li>\n * </ol>\n * );\n * }\n * ```\n *\n * @example Factory function with dependencies\n * ```tsx\n * function DynamicList({ marker }: { marker: string }) {\n * const styleName = useCounterStyle(\n * () => ({\n * system: 'cyclic',\n * symbols: `\"${marker}\"`,\n * suffix: '\" \"',\n * }),\n * [marker],\n * );\n *\n * return <ol style={{ listStyleType: styleName }}>...</ol>;\n * }\n * ```\n */\n\n// Overload 1: Static descriptors\nexport function useCounterStyle(\n descriptors: CounterStyleDescriptors,\n options?: UseCounterStyleOptions,\n): string;\n\n// Overload 2: Factory function with dependencies\nexport function useCounterStyle(\n factory: () => CounterStyleDescriptors,\n deps: readonly unknown[],\n options?: UseCounterStyleOptions,\n): string;\n\n// Implementation\nexport function useCounterStyle(\n descriptorsOrFactory:\n | CounterStyleDescriptors\n | (() => CounterStyleDescriptors),\n depsOrOptions?: readonly unknown[] | UseCounterStyleOptions,\n options?: UseCounterStyleOptions,\n): string {\n const ssrCollector = getRegisteredSSRCollector();\n\n const isFactory = typeof descriptorsOrFactory === 'function';\n\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseCounterStyleOptions | undefined);\n\n // Stable key for the static path — avoids re-triggering when caller\n // passes an inline object literal with the same content.\n const inputKey = useMemo(\n () => (isFactory ? null : JSON.stringify(descriptorsOrFactory)),\n [isFactory ? null : descriptorsOrFactory],\n );\n\n const descriptorsData = useMemo(\n () => {\n const descriptors = isFactory\n ? (descriptorsOrFactory as () => CounterStyleDescriptors)()\n : (descriptorsOrFactory as CounterStyleDescriptors);\n\n if (!descriptors || !descriptors.system) {\n return null;\n }\n\n return descriptors;\n },\n\n isFactory ? (deps ?? []) : [inputKey],\n );\n\n const name = useMemo(() => {\n if (!descriptorsData) {\n return '';\n }\n\n // SSR path: format and collect, return name without DOM injection\n if (ssrCollector) {\n const actualName = ssrCollector.allocateCounterStyleName(opts?.name);\n const css = formatCounterStyleRule(actualName, descriptorsData);\n ssrCollector.collectCounterStyle(actualName, css);\n return actualName;\n }\n\n // Client path: return the name (injection happens in useInsertionEffect)\n return opts?.name ?? `cs${clientCounterStyleCounter++}`;\n }, [descriptorsData, opts?.name, ssrCollector]);\n\n // Client path: inject via DOM\n useInsertionEffect(() => {\n if (!descriptorsData || !name) return;\n\n const injector = getGlobalInjector();\n injector.counterStyle(name, descriptorsData, { root: opts?.root });\n }, [descriptorsData, name, opts?.root]);\n\n return name;\n}\n"],"mappings":";;;;;AAYA,IAAI,4BAA4B;AAuDhC,SAAgB,gBACd,sBAGA,eACA,SACQ;CACR,MAAM,eAAe,2BAA2B;CAEhD,MAAM,YAAY,OAAO,yBAAyB;CAElD,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB,KAAA;CAC9D,MAAM,OAAO,YACT,UACC;CAIL,MAAM,WAAW,cACR,YAAY,OAAO,KAAK,UAAU,qBAAqB,EAC9D,CAAC,YAAY,OAAO,qBAAqB,CAC1C;CAED,MAAM,kBAAkB,cAChB;EACJ,MAAM,cAAc,YACf,sBAAwD,GACxD;AAEL,MAAI,CAAC,eAAe,CAAC,YAAY,OAC/B,QAAO;AAGT,SAAO;IAGT,YAAa,QAAQ,EAAE,GAAI,CAAC,SAAS,CACtC;CAED,MAAM,OAAO,cAAc;AACzB,MAAI,CAAC,gBACH,QAAO;AAIT,MAAI,cAAc;GAChB,MAAM,aAAa,aAAa,yBAAyB,MAAM,KAAK;GACpE,MAAM,MAAM,uBAAuB,YAAY,gBAAgB;AAC/D,gBAAa,oBAAoB,YAAY,IAAI;AACjD,UAAO;;AAIT,SAAO,MAAM,QAAQ,KAAK;IACzB;EAAC;EAAiB,MAAM;EAAM;EAAa,CAAC;AAG/C,0BAAyB;AACvB,MAAI,CAAC,mBAAmB,CAAC,KAAM;AAEd,qBAAmB,CAC3B,aAAa,MAAM,iBAAiB,EAAE,MAAM,MAAM,MAAM,CAAC;IACjE;EAAC;EAAiB;EAAM,MAAM;EAAK,CAAC;AAEvC,QAAO"}
1
+ {"version":3,"file":"useCounterStyle.js","names":[],"sources":["../../src/hooks/useCounterStyle.ts"],"sourcesContent":["import { getGlobalInjector } from '../config';\nimport { formatCounterStyleRule } from '../counter-style';\nimport type { CounterStyleDescriptors } from '../injector/types';\nimport { getStyleTarget, pushRSCCSS } from '../rsc-cache';\n\ninterface UseCounterStyleOptions {\n name?: string;\n root?: Document | ShadowRoot;\n}\n\nlet clientCounterStyleCounter = 0;\n\nconst clientContentToName = new Map<string, string>();\n\n/* @internal — used only for tests */\nexport function _resetCounterStyleCache(): void {\n clientContentToName.clear();\n clientCounterStyleCounter = 0;\n}\n\n/**\n * Inject a CSS @counter-style rule and return the generated name.\n * Permanent — no cleanup on unmount. Deduplicates by name.\n *\n * Works in all environments: client, SSR with collector, and React Server Components.\n *\n * @example Basic usage\n * ```tsx\n * function EmojiList() {\n * const styleName = useCounterStyle({\n * system: 'cyclic',\n * symbols: '\"👍\"',\n * suffix: '\" \"',\n * }, { name: 'thumbs' });\n *\n * return (\n * <ol style={{ listStyleType: styleName }}>\n * <li>First</li>\n * <li>Second</li>\n * </ol>\n * );\n * }\n * ```\n *\n */\nexport function useCounterStyle(\n descriptors: CounterStyleDescriptors,\n options?: UseCounterStyleOptions,\n): string {\n if (!descriptors || !descriptors.system) {\n return '';\n }\n\n const target = getStyleTarget();\n\n if (target.mode === 'ssr') {\n const actualName = target.collector.allocateCounterStyleName(options?.name);\n const css = formatCounterStyleRule(actualName, descriptors);\n target.collector.collectCounterStyle(actualName, css);\n return actualName;\n }\n\n if (target.mode === 'rsc') {\n const serializedContent = JSON.stringify(descriptors);\n const key = `__cs:${options?.name ?? ''}:${serializedContent}`;\n\n const existingName = target.cache.generatedNames.get(key);\n if (existingName) return existingName;\n\n const actualName =\n options?.name ?? `cs${target.cache.counterStyleCounter++}`;\n const css = formatCounterStyleRule(actualName, descriptors);\n pushRSCCSS(target.cache, key, css);\n target.cache.generatedNames.set(key, actualName);\n return actualName;\n }\n\n // Client path: stable name via content-based dedup\n const serializedContent = JSON.stringify(descriptors);\n const cacheKey = `${options?.name ?? ''}:${serializedContent}`;\n\n const existingName = clientContentToName.get(cacheKey);\n if (existingName) {\n return existingName;\n }\n\n const name = options?.name ?? `cs${clientCounterStyleCounter++}`;\n clientContentToName.set(cacheKey, name);\n\n const injector = getGlobalInjector();\n injector.counterStyle(name, descriptors, { root: options?.root });\n\n return name;\n}\n"],"mappings":";;;;AAUA,IAAI,4BAA4B;AAEhC,MAAM,sCAAsB,IAAI,KAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCrD,SAAgB,gBACd,aACA,SACQ;AACR,KAAI,CAAC,eAAe,CAAC,YAAY,OAC/B,QAAO;CAGT,MAAM,SAAS,gBAAgB;AAE/B,KAAI,OAAO,SAAS,OAAO;EACzB,MAAM,aAAa,OAAO,UAAU,yBAAyB,SAAS,KAAK;EAC3E,MAAM,MAAM,uBAAuB,YAAY,YAAY;AAC3D,SAAO,UAAU,oBAAoB,YAAY,IAAI;AACrD,SAAO;;AAGT,KAAI,OAAO,SAAS,OAAO;EACzB,MAAM,oBAAoB,KAAK,UAAU,YAAY;EACrD,MAAM,MAAM,QAAQ,SAAS,QAAQ,GAAG,GAAG;EAE3C,MAAM,eAAe,OAAO,MAAM,eAAe,IAAI,IAAI;AACzD,MAAI,aAAc,QAAO;EAEzB,MAAM,aACJ,SAAS,QAAQ,KAAK,OAAO,MAAM;EACrC,MAAM,MAAM,uBAAuB,YAAY,YAAY;AAC3D,aAAW,OAAO,OAAO,KAAK,IAAI;AAClC,SAAO,MAAM,eAAe,IAAI,KAAK,WAAW;AAChD,SAAO;;CAIT,MAAM,oBAAoB,KAAK,UAAU,YAAY;CACrD,MAAM,WAAW,GAAG,SAAS,QAAQ,GAAG,GAAG;CAE3C,MAAM,eAAe,oBAAoB,IAAI,SAAS;AACtD,KAAI,aACF,QAAO;CAGT,MAAM,OAAO,SAAS,QAAQ,KAAK;AACnC,qBAAoB,IAAI,UAAU,KAAK;AAEtB,oBAAmB,CAC3B,aAAa,MAAM,aAAa,EAAE,MAAM,SAAS,MAAM,CAAC;AAEjE,QAAO"}
@@ -5,9 +5,11 @@ interface UseFontFaceOptions {
5
5
  root?: Document | ShadowRoot;
6
6
  }
7
7
  /**
8
- * Hook to inject CSS @font-face rules.
8
+ * Inject CSS @font-face rules.
9
9
  * Permanent — no cleanup on unmount. Deduplicates by content hash.
10
10
  *
11
+ * Works in all environments: client, SSR with collector, and React Server Components.
12
+ *
11
13
  * @param family - The font-family name
12
14
  * @param input - Single descriptor object or array of descriptors (for multiple weights/styles)
13
15
  * @param options - Optional settings (e.g. Shadow DOM root)
@@ -1,12 +1,13 @@
1
1
  import { fontFaceContentHash, formatFontFaceRule } from "../font-face/index.js";
2
2
  import { getGlobalInjector } from "../config.js";
3
- import { getRegisteredSSRCollector } from "../ssr/ssr-collector-ref.js";
4
- import { useInsertionEffect, useMemo } from "react";
3
+ import { getStyleTarget, pushRSCCSS } from "../rsc-cache.js";
5
4
  //#region src/hooks/useFontFace.ts
6
5
  /**
7
- * Hook to inject CSS @font-face rules.
6
+ * Inject CSS @font-face rules.
8
7
  * Permanent — no cleanup on unmount. Deduplicates by content hash.
9
8
  *
9
+ * Works in all environments: client, SSR with collector, and React Server Components.
10
+ *
10
11
  * @param family - The font-family name
11
12
  * @param input - Single descriptor object or array of descriptors (for multiple weights/styles)
12
13
  * @param options - Optional settings (e.g. Shadow DOM root)
@@ -37,31 +38,27 @@ import { useInsertionEffect, useMemo } from "react";
37
38
  * ```
38
39
  */
39
40
  function useFontFace(family, input, options) {
40
- const ssrCollector = getRegisteredSSRCollector();
41
- const inputKey = useMemo(() => JSON.stringify(input), [input]);
42
- useMemo(() => {
43
- if (!ssrCollector || !family) return;
44
- const descriptors = Array.isArray(input) ? input : [input];
41
+ if (!family) return;
42
+ const descriptors = Array.isArray(input) ? input : [input];
43
+ const target = getStyleTarget();
44
+ if (target.mode === "ssr") {
45
+ for (const desc of descriptors) {
46
+ const hash = fontFaceContentHash(family, desc);
47
+ const css = formatFontFaceRule(family, desc);
48
+ target.collector.collectFontFace(hash, css);
49
+ }
50
+ return;
51
+ }
52
+ if (target.mode === "rsc") {
45
53
  for (const desc of descriptors) {
46
54
  const hash = fontFaceContentHash(family, desc);
47
55
  const css = formatFontFaceRule(family, desc);
48
- ssrCollector.collectFontFace(hash, css);
56
+ pushRSCCSS(target.cache, `__ff:${hash}`, css);
49
57
  }
50
- }, [
51
- ssrCollector,
52
- family,
53
- inputKey
54
- ]);
55
- useInsertionEffect(() => {
56
- if (!family) return;
57
- const injector = getGlobalInjector();
58
- const descriptors = Array.isArray(input) ? input : [input];
59
- for (const desc of descriptors) injector.fontFace(family, desc, { root: options?.root });
60
- }, [
61
- family,
62
- inputKey,
63
- options?.root
64
- ]);
58
+ return;
59
+ }
60
+ const injector = getGlobalInjector();
61
+ for (const desc of descriptors) injector.fontFace(family, desc, { root: options?.root });
65
62
  }
66
63
  //#endregion
67
64
  export { useFontFace };
@@ -1 +1 @@
1
- {"version":3,"file":"useFontFace.js","names":[],"sources":["../../src/hooks/useFontFace.ts"],"sourcesContent":["import { useInsertionEffect, useMemo } from 'react';\n\nimport { getGlobalInjector } from '../config';\nimport { fontFaceContentHash, formatFontFaceRule } from '../font-face';\nimport type { FontFaceDescriptors, FontFaceInput } from '../injector/types';\nimport { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';\n\ninterface UseFontFaceOptions {\n root?: Document | ShadowRoot;\n}\n\n/**\n * Hook to inject CSS @font-face rules.\n * Permanent — no cleanup on unmount. Deduplicates by content hash.\n *\n * @param family - The font-family name\n * @param input - Single descriptor object or array of descriptors (for multiple weights/styles)\n * @param options - Optional settings (e.g. Shadow DOM root)\n *\n * @example Single weight\n * ```tsx\n * function App() {\n * useFontFace('Brand Sans', {\n * src: 'url(\"/fonts/brand-sans.woff2\") format(\"woff2\")',\n * fontWeight: '400 700',\n * fontDisplay: 'swap',\n * });\n *\n * return <div style={{ fontFamily: '\"Brand Sans\", sans-serif' }}>Hello</div>;\n * }\n * ```\n *\n * @example Multiple weights\n * ```tsx\n * function App() {\n * useFontFace('Brand Sans', [\n * { src: 'url(\"/fonts/brand-regular.woff2\") format(\"woff2\")', fontWeight: 400, fontDisplay: 'swap' },\n * { src: 'url(\"/fonts/brand-bold.woff2\") format(\"woff2\")', fontWeight: 700, fontDisplay: 'swap' },\n * ]);\n *\n * return <div style={{ fontFamily: '\"Brand Sans\", sans-serif' }}>Hello</div>;\n * }\n * ```\n */\nexport function useFontFace(\n family: string,\n input: FontFaceInput,\n options?: UseFontFaceOptions,\n): void {\n const ssrCollector = getRegisteredSSRCollector();\n\n const inputKey = useMemo(() => JSON.stringify(input), [input]);\n\n // SSR path: collect @font-face CSS during render\n useMemo(() => {\n if (!ssrCollector || !family) return;\n\n const descriptors: FontFaceDescriptors[] = Array.isArray(input)\n ? input\n : [input];\n\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 }, [ssrCollector, family, inputKey]);\n\n // Client path: inject via DOM\n useInsertionEffect(() => {\n if (!family) return;\n\n const injector = getGlobalInjector();\n const descriptors: FontFaceDescriptors[] = Array.isArray(input)\n ? input\n : [input];\n\n for (const desc of descriptors) {\n injector.fontFace(family, desc, { root: options?.root });\n }\n }, [family, inputKey, options?.root]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,YACd,QACA,OACA,SACM;CACN,MAAM,eAAe,2BAA2B;CAEhD,MAAM,WAAW,cAAc,KAAK,UAAU,MAAM,EAAE,CAAC,MAAM,CAAC;AAG9D,eAAc;AACZ,MAAI,CAAC,gBAAgB,CAAC,OAAQ;EAE9B,MAAM,cAAqC,MAAM,QAAQ,MAAM,GAC3D,QACA,CAAC,MAAM;AAEX,OAAK,MAAM,QAAQ,aAAa;GAC9B,MAAM,OAAO,oBAAoB,QAAQ,KAAK;GAC9C,MAAM,MAAM,mBAAmB,QAAQ,KAAK;AAC5C,gBAAa,gBAAgB,MAAM,IAAI;;IAExC;EAAC;EAAc;EAAQ;EAAS,CAAC;AAGpC,0BAAyB;AACvB,MAAI,CAAC,OAAQ;EAEb,MAAM,WAAW,mBAAmB;EACpC,MAAM,cAAqC,MAAM,QAAQ,MAAM,GAC3D,QACA,CAAC,MAAM;AAEX,OAAK,MAAM,QAAQ,YACjB,UAAS,SAAS,QAAQ,MAAM,EAAE,MAAM,SAAS,MAAM,CAAC;IAEzD;EAAC;EAAQ;EAAU,SAAS;EAAK,CAAC"}
1
+ {"version":3,"file":"useFontFace.js","names":[],"sources":["../../src/hooks/useFontFace.ts"],"sourcesContent":["import { getGlobalInjector } from '../config';\nimport { fontFaceContentHash, formatFontFaceRule } from '../font-face';\nimport type { FontFaceDescriptors, FontFaceInput } from '../injector/types';\nimport { getStyleTarget, pushRSCCSS } from '../rsc-cache';\n\ninterface UseFontFaceOptions {\n root?: Document | ShadowRoot;\n}\n\n/**\n * Inject CSS @font-face rules.\n * Permanent — no cleanup on unmount. Deduplicates by content hash.\n *\n * Works in all environments: client, SSR with collector, and React Server Components.\n *\n * @param family - The font-family name\n * @param input - Single descriptor object or array of descriptors (for multiple weights/styles)\n * @param options - Optional settings (e.g. Shadow DOM root)\n *\n * @example Single weight\n * ```tsx\n * function App() {\n * useFontFace('Brand Sans', {\n * src: 'url(\"/fonts/brand-sans.woff2\") format(\"woff2\")',\n * fontWeight: '400 700',\n * fontDisplay: 'swap',\n * });\n *\n * return <div style={{ fontFamily: '\"Brand Sans\", sans-serif' }}>Hello</div>;\n * }\n * ```\n *\n * @example Multiple weights\n * ```tsx\n * function App() {\n * useFontFace('Brand Sans', [\n * { src: 'url(\"/fonts/brand-regular.woff2\") format(\"woff2\")', fontWeight: 400, fontDisplay: 'swap' },\n * { src: 'url(\"/fonts/brand-bold.woff2\") format(\"woff2\")', fontWeight: 700, fontDisplay: 'swap' },\n * ]);\n *\n * return <div style={{ fontFamily: '\"Brand Sans\", sans-serif' }}>Hello</div>;\n * }\n * ```\n */\nexport function useFontFace(\n family: string,\n input: FontFaceInput,\n options?: UseFontFaceOptions,\n): void {\n if (!family) return;\n\n const descriptors: FontFaceDescriptors[] = Array.isArray(input)\n ? input\n : [input];\n\n const target = getStyleTarget();\n\n if (target.mode === 'ssr') {\n for (const desc of descriptors) {\n const hash = fontFaceContentHash(family, desc);\n const css = formatFontFaceRule(family, desc);\n target.collector.collectFontFace(hash, css);\n }\n return;\n }\n\n if (target.mode === 'rsc') {\n for (const desc of descriptors) {\n const hash = fontFaceContentHash(family, desc);\n const css = formatFontFaceRule(family, desc);\n pushRSCCSS(target.cache, `__ff:${hash}`, css);\n }\n return;\n }\n\n const injector = getGlobalInjector();\n for (const desc of descriptors) {\n injector.fontFace(family, desc, { root: options?.root });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,YACd,QACA,OACA,SACM;AACN,KAAI,CAAC,OAAQ;CAEb,MAAM,cAAqC,MAAM,QAAQ,MAAM,GAC3D,QACA,CAAC,MAAM;CAEX,MAAM,SAAS,gBAAgB;AAE/B,KAAI,OAAO,SAAS,OAAO;AACzB,OAAK,MAAM,QAAQ,aAAa;GAC9B,MAAM,OAAO,oBAAoB,QAAQ,KAAK;GAC9C,MAAM,MAAM,mBAAmB,QAAQ,KAAK;AAC5C,UAAO,UAAU,gBAAgB,MAAM,IAAI;;AAE7C;;AAGF,KAAI,OAAO,SAAS,OAAO;AACzB,OAAK,MAAM,QAAQ,aAAa;GAC9B,MAAM,OAAO,oBAAoB,QAAQ,KAAK;GAC9C,MAAM,MAAM,mBAAmB,QAAQ,KAAK;AAC5C,cAAW,OAAO,OAAO,QAAQ,QAAQ,IAAI;;AAE/C;;CAGF,MAAM,WAAW,mBAAmB;AACpC,MAAK,MAAM,QAAQ,YACjB,UAAS,SAAS,QAAQ,MAAM,EAAE,MAAM,SAAS,MAAM,CAAC"}
@@ -1,15 +1,31 @@
1
1
  import { Styles } from "../styles/types.js";
2
2
 
3
3
  //#region src/hooks/useGlobalStyles.d.ts
4
+ interface UseGlobalStylesOptions {
5
+ /**
6
+ * Stable identifier for update tracking (client-only). When provided,
7
+ * changing the styles will dispose the previous injection and inject the
8
+ * new one. Without an id, the selector is used as the slot key.
9
+ * In RSC mode, renders are single-pass so update tracking does not apply.
10
+ */
11
+ id?: string;
12
+ }
4
13
  /**
5
- * Hook to inject global styles for a given selector.
14
+ * Inject global styles for a given selector.
6
15
  * Useful for styling elements by selector without generating classNames.
7
16
  *
8
17
  * SSR-aware: when a ServerStyleCollector is available, CSS is collected
9
18
  * during the render phase instead of being injected into the DOM.
10
19
  *
20
+ * Works in all environments: client, SSR with collector, and React Server Components.
21
+ *
22
+ * Injected styles are permanent — they are not cleaned up on component unmount.
23
+ * Use the `id` option for update tracking when styles change over the
24
+ * component lifecycle.
25
+ *
11
26
  * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')
12
27
  * @param styles - Tasty styles object
28
+ * @param options - Optional settings including `id` for update tracking
13
29
  *
14
30
  * @example
15
31
  * ```tsx
@@ -24,7 +40,7 @@ import { Styles } from "../styles/types.js";
24
40
  * }
25
41
  * ```
26
42
  */
27
- declare function useGlobalStyles(selector: string, styles?: Styles): void;
43
+ declare function useGlobalStyles(selector: string, styles?: Styles, options?: UseGlobalStylesOptions): void;
28
44
  //#endregion
29
45
  export { useGlobalStyles };
30
46
  //# sourceMappingURL=useGlobalStyles.d.ts.map
@@ -1,21 +1,29 @@
1
1
  import { renderStyles } from "../pipeline/index.js";
2
2
  import { getConfig } from "../config.js";
3
3
  import { injectGlobal } from "../injector/index.js";
4
- import { collectAutoInferredProperties } from "../ssr/collect-auto-properties.js";
4
+ import { getStyleTarget, pushRSCCSS } from "../rsc-cache.js";
5
+ import { collectAutoInferredProperties, collectAutoInferredPropertiesRSC } from "../ssr/collect-auto-properties.js";
5
6
  import { formatGlobalRules } from "../ssr/format-global-rules.js";
6
- import { getRegisteredSSRCollector } from "../ssr/ssr-collector-ref.js";
7
7
  import { resolveRecipes } from "../utils/resolve-recipes.js";
8
- import { useInsertionEffect, useMemo, useRef } from "react";
8
+ import { hashString } from "../utils/hash.js";
9
9
  //#region src/hooks/useGlobalStyles.ts
10
+ const clientGlobalEntries = /* @__PURE__ */ new Map();
10
11
  /**
11
- * Hook to inject global styles for a given selector.
12
+ * Inject global styles for a given selector.
12
13
  * Useful for styling elements by selector without generating classNames.
13
14
  *
14
15
  * SSR-aware: when a ServerStyleCollector is available, CSS is collected
15
16
  * during the render phase instead of being injected into the DOM.
16
17
  *
18
+ * Works in all environments: client, SSR with collector, and React Server Components.
19
+ *
20
+ * Injected styles are permanent — they are not cleaned up on component unmount.
21
+ * Use the `id` option for update tracking when styles change over the
22
+ * component lifecycle.
23
+ *
17
24
  * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')
18
25
  * @param styles - Tasty styles object
26
+ * @param options - Optional settings including `id` for update tracking
19
27
  *
20
28
  * @example
21
29
  * ```tsx
@@ -30,46 +38,49 @@ import { useInsertionEffect, useMemo, useRef } from "react";
30
38
  * }
31
39
  * ```
32
40
  */
33
- function useGlobalStyles(selector, styles) {
34
- const ssrCollector = getRegisteredSSRCollector();
35
- const disposeRef = useRef(null);
36
- const resolvedStyles = useMemo(() => {
37
- if (!styles) return styles;
38
- return resolveRecipes(styles);
39
- }, [styles]);
40
- const styleResults = useMemo(() => {
41
- if (!resolvedStyles) return [];
42
- if (!selector) {
43
- console.warn("[Tasty] useGlobalStyles: selector is required and cannot be empty. Styles will not be injected.");
44
- return [];
41
+ function useGlobalStyles(selector, styles, options) {
42
+ if (!styles) return;
43
+ if (!selector) {
44
+ console.warn("[Tasty] useGlobalStyles: selector is required and cannot be empty. Styles will not be injected.");
45
+ return;
46
+ }
47
+ const target = getStyleTarget();
48
+ if (target.mode === "client") {
49
+ const slotKey = options?.id ?? selector;
50
+ const stylesKey = JSON.stringify(styles);
51
+ const existing = clientGlobalEntries.get(slotKey);
52
+ if (existing && existing.stylesKey === stylesKey) return;
53
+ }
54
+ const resolvedStyles = resolveRecipes(styles);
55
+ const styleResults = renderStyles(resolvedStyles, selector);
56
+ if (styleResults.length === 0) return;
57
+ if (target.mode === "ssr") {
58
+ target.collector.collectInternals();
59
+ const css = formatGlobalRules(styleResults);
60
+ if (css) {
61
+ const key = options?.id ? `global:${options.id}` : `global:${selector}:${hashString(css)}`;
62
+ target.collector.collectGlobalStyles(key, css);
45
63
  }
46
- return renderStyles(resolvedStyles, selector);
47
- }, [resolvedStyles, selector]);
48
- useMemo(() => {
49
- if (!ssrCollector || styleResults.length === 0) return;
50
- ssrCollector.collectInternals();
64
+ if (getConfig().autoPropertyTypes !== false) collectAutoInferredProperties(styleResults, target.collector, resolvedStyles);
65
+ return;
66
+ }
67
+ if (target.mode === "rsc") {
51
68
  const css = formatGlobalRules(styleResults);
52
69
  if (css) {
53
- const key = `global:${selector}:${css.length}:${css.slice(0, 64)}`;
54
- ssrCollector.collectGlobalStyles(key, css);
70
+ const key = options?.id ? `__global:${options.id}` : `__global:${selector}:${hashString(css)}`;
71
+ pushRSCCSS(target.cache, key, css);
55
72
  }
56
- if (getConfig().autoPropertyTypes !== false) collectAutoInferredProperties(styleResults, ssrCollector, resolvedStyles);
57
- }, [
58
- ssrCollector,
59
- styleResults,
60
- selector
61
- ]);
62
- useInsertionEffect(() => {
63
- disposeRef.current?.();
64
- if (styleResults.length > 0) {
65
- const { dispose } = injectGlobal(styleResults);
66
- disposeRef.current = dispose;
67
- } else disposeRef.current = null;
68
- return () => {
69
- disposeRef.current?.();
70
- disposeRef.current = null;
71
- };
72
- }, [styleResults]);
73
+ if (getConfig().autoPropertyTypes !== false) collectAutoInferredPropertiesRSC(styleResults, target.cache, resolvedStyles);
74
+ return;
75
+ }
76
+ const slotKey = options?.id ?? selector;
77
+ const existing = clientGlobalEntries.get(slotKey);
78
+ if (existing) existing.dispose();
79
+ const { dispose } = injectGlobal(styleResults);
80
+ clientGlobalEntries.set(slotKey, {
81
+ stylesKey: JSON.stringify(styles),
82
+ dispose
83
+ });
73
84
  }
74
85
  //#endregion
75
86
  export { useGlobalStyles };
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalStyles.js","names":[],"sources":["../../src/hooks/useGlobalStyles.ts"],"sourcesContent":["import { 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 { formatGlobalRules } from '../ssr/format-global-rules';\nimport { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';\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 ssrCollector = getRegisteredSSRCollector();\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,gBAAgB,UAAkB,QAAuB;CACvE,MAAM,eAAe,2BAA2B;CAEhD,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
+ {"version":3,"file":"useGlobalStyles.js","names":[],"sources":["../../src/hooks/useGlobalStyles.ts"],"sourcesContent":["import { getConfig } from '../config';\nimport { injectGlobal } from '../injector';\nimport type { StyleResult } from '../pipeline';\nimport { renderStyles } from '../pipeline';\nimport { getStyleTarget, pushRSCCSS } from '../rsc-cache';\nimport {\n collectAutoInferredProperties,\n collectAutoInferredPropertiesRSC,\n} from '../ssr/collect-auto-properties';\nimport { formatGlobalRules } from '../ssr/format-global-rules';\nimport type { Styles } from '../styles/types';\nimport { hashString } from '../utils/hash';\nimport { resolveRecipes } from '../utils/resolve-recipes';\n\ninterface UseGlobalStylesOptions {\n /**\n * Stable identifier for update tracking (client-only). When provided,\n * changing the styles will dispose the previous injection and inject the\n * new one. Without an id, the selector is used as the slot key.\n * In RSC mode, renders are single-pass so update tracking does not apply.\n */\n id?: string;\n}\n\ninterface ClientGlobalEntry {\n stylesKey: string;\n dispose: () => void;\n}\n\nconst clientGlobalEntries = new Map<string, ClientGlobalEntry>();\n\n/* @internal — used only for tests */\nexport function _resetGlobalStylesCache(): void {\n clientGlobalEntries.clear();\n}\n\n/**\n * 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 * Works in all environments: client, SSR with collector, and React Server Components.\n *\n * Injected styles are permanent — they are not cleaned up on component unmount.\n * Use the `id` option for update tracking when styles change over the\n * component lifecycle.\n *\n * @param selector - CSS selector to apply styles to (e.g., '.my-class', ':root', 'body')\n * @param styles - Tasty styles object\n * @param options - Optional settings including `id` for update tracking\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(\n selector: string,\n styles?: Styles,\n options?: UseGlobalStylesOptions,\n): void {\n if (!styles) 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 target = getStyleTarget();\n\n // Client fast path: skip resolveRecipes/renderStyles if styles haven't changed\n if (target.mode === 'client') {\n const slotKey = options?.id ?? selector;\n const stylesKey = JSON.stringify(styles);\n const existing = clientGlobalEntries.get(slotKey);\n if (existing && existing.stylesKey === stylesKey) return;\n }\n\n const resolvedStyles = resolveRecipes(styles);\n\n const styleResults = renderStyles(resolvedStyles, selector) as StyleResult[];\n\n if (styleResults.length === 0) return;\n\n if (target.mode === 'ssr') {\n target.collector.collectInternals();\n\n const css = formatGlobalRules(styleResults);\n if (css) {\n const key = options?.id\n ? `global:${options.id}`\n : `global:${selector}:${hashString(css)}`;\n target.collector.collectGlobalStyles(key, css);\n }\n\n if (getConfig().autoPropertyTypes !== false) {\n collectAutoInferredProperties(\n styleResults,\n target.collector,\n resolvedStyles,\n );\n }\n return;\n }\n\n if (target.mode === 'rsc') {\n const css = formatGlobalRules(styleResults);\n if (css) {\n const key = options?.id\n ? `__global:${options.id}`\n : `__global:${selector}:${hashString(css)}`;\n pushRSCCSS(target.cache, key, css);\n }\n\n if (getConfig().autoPropertyTypes !== false) {\n collectAutoInferredPropertiesRSC(\n styleResults,\n target.cache,\n resolvedStyles,\n );\n }\n return;\n }\n\n // Client path\n const slotKey = options?.id ?? selector;\n\n const existing = clientGlobalEntries.get(slotKey);\n if (existing) {\n existing.dispose();\n }\n\n const { dispose } = injectGlobal(styleResults);\n clientGlobalEntries.set(slotKey, {\n stylesKey: JSON.stringify(styles),\n dispose,\n });\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAM,sCAAsB,IAAI,KAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqChE,SAAgB,gBACd,UACA,QACA,SACM;AACN,KAAI,CAAC,OAAQ;AAEb,KAAI,CAAC,UAAU;AAEX,UAAQ,KACN,kGAED;AAEH;;CAGF,MAAM,SAAS,gBAAgB;AAG/B,KAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,UAAU,SAAS,MAAM;EAC/B,MAAM,YAAY,KAAK,UAAU,OAAO;EACxC,MAAM,WAAW,oBAAoB,IAAI,QAAQ;AACjD,MAAI,YAAY,SAAS,cAAc,UAAW;;CAGpD,MAAM,iBAAiB,eAAe,OAAO;CAE7C,MAAM,eAAe,aAAa,gBAAgB,SAAS;AAE3D,KAAI,aAAa,WAAW,EAAG;AAE/B,KAAI,OAAO,SAAS,OAAO;AACzB,SAAO,UAAU,kBAAkB;EAEnC,MAAM,MAAM,kBAAkB,aAAa;AAC3C,MAAI,KAAK;GACP,MAAM,MAAM,SAAS,KACjB,UAAU,QAAQ,OAClB,UAAU,SAAS,GAAG,WAAW,IAAI;AACzC,UAAO,UAAU,oBAAoB,KAAK,IAAI;;AAGhD,MAAI,WAAW,CAAC,sBAAsB,MACpC,+BACE,cACA,OAAO,WACP,eACD;AAEH;;AAGF,KAAI,OAAO,SAAS,OAAO;EACzB,MAAM,MAAM,kBAAkB,aAAa;AAC3C,MAAI,KAAK;GACP,MAAM,MAAM,SAAS,KACjB,YAAY,QAAQ,OACpB,YAAY,SAAS,GAAG,WAAW,IAAI;AAC3C,cAAW,OAAO,OAAO,KAAK,IAAI;;AAGpC,MAAI,WAAW,CAAC,sBAAsB,MACpC,kCACE,cACA,OAAO,OACP,eACD;AAEH;;CAIF,MAAM,UAAU,SAAS,MAAM;CAE/B,MAAM,WAAW,oBAAoB,IAAI,QAAQ;AACjD,KAAI,SACF,UAAS,SAAS;CAGpB,MAAM,EAAE,YAAY,aAAa,aAAa;AAC9C,qBAAoB,IAAI,SAAS;EAC/B,WAAW,KAAK,UAAU,OAAO;EACjC;EACD,CAAC"}
@@ -6,8 +6,10 @@ interface UseKeyframesOptions {
6
6
  root?: Document | ShadowRoot;
7
7
  }
8
8
  /**
9
- * Hook to inject CSS @keyframes and return the generated animation name.
10
- * Handles keyframes injection with proper cleanup on unmount or dependency changes.
9
+ * Inject CSS @keyframes and return the generated animation name.
10
+ * Deduplicates by content identical steps always return the same name.
11
+ *
12
+ * Works in all environments: client, SSR with collector, and React Server Components.
11
13
  *
12
14
  * @example Basic usage - steps object is the dependency
13
15
  * ```tsx
@@ -1,60 +1,51 @@
1
1
  import { keyframes } from "../injector/index.js";
2
+ import { getStyleTarget, pushRSCCSS } from "../rsc-cache.js";
2
3
  import { formatKeyframesCSS } from "../ssr/format-keyframes.js";
3
- import { getRegisteredSSRCollector } from "../ssr/ssr-collector-ref.js";
4
- import { useInsertionEffect, useMemo, useRef } from "react";
4
+ import { depsEqual } from "../utils/deps-equal.js";
5
5
  //#region src/hooks/useKeyframes.ts
6
+ const clientContentToName = /* @__PURE__ */ new Map();
7
+ const factoryDepsCache = /* @__PURE__ */ new Map();
6
8
  function useKeyframes(stepsOrFactory, depsOrOptions, options) {
7
- const ssrCollector = getRegisteredSSRCollector();
8
9
  const isFactory = typeof stepsOrFactory === "function";
9
10
  const deps = isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : void 0;
10
11
  const opts = isFactory ? options : depsOrOptions;
11
- const stepsData = useMemo(() => {
12
- const steps = isFactory ? stepsOrFactory() : stepsOrFactory;
13
- if (!steps || Object.keys(steps).length === 0) return null;
14
- return steps;
15
- }, isFactory ? deps ?? [] : [stepsOrFactory]);
16
- const renderResultRef = useRef(null);
17
- const effectResultRef = useRef(null);
18
- const name = useMemo(() => {
19
- if (!stepsData) return "";
20
- if (ssrCollector) {
21
- const actualName = ssrCollector.allocateKeyframeName(opts?.name);
22
- const css = formatKeyframesCSS(actualName, stepsData);
23
- ssrCollector.collectKeyframes(actualName, css);
24
- return actualName;
25
- }
26
- renderResultRef.current?.dispose();
27
- renderResultRef.current = null;
28
- const result = keyframes(stepsData, {
29
- name: opts?.name,
30
- root: opts?.root
31
- });
32
- renderResultRef.current = result;
33
- return result.toString();
34
- }, [
35
- stepsData,
36
- opts?.name,
37
- opts?.root,
38
- ssrCollector
39
- ]);
40
- useInsertionEffect(() => {
41
- effectResultRef.current?.dispose();
42
- effectResultRef.current = null;
43
- if (stepsData) effectResultRef.current = keyframes(stepsData, {
44
- name: opts?.name,
45
- root: opts?.root
46
- });
47
- return () => {
48
- effectResultRef.current?.dispose();
49
- effectResultRef.current = null;
50
- renderResultRef.current?.dispose();
51
- renderResultRef.current = null;
52
- };
53
- }, [
54
- stepsData,
55
- opts?.name,
56
- opts?.root
57
- ]);
12
+ const target = getStyleTarget();
13
+ if (isFactory && deps && opts?.name && target.mode === "client") {
14
+ const cached = factoryDepsCache.get(opts.name);
15
+ if (cached && depsEqual(cached.deps, deps)) return cached.name;
16
+ }
17
+ const steps = isFactory ? stepsOrFactory() : stepsOrFactory;
18
+ if (!steps || Object.keys(steps).length === 0) return "";
19
+ if (target.mode === "ssr") {
20
+ const actualName = target.collector.allocateKeyframeName(opts?.name);
21
+ const css = formatKeyframesCSS(actualName, steps);
22
+ target.collector.collectKeyframes(actualName, css);
23
+ return actualName;
24
+ }
25
+ if (target.mode === "rsc") {
26
+ const serializedContent = JSON.stringify(steps);
27
+ const key = `__kf:${opts?.name ?? ""}:${serializedContent}`;
28
+ const existingName = target.cache.generatedNames.get(key);
29
+ if (existingName) return existingName;
30
+ const actualName = opts?.name ?? `k${target.cache.keyframesCounter++}`;
31
+ const css = formatKeyframesCSS(actualName, steps);
32
+ pushRSCCSS(target.cache, key, css);
33
+ target.cache.generatedNames.set(key, actualName);
34
+ return actualName;
35
+ }
36
+ const serializedContent = JSON.stringify(steps);
37
+ const cacheKey = `${opts?.name ?? ""}:${serializedContent}`;
38
+ const cachedName = clientContentToName.get(cacheKey);
39
+ if (cachedName) return cachedName;
40
+ const name = keyframes(steps, {
41
+ name: opts?.name,
42
+ root: opts?.root
43
+ }).toString();
44
+ clientContentToName.set(cacheKey, name);
45
+ if (deps && opts?.name) factoryDepsCache.set(opts.name, {
46
+ deps,
47
+ name
48
+ });
58
49
  return name;
59
50
  }
60
51
  //#endregion
@@ -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';\nimport { formatKeyframesCSS } from '../ssr/format-keyframes';\nimport { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';\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 ssrCollector = getRegisteredSSRCollector();\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":";;;;;AAuEA,SAAgB,aACd,gBACA,eACA,SACQ;CACR,MAAM,eAAe,2BAA2B;CAGhD,MAAM,YAAY,OAAO,mBAAmB;CAG5C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB,KAAA;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
+ {"version":3,"file":"useKeyframes.js","names":[],"sources":["../../src/hooks/useKeyframes.ts"],"sourcesContent":["import { keyframes } from '../injector';\nimport type { KeyframesSteps } from '../injector/types';\nimport { getStyleTarget, pushRSCCSS } from '../rsc-cache';\nimport { formatKeyframesCSS } from '../ssr/format-keyframes';\nimport { depsEqual } from '../utils/deps-equal';\n\ninterface UseKeyframesOptions {\n name?: string;\n root?: Document | ShadowRoot;\n}\n\nconst clientContentToName = new Map<string, string>();\n\ninterface FactoryDepsEntry {\n deps: readonly unknown[];\n name: string;\n}\n\nconst factoryDepsCache = new Map<string, FactoryDepsEntry>();\n\n/* @internal — used only for tests */\nexport function _resetKeyframesCache(): void {\n clientContentToName.clear();\n factoryDepsCache.clear();\n}\n\n/**\n * Inject CSS @keyframes and return the generated animation name.\n * Deduplicates by content identical steps always return the same name.\n *\n * Works in all environments: client, SSR with collector, and React Server Components.\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 isFactory = typeof stepsOrFactory === 'function';\n\n const deps =\n isFactory && Array.isArray(depsOrOptions) ? depsOrOptions : undefined;\n const opts = isFactory\n ? options\n : (depsOrOptions as UseKeyframesOptions | undefined);\n\n const target = getStyleTarget();\n\n // Client deps cache: skip factory re-evaluation when deps haven't changed\n if (isFactory && deps && opts?.name && target.mode === 'client') {\n const cached = factoryDepsCache.get(opts.name);\n if (cached && depsEqual(cached.deps, deps)) {\n return cached.name;\n }\n }\n\n const steps = isFactory\n ? (stepsOrFactory as () => KeyframesSteps)()\n : (stepsOrFactory as KeyframesSteps);\n\n if (!steps || Object.keys(steps).length === 0) {\n return '';\n }\n\n if (target.mode === 'ssr') {\n const actualName = target.collector.allocateKeyframeName(opts?.name);\n const css = formatKeyframesCSS(actualName, steps);\n target.collector.collectKeyframes(actualName, css);\n return actualName;\n }\n\n if (target.mode === 'rsc') {\n const serializedContent = JSON.stringify(steps);\n const key = `__kf:${opts?.name ?? ''}:${serializedContent}`;\n\n const existingName = target.cache.generatedNames.get(key);\n if (existingName) return existingName;\n\n const actualName = opts?.name ?? `k${target.cache.keyframesCounter++}`;\n const css = formatKeyframesCSS(actualName, steps);\n pushRSCCSS(target.cache, key, css);\n target.cache.generatedNames.set(key, actualName);\n return actualName;\n }\n\n // Client path: stable name via content-based dedup\n const serializedContent = JSON.stringify(steps);\n const cacheKey = `${opts?.name ?? ''}:${serializedContent}`;\n\n const cachedName = clientContentToName.get(cacheKey);\n if (cachedName) {\n return cachedName;\n }\n\n const result = keyframes(steps, {\n name: opts?.name,\n root: opts?.root,\n });\n\n const name = result.toString();\n clientContentToName.set(cacheKey, name);\n\n if (deps && opts?.name) {\n factoryDepsCache.set(opts.name, { deps, name });\n }\n\n return name;\n}\n"],"mappings":";;;;;AAWA,MAAM,sCAAsB,IAAI,KAAqB;AAOrD,MAAM,mCAAmB,IAAI,KAA+B;AAqE5D,SAAgB,aACd,gBACA,eACA,SACQ;CACR,MAAM,YAAY,OAAO,mBAAmB;CAE5C,MAAM,OACJ,aAAa,MAAM,QAAQ,cAAc,GAAG,gBAAgB,KAAA;CAC9D,MAAM,OAAO,YACT,UACC;CAEL,MAAM,SAAS,gBAAgB;AAG/B,KAAI,aAAa,QAAQ,MAAM,QAAQ,OAAO,SAAS,UAAU;EAC/D,MAAM,SAAS,iBAAiB,IAAI,KAAK,KAAK;AAC9C,MAAI,UAAU,UAAU,OAAO,MAAM,KAAK,CACxC,QAAO,OAAO;;CAIlB,MAAM,QAAQ,YACT,gBAAyC,GACzC;AAEL,KAAI,CAAC,SAAS,OAAO,KAAK,MAAM,CAAC,WAAW,EAC1C,QAAO;AAGT,KAAI,OAAO,SAAS,OAAO;EACzB,MAAM,aAAa,OAAO,UAAU,qBAAqB,MAAM,KAAK;EACpE,MAAM,MAAM,mBAAmB,YAAY,MAAM;AACjD,SAAO,UAAU,iBAAiB,YAAY,IAAI;AAClD,SAAO;;AAGT,KAAI,OAAO,SAAS,OAAO;EACzB,MAAM,oBAAoB,KAAK,UAAU,MAAM;EAC/C,MAAM,MAAM,QAAQ,MAAM,QAAQ,GAAG,GAAG;EAExC,MAAM,eAAe,OAAO,MAAM,eAAe,IAAI,IAAI;AACzD,MAAI,aAAc,QAAO;EAEzB,MAAM,aAAa,MAAM,QAAQ,IAAI,OAAO,MAAM;EAClD,MAAM,MAAM,mBAAmB,YAAY,MAAM;AACjD,aAAW,OAAO,OAAO,KAAK,IAAI;AAClC,SAAO,MAAM,eAAe,IAAI,KAAK,WAAW;AAChD,SAAO;;CAIT,MAAM,oBAAoB,KAAK,UAAU,MAAM;CAC/C,MAAM,WAAW,GAAG,MAAM,QAAQ,GAAG,GAAG;CAExC,MAAM,aAAa,oBAAoB,IAAI,SAAS;AACpD,KAAI,WACF,QAAO;CAQT,MAAM,OALS,UAAU,OAAO;EAC9B,MAAM,MAAM;EACZ,MAAM,MAAM;EACb,CAAC,CAEkB,UAAU;AAC9B,qBAAoB,IAAI,UAAU,KAAK;AAEvC,KAAI,QAAQ,MAAM,KAChB,kBAAiB,IAAI,KAAK,MAAM;EAAE;EAAM;EAAM,CAAC;AAGjD,QAAO"}
@@ -22,17 +22,19 @@ interface UsePropertyOptions {
22
22
  root?: Document | ShadowRoot;
23
23
  }
24
24
  /**
25
- * Hook to register a CSS @property custom property.
25
+ * Register a CSS @property custom property.
26
26
  * This enables advanced features like animating custom properties.
27
27
  *
28
28
  * Note: @property rules are global and persistent once defined.
29
- * The hook ensures the property is only registered once per root.
29
+ * The function ensures the property is only registered once per root.
30
30
  *
31
31
  * Accepts tasty token syntax for the property name:
32
32
  * - `$name` → defines `--name`
33
33
  * - `#name` → defines `--name-color` (auto-sets syntax: '<color>', defaults initialValue: 'transparent')
34
34
  * - `--name` → defines `--name` (legacy format)
35
35
  *
36
+ * Works in all environments: client, SSR with collector, and React Server Components.
37
+ *
36
38
  * @param name - The property token ($name, #name) or CSS property name (--name)
37
39
  * @param options - Property configuration
38
40
  *