@tenphi/tasty 3.10.0 → 3.11.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 (88) hide show
  1. package/README.md +403 -380
  2. package/dist/{babel-types-C_7kRP38.d.ts → babel-types-BKDTT4--.d.ts} +2 -2
  3. package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
  4. package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
  5. package/dist/chunks/{build-config-l2Ys-AJr.js → build-config-5d8WlQbq.js} +5 -5
  6. package/dist/chunks/{build-config-l2Ys-AJr.js.map → build-config-5d8WlQbq.js.map} +1 -1
  7. package/dist/chunks/{collector-CaNtsRtn.js → collector-CjCak4iE.js} +4 -4
  8. package/dist/chunks/{collector-CaNtsRtn.js.map → collector-CjCak4iE.js.map} +1 -1
  9. package/dist/chunks/{config-engine-CpExZO_T.js → config-engine-uOFYZidB.js} +4 -4
  10. package/dist/chunks/{config-engine-CpExZO_T.js.map → config-engine-uOFYZidB.js.map} +1 -1
  11. package/dist/chunks/config-file-DlZXJMj8.js +25 -0
  12. package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
  13. package/dist/chunks/{css-definitions-DAIJxzFY.js → css-definitions-ubrHP97m.js} +88 -77
  14. package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
  15. package/dist/chunks/{debug-CKy49GEr.js → debug-CJ31U_WE.js} +6 -6
  16. package/dist/chunks/{debug-CKy49GEr.js.map → debug-CJ31U_WE.js.map} +1 -1
  17. package/dist/chunks/{dsl-DRhFWQzo.js → dsl-DYSJc1-T.js} +11 -6
  18. package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
  19. package/dist/chunks/{react-runtime-oi6i3ELR.js → react-runtime-CyyCI02N.js} +168 -61
  20. package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
  21. package/dist/chunks/{runtime-engine-DoULyhPt.js → runtime-engine-BQmOu5-W.js} +53 -36
  22. package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
  23. package/dist/chunks/{runtime-state-CZ2J5QiZ.js → runtime-state-DEsyUMU-.js} +14 -3
  24. package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
  25. package/dist/chunks/{style-engine-CeKsjtIr.js → style-engine-BX2LshDZ.js} +116 -56
  26. package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
  27. package/dist/chunks/{zero-engine-fxKu8Jbn.js → zero-engine-Dw0P4yGM.js} +22 -7
  28. package/dist/chunks/zero-engine-Dw0P4yGM.js.map +1 -0
  29. package/dist/{collector-w4mBj5gd.d.ts → collector-CTU-nl0g.d.ts} +2 -2
  30. package/dist/{config-CrZOfNJ5.d.ts → config-CcxqSZI2.d.ts} +28 -9
  31. package/dist/core/index.d.ts +4 -4
  32. package/dist/core/index.js +7 -7
  33. package/dist/hydrate-CRyjjAyl.d.ts +22 -0
  34. package/dist/{index-CYJtAI9a.d.ts → index--GdqCPlp.d.ts} +58 -16
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.js +8 -8
  37. package/dist/{merge-styles-VXwJ0Frr.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -2
  38. package/dist/ssr/astro.js +4 -4
  39. package/dist/ssr/client.d.ts +2 -0
  40. package/dist/ssr/client.js +2 -0
  41. package/dist/ssr/index.d.ts +3 -22
  42. package/dist/ssr/index.js +4 -4
  43. package/dist/ssr/index.js.map +1 -1
  44. package/dist/ssr/next-config.d.ts +1 -1
  45. package/dist/ssr/next-config.js +6 -17
  46. package/dist/ssr/next-config.js.map +1 -1
  47. package/dist/ssr/next.d.ts +1 -1
  48. package/dist/ssr/next.js +3 -3
  49. package/dist/static/index.d.ts +2 -2
  50. package/dist/{types-JEY8PVYP.d.ts → types-DJEM_BOJ.d.ts} +6 -6
  51. package/dist/zero/babel.d.ts +1 -1
  52. package/dist/zero/babel.js +30 -19
  53. package/dist/zero/babel.js.map +1 -1
  54. package/dist/zero/index.d.ts +24 -6
  55. package/dist/zero/index.js +4 -4
  56. package/dist/zero/index.js.map +1 -1
  57. package/dist/zero/next.d.ts +1 -1
  58. package/dist/zero/next.js +3 -7
  59. package/dist/zero/next.js.map +1 -1
  60. package/docs/README.md +4 -1
  61. package/docs/adoption.md +25 -17
  62. package/docs/ai-agents.md +39 -34
  63. package/docs/comparison.md +9 -8
  64. package/docs/configuration.md +174 -58
  65. package/docs/debug.md +3 -2
  66. package/docs/design-system.md +117 -15
  67. package/docs/dsl.md +145 -81
  68. package/docs/getting-started.md +81 -24
  69. package/docs/injector.md +41 -40
  70. package/docs/methodology.md +46 -21
  71. package/docs/migration-v3.md +3 -3
  72. package/docs/pipeline.md +19 -16
  73. package/docs/plugins.md +21 -41
  74. package/docs/react-api.md +61 -9
  75. package/docs/runtime-benchmarks.md +4 -4
  76. package/docs/ssr.md +92 -66
  77. package/docs/styles.md +33 -17
  78. package/docs/tasty-static.md +129 -58
  79. package/package.json +23 -9
  80. package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
  81. package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
  82. package/dist/chunks/css-definitions-DAIJxzFY.js.map +0 -1
  83. package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
  84. package/dist/chunks/react-runtime-oi6i3ELR.js.map +0 -1
  85. package/dist/chunks/runtime-engine-DoULyhPt.js.map +0 -1
  86. package/dist/chunks/runtime-state-CZ2J5QiZ.js.map +0 -1
  87. package/dist/chunks/style-engine-CeKsjtIr.js.map +0 -1
  88. package/dist/chunks/zero-engine-fxKu8Jbn.js.map +0 -1
package/docs/react-api.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use `tasty()` to turn state-aware style definitions into normal React components, extend existing Tasty components, and expose typed styling APIs to consumers.
4
4
 
5
- All Tasty style functions — `tasty()` components, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — are hook-free and compatible with React Server Components. No `'use client'` directive is needed. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
5
+ Every Tasty style function — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — works in React Server Components with no `'use client'` directive. All of them except `useStyles()` are hook-free. `useStyles()` reads the `TastyRegistry` collector from React context on the client, so it is a real hook there; outside React, call `computeStyles()` instead. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
6
6
 
7
7
  > **Note:** This file was previously named `runtime.md`. All functionality documented here works in both server and client contexts — "runtime" referred to style computation during React rendering, not to client-side JavaScript.
8
8
 
@@ -44,6 +44,30 @@ const PrimaryButton = tasty(Button, {
44
44
 
45
45
  Style maps merge intelligently — see [Style DSL — Extending vs. Replacing State Maps](dsl.md#extending-vs-replacing-state-maps) for extend mode, replace mode, `@inherit`, `null`, and `false` tombstones.
46
46
 
47
+ The extension form takes the same component options as `tasty({ … })`:
48
+
49
+ ```jsx
50
+ const ActionCard = tasty(Card, {
51
+ styles: { fill: { '': '#surface', isRaised: '#surface-raised' } },
52
+ styleProps: ['gap'] as const, // gap="2x" becomes a style
53
+ modProps: ['isRaised'] as const, // isRaised becomes a mod (data-is-raised)
54
+ tokenProps: ['accentColor'] as const, // accentColor sets the #accent token
55
+ variants: {
56
+ plain: { border: 0 },
57
+ framed: { border: '2bw #accent' },
58
+ },
59
+ variant: 'framed', // applied when no variant prop is passed
60
+ elements: { Footer: 'footer' },
61
+ });
62
+
63
+ <ActionCard gap="2x" isRaised accentColor="#purple">
64
+ <ActionCard.Title>Kept from Card</ActionCard.Title>
65
+ <ActionCard.Footer>Added here</ActionCard.Footer>
66
+ </ActionCard>;
67
+ ```
68
+
69
+ The extension turns its own `styleProps`, `modProps`, and `tokenProps` into the `styles`, `mods`, and `tokens` it passes to the base, which renders them as usual: mods as `data-*` attributes, tokens as inline custom properties. Its `variants` merge over its `styles`, and the `variant` prop is still forwarded, so it selects the base's own variants too and accepts either set's names. Both sets share that one prop, so a name only one of them defines leaves the other at its fallback: if `Card` had an `accent` variant, `variant="accent"` would apply it together with `framed`, and `variant="plain"` would leave `Card` on its default. For two independent choices, give one of them its own prop, such as a `modProps` value list. `elements` adds sub-components, and the base's are kept: `ActionCard.Title` is `Card.Title`. The returned component is typed with all of these props and sub-components.
70
+
47
71
  `tasty(Component, ...)` always wraps `Component` and forwards every prop to it, so `Component` must be Tasty-aware (i.e. it accepts `styles`, `mods`, `qa`, etc. and renders them through its own Tasty pipeline). To apply styles to a third-party component or a string DOM tag via `className`, use the options-only form with `as`:
48
72
 
49
73
  ```jsx
@@ -68,7 +92,7 @@ const Span = tasty({
68
92
  </Link>;
69
93
  ```
70
94
 
71
- The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty-specific props (`qa`, `qaVal`, `mods`, `tokens`, `styleProps`, `modProps`, `tokenProps`) are consumed by Tasty and never leak to the DOM.
95
+ The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty's own props (`styles`, `mods`, `tokens`, `qa`, `qaVal`, `theme`, `element`, the `is*` props) and the props you declared with `styleProps`, `modProps`, or `tokenProps` are consumed by Tasty; mods, `qa`, `qaVal`, `theme`, and `element` reach the DOM only as `data-*` attributes.
72
96
 
73
97
  Props forwarded to the `as` component keep their declared types and requiredness. For example, a component with `position: 'top-right' | 'bottom-left'` accepts those values through `tasty({ as: Component })`; CSS positioning can still be set through `styles.position`. A prop explicitly listed in `styleProps` is consumed as a style value instead. The always-available base style props below are also consumed by Tasty, so keep component-specific prop names separate from those reserved names.
74
98
 
@@ -112,12 +136,12 @@ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteS
112
136
 
113
137
  ## `Element`
114
138
 
115
- `Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts all Tasty props (`styles`, `styleProps`, `mods`, `tokens`, `as`, `qa`, `theme`, the `is*` props, etc.) and is useful as a generic styled box or as a building block for layout primitives.
139
+ `Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts the props every Tasty component has (`as`, `styles`, `mods`, `tokens`, `qa`, `theme`, the `is*` props) plus the [always-available style props](#always-available-style-props), and is useful as a generic box or as a building block for layout primitives. Other style names, such as `padding`, are not props on it: create a component with `styleProps` for those, or pass them through `styles`.
116
140
 
117
141
  ```jsx
118
142
  import { Element } from '@tenphi/tasty';
119
143
 
120
- <Element as="section" padding="4x" fill="#surface">
144
+ <Element as="section" display="grid" preset="t2" styles={{ padding: '4x' }}>
121
145
  Content
122
146
  </Element>;
123
147
  ```
@@ -144,6 +168,7 @@ List modifier key names. Types default to `ModValue` (`boolean | string | number
144
168
 
145
169
  ```jsx
146
170
  const Button = tasty({
171
+ as: 'button',
147
172
  modProps: ['isLoading', 'isSelected'] as const,
148
173
  styles: {
149
174
  fill: { '': '#surface', isLoading: '#surface.5' },
@@ -161,6 +186,7 @@ Map modifier names to type descriptors for precise TypeScript types:
161
186
 
162
187
  ```tsx
163
188
  const Button = tasty({
189
+ as: 'button',
164
190
  modProps: {
165
191
  isLoading: Boolean, // isLoading?: boolean
166
192
  isSelected: Boolean, // isSelected?: boolean
@@ -297,6 +323,24 @@ const Button = tasty({
297
323
  <Button variant="danger">Delete</Button>;
298
324
  ```
299
325
 
326
+ With `variant` omitted, or set to a key `variants` does not define, the `default` variant applies; without a `default` key only the base `styles` apply. `variant` is typed from the variant keys.
327
+
328
+ To fall back to another variant instead, name it in the `variant` option:
329
+
330
+ ```jsx
331
+ const Button = tasty({
332
+ styles: { padding: '2x 4x' },
333
+ variants: {
334
+ primary: { fill: '#blue', color: '#white' },
335
+ danger: { fill: '#red', color: '#white' },
336
+ },
337
+ variant: 'primary',
338
+ });
339
+
340
+ <Button>Save</Button>; // primary
341
+ <Button variant="danger">Delete</Button>;
342
+ ```
343
+
300
344
  ### Extending Variants with Base State Maps
301
345
 
302
346
  When base `styles` contain an extend-mode state map (an object **without** a `''` key), it is applied **after** the variant merge. This lets you add or override states across all variants without repeating yourself:
@@ -401,19 +445,19 @@ const { className } = computeStyles({
401
445
  });
402
446
  ```
403
447
 
404
- On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS is collected via the SSR collector if one is available. This is the same function that `tasty()` components use internally.
448
+ On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server with an SSR collector (from `runWithCollector()` or `TastyRegistry`), the CSS goes to the collector. With neither a collector nor a `document` — a React Server Component — nothing is injected: the CSS comes back as `css`, and you render it in a `style` tag yourself, as `tasty()` components do. This is the same function that `tasty()` components use internally.
405
449
 
406
450
  ---
407
451
 
408
452
  ## Style Functions
409
453
 
410
- All style functions below are plain functions (not React hooks) and can be used in any environment: client components, SSR with a `ServerStyleCollector`, and React Server Components. They retain their `use` prefix for backward compatibility, but do not use any React hooks internally.
454
+ All style functions below work in client components, SSR with a `ServerStyleCollector`, and React Server Components. All except `useStyles()` are plain functions, not React hooks; they keep the `use` prefix for backward compatibility.
411
455
 
412
456
  In server-only contexts, components that use only Tasty style functions ship no Tasty styling runtime. Astro without `client:*` directives produces no client JavaScript; server-only Next.js RSC follows the same Tasty architecture, while final output depends on the application deployment. Tasty never forces the `'use client'` boundary — that decision belongs to your component when it needs React interactivity (state, effects, event handlers).
413
457
 
414
458
  ### useStyles
415
459
 
416
- Generate a className from a style object. Thin wrapper around `computeStyles()`:
460
+ Generate a className from a style object. A thin wrapper around `computeStyles()` that also picks up the `TastyRegistry` collector through React context. That makes it a hook on the client: call it inside a component, and use `computeStyles()` outside React. It also works in a Server Component, where it skips the context lookup; like the other style functions there, it cannot emit CSS itself, so the next `tasty()` element in the tree emits it.
417
461
 
418
462
  ```tsx
419
463
  import { useStyles } from '@tenphi/tasty';
@@ -482,6 +526,8 @@ function GlobalReset() {
482
526
  An `id` slot holds one injection per `root`. Without an `id` the CSS is deduped
483
527
  by content and permanent — there is nothing to replace it with later.
484
528
 
529
+ It also accepts a factory and deps — `useRawCSS(() => css, [deps], { id })` — and skips the factory while the deps for that `id` are unchanged.
530
+
485
531
  ### useKeyframes
486
532
 
487
533
  Inject `@keyframes` rules and return the generated animation name:
@@ -591,7 +637,11 @@ useFontFace('Brand Sans', [
591
637
  Signature:
592
638
 
593
639
  ```ts
594
- function useFontFace(family: string, input: FontFaceInput): void;
640
+ function useFontFace(
641
+ family: string,
642
+ input: FontFaceInput,
643
+ options?: { root?: Document | ShadowRoot },
644
+ ): void;
595
645
  ```
596
646
 
597
647
  ### useCounterStyle
@@ -673,9 +723,10 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
673
723
 
674
724
  - Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
675
725
  - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. All style functions discover the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
726
+ - "Server rendering without a style collector" in development: the server render has no request scope, so each `tasty()` element inlines its own CSS and configured globals are not emitted. Add `tastyIntegration()` (Astro), `TastyRegistry` (Next.js), or `runWithCollector()` — see the [SSR guide](ssr.md).
676
727
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
677
728
  - For dynamic styles that change over the component lifecycle, use the `id` option in `useGlobalStyles()` and `useRawCSS()` to enable update tracking.
678
- - RSC inline mode: CSS accumulated by standalone style functions (`useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
729
+ - RSC inline mode: CSS accumulated by standalone style functions (`useStyles`, `useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
679
730
 
680
731
  ---
681
732
 
@@ -684,6 +735,7 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
684
735
  - **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
685
736
  - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
686
737
  - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
738
+ - **[Configuration → TypeScript](configuration.md#typescript)** — How props are typed and how to register your names
687
739
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
688
740
  - **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin
689
741
  - **[Server-Side Rendering](ssr.md)** — SSR setup for Next.js, Astro, and generic frameworks
@@ -197,8 +197,8 @@ The benchmarks above measure mounting and whole-tree updates. A running
197
197
  application spends most of its time on neither. It flips mods — hovered,
198
198
  pressed, selected, expanded — on elements whose styles never change, one
199
199
  element at a time, and it mounts and unmounts small styled subtrees as menus
200
- and dialogs open. Both paths go through the state-map and ref-counting
201
- machinery rather than the parser, so a regression in them is invisible to every
200
+ and dialogs open. Both paths go through mod handling and the chunk cache
201
+ rather than the parser, so a regression in them is invisible to every
202
202
  other benchmark here.
203
203
 
204
204
  [`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-interaction.bench.tsx) pairs each
@@ -371,8 +371,8 @@ blindly. They describe different paths:
371
371
  rule.
372
372
  - A genuinely new style pays generation and injection once, then becomes
373
373
  reusable.
374
- - A mod flip on an already-styled element pays neither; it is a class-name
375
- change.
374
+ - A mod flip on an already-styled element pays neither; it toggles a `data-*`
375
+ attribute and the class stays the same.
376
376
  - Browser style resolution, layout, and paint depend on the actual document and
377
377
  need application-level profiling.
378
378
 
package/docs/ssr.md CHANGED
@@ -15,10 +15,10 @@ Zero-runtime delivery is an outcome, not an alias for `tastyStatic()`. When `tas
15
15
  | Dependency | Version | Required for |
16
16
  | ---------- | ------- | ----------------------------------------------------------------------------------------------- |
17
17
  | `react` | >= 18 | All SSR entry points (matches the current peer dependency of `@tenphi/tasty`) |
18
- | `next` | >= 13 | Next.js integration (`@tenphi/tasty/ssr/next`) — App Router with `useServerInsertedHTML` |
18
+ | `next` | >= 14 | Next.js integration (`@tenphi/tasty/ssr/next`) — App Router with `useServerInsertedHTML` |
19
19
  | Node.js | >= 20 | Generic / streaming SSR (`@tenphi/tasty/ssr`) — uses `node:async_hooks` for `AsyncLocalStorage` |
20
20
 
21
- The Astro integration (`@tenphi/tasty/ssr/astro`) has no additional dependencies beyond `react`.
21
+ The Astro integration (`@tenphi/tasty/ssr/astro`) needs `@astrojs/react`. Like `@tenphi/tasty/ssr`, it imports `node:async_hooks`, so the server runtime must provide it. `css: { mode: 'extract' }` requires Astro 5 or newer.
22
22
 
23
23
  ---
24
24
 
@@ -137,7 +137,11 @@ import config from './tasty.config';
137
137
 
138
138
  configure(config);
139
139
 
140
- export default function TastyStyleRegistry({ children }) {
140
+ export default function TastyStyleRegistry({
141
+ children,
142
+ }: {
143
+ children: React.ReactNode;
144
+ }) {
141
145
  return <TastyRegistry>{children}</TastyRegistry>;
142
146
  }
143
147
  ```
@@ -147,8 +151,9 @@ The generated file contains eager configuration artifacts: built-in and custom
147
151
  CSS `@function` definitions, and `globalStyles`. Route-dependent component
148
152
  rules and calls to `useGlobalStyles`, `useRawCSS`, `useKeyframes`,
149
153
  `useProperty`, `useFontFace`, `useCounterStyle`, and `useFunction` remain
150
- route-specific. Configured keyframes also remain lazy and are streamed only
151
- when a route references them.
154
+ route-specific. Configured keyframes referenced by `globalStyles` are included in the file; all
155
+ other configured keyframes stay lazy and stream only when a route references
156
+ them.
152
157
 
153
158
  The default output directory is `public/_tasty`. The wrapper adds the generated
154
159
  URL to `TastyRegistry`, respects `basePath`, preserves existing `env` and
@@ -175,20 +180,21 @@ meaning; use root-relative, absolute, or data URLs.
175
180
  ### How it works
176
181
 
177
182
  - `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load. The `'use client'` boundary is required solely to access `useServerInsertedHTML` — **not** because `tasty()` components need the client.
178
- - During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level getter (not `globalThis` — this avoids leaking between Next.js's separate RSC and SSR module graphs). It also wraps children in a React context provider so that hooks inside the SSR tree can discover the collector. All style functions — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()` — discover the collector through the module-level getter or context provider.
183
+ - During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level getter (not `globalThis` — this avoids leaking between Next.js's separate RSC and SSR module graphs). It also wraps children in a React context provider so that hooks inside the SSR tree can discover the collector. All style functions — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — discover the collector through the module-level getter or context provider.
179
184
  - `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible — styles are injected alongside each Suspense boundary as it resolves.
180
185
  - A companion inline `<script>` tag pushes the rendered class names into `window.__TASTY__` for each flush. This streaming-friendly approach accumulates the class list incrementally as Suspense boundaries resolve.
181
186
  - When the `@tenphi/tasty/ssr/next` module loads on the client, `hydrateTastyClasses()` runs automatically from `window.__TASTY__` and pre-populates the injector's rules map. During hydration, `computeStyles()` sees the class name is already registered and skips the entire pipeline.
182
187
 
183
188
  ### Using Tasty in Server Components
184
189
 
185
- All Tasty style functions are hook-free and do not require `'use client'`. They can be used directly in React Server Components:
190
+ Every Tasty style function needs no `'use client'`, so it can be used directly in React Server Components:
186
191
 
187
192
  - `tasty()` components — dynamic `styleProps` like `<Grid flow="column">` work normally
188
- - `useStyles()`, `useGlobalStyles()`, `useRawCSS()` — inject styles by class or selector
189
- - `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()` — inject ancillary CSS rules
193
+ - `useStyles()` and its hook-free equivalent `computeStyles()` — a class name for any element
194
+ - `useGlobalStyles()`, `useRawCSS()` — inject styles by selector
195
+ - `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, `useFunction()` — inject ancillary CSS rules
190
196
 
191
- During SSR, all functions discover the collector via the same global getter registered by `TastyRegistry` — no React context or client boundary needed. In RSC mode without a collector (e.g., Astro zero-setup), CSS is accumulated in a per-request cache and flushed into an inline `<style>` tag by the next `tasty()` component in the tree. Ensure at least one `tasty()` component is present in every RSC render tree — standalone style functions alone cannot emit their CSS without a `tasty()` component to trigger the flush.
197
+ Next.js renders Server Components in a separate module graph that cannot see the collector `TastyRegistry` registers, so their CSS takes the RSC inline path: it is accumulated in a per-request `React.cache` and emitted as an inline `<style>` tag by the next `tasty()` component in the tree. Standalone style functions, `useStyles()` included, cannot emit CSS on their own, so make sure a `tasty()` component renders after them in every RSC tree. The RSC path does not emit configured globals (tokens, `@property`, `globalStyles`); those come from `TastyRegistry`'s collector or the `withTastyNext()` shared stylesheet. For the same reason, import the module that calls `configure()` from `app/layout.tsx` as well as from the registry module, so the RSC graph sees the same states, recipes, and units.
192
198
 
193
199
  ### Options
194
200
 
@@ -202,13 +208,13 @@ During SSR, all functions discover the collector via the same global getter regi
202
208
  If your app uses Content Security Policy with nonces, configure it before rendering:
203
209
 
204
210
  ```tsx
205
- // app/layout.tsx or a server-side init file
211
+ // app/tasty-registry.tsx: the 'use client' module that renders TastyRegistry
206
212
  import { configure } from '@tenphi/tasty';
207
213
 
208
214
  configure({ nonce: 'your-nonce-value' });
209
215
  ```
210
216
 
211
- The nonce is automatically applied to all `<style>` and `<script>` tags injected by `TastyRegistry`.
217
+ The nonce is automatically applied to all `<style>` and `<script>` tags injected by `TastyRegistry`. Call it in the module that renders `TastyRegistry`; `app/layout.tsx` is a Server Component and configures a separate module instance.
212
218
 
213
219
  ---
214
220
 
@@ -216,13 +222,13 @@ The nonce is automatically applied to all `<style>` and `<script>` tags injected
216
222
 
217
223
  Tasty offers several levels of Astro integration. Choose the one that matches your needs:
218
224
 
219
- | Setup | Config needed | Deduplication | Hooks work | Client JS |
220
- | ---------------------------------------------------------------- | ------------- | ------------------------- | ---------------------- | -------------- |
221
- | Zero setup | None | Per render tree | Yes (within each tree) | None |
222
- | `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
223
- | `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
224
- | `tastyIntegration({ css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | Auto-hydration |
225
- | `tastyIntegration({ islands: false, css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | None |
225
+ | Setup | Config needed | Deduplication | Hooks work | Client JS |
226
+ | ---------------------------------------------------------------- | ------------- | ----------------------------------- | --------------------------------- | -------------- |
227
+ | Zero setup | None | None (each element inlines its CSS) | No (style functions emit nothing) | None |
228
+ | `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
229
+ | `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
230
+ | `tastyIntegration({ css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | Auto-hydration |
231
+ | `tastyIntegration({ islands: false, css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | None |
226
232
 
227
233
  ### Zero setup (static pages)
228
234
 
@@ -235,9 +241,9 @@ import { tasty } from '@tenphi/tasty';
235
241
  const Card = tasty({
236
242
  styles: {
237
243
  padding: '4x',
238
- fill: '#surface',
244
+ fill: '#fff',
239
245
  radius: '1r',
240
- border: true,
246
+ border: '1bw solid #ddd',
241
247
  },
242
248
  });
243
249
 
@@ -257,9 +263,9 @@ import Card from '../components/Card.tsx';
257
263
  </html>
258
264
  ```
259
265
 
260
- **Trade-offs**: Styles are deduplicated within each React render tree, but Astro renders separate component trees independently, so shared CSS (tokens, `@property` rules) may appear more than once. All style functions (`useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`) work in zero-setup mode — their CSS is accumulated in the RSC cache and flushed by the next `tasty()` component in the tree.
266
+ **Trade-offs**: Astro renders React with the standard `react` build, where `React.cache` does nothing, so there is no per-request style cache. Every `tasty()` element inlines the full CSS for its classes, even when the same component renders twice. Configured globals (`:root` tokens, `@property` defaults, `globalStyles`, `@font-face`) are never emitted, because only the SSR collector emits them, and CSS from `useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`, and `useFunction` is dropped. Without the [default properties](dsl.md#default-properties), styles that read them resolve to nothing: `border: true` and `outline: true` draw nothing, and `transition` with no duration does not animate. In development, Tasty warns once that it is rendering without a style collector.
261
267
 
262
- Best for quick prototyping, small static sites, or trying Tasty out in Astro.
268
+ Best for trying Tasty out with plain values, as above. Use `tastyIntegration()` for anything that relies on `configure()` or on Tasty's default tokens.
263
269
 
264
270
  ### Astro Integration (recommended)
265
271
 
@@ -296,7 +302,7 @@ import Interactive from '../components/Interactive.tsx';
296
302
 
297
303
  <html>
298
304
  <body>
299
- <Card>Static -- styles in <style data-tasty-ssr></Card>
305
+ <Card>Static -- styles are inlined into the page head</Card>
300
306
  <Interactive client:load>Island -- cache hydrated automatically</Interactive>
301
307
  </body>
302
308
  </html>
@@ -401,6 +407,8 @@ import '@tenphi/tasty/ssr/astro-client';
401
407
  export const onRequest = tastyMiddleware({ transferCache: false });
402
408
  ```
403
409
 
410
+ Build-wide extraction (`css: { mode: 'extract' }`) is available only through `tastyIntegration()`; `tastyMiddleware()` always inlines CSS.
411
+
404
412
  ### How it works
405
413
 
406
414
  Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware creates a `ServerStyleCollector` and binds it via `AsyncLocalStorage`. All `computeStyles()` calls within the request discover this collector automatically.
@@ -425,20 +433,16 @@ Call `configure({ nonce: '...' })` before any rendering happens. The middleware
425
433
  Any React-based framework can integrate using `runWithCollector`, which binds a `ServerStyleCollector` to the current async context via `AsyncLocalStorage`. All style function calls within the render automatically discover the collector.
426
434
 
427
435
  ```tsx
436
+ // ── Server ──────────────────────────────────────────────
428
437
  import {
429
- ServerStyleCollector,
430
438
  createServerStyleCollector,
431
439
  runWithCollector,
432
- hydrateTastyClasses,
433
440
  } from '@tenphi/tasty/ssr';
434
441
  import { renderToString } from 'react-dom/server';
435
- import { hydrateRoot } from 'react-dom/client';
436
-
437
- // ── Server ──────────────────────────────────────────────
438
442
 
439
443
  const collector = createServerStyleCollector();
440
444
 
441
- const html = await runWithCollector(collector, () => renderToString(<App />));
445
+ const html = runWithCollector(collector, () => renderToString(<App />));
442
446
 
443
447
  const css = collector.getCSS();
444
448
  const classNames = collector.getRenderedClassNames();
@@ -455,42 +459,56 @@ const fullHtml = `
455
459
  </body>
456
460
  </html>
457
461
  `;
462
+ ```
458
463
 
464
+ ```tsx
459
465
  // ── Client ──────────────────────────────────────────────
466
+ import { hydrateTastyClasses } from '@tenphi/tasty/ssr/client';
467
+ import { hydrateRoot } from 'react-dom/client';
460
468
 
461
- // Before hydration:
462
- hydrateTastyClasses(); // reads from window.__TASTY__
463
-
469
+ hydrateTastyClasses(); // reads window.__TASTY__
464
470
  hydrateRoot(document.getElementById('root'), <App />);
465
471
  ```
466
472
 
473
+ `@tenphi/tasty/ssr/client` is the browser half: it exports `hydrateTastyClasses()` and has no import side effects. A module shared by the server and the client can import `@tenphi/tasty/ssr` too. It is safe in a browser bundle, where there is no server collector to find, and only `runWithCollector()` throws there.
474
+
467
475
  ### Streaming SSR
468
476
 
469
477
  For streaming with `renderToPipeableStream`, use `flushCSS()` instead of `getCSS()`:
470
478
 
471
479
  ```tsx
480
+ import { PassThrough } from 'node:stream';
481
+ import { renderToPipeableStream } from 'react-dom/server';
482
+
472
483
  const collector = createServerStyleCollector();
473
484
 
474
- const stream = await runWithCollector(collector, () =>
475
- renderToPipeableStream(<App />, {
485
+ // React ends the stream it pipes into, so pipe into a PassThrough and
486
+ // append the remaining styles and the class list after it ends.
487
+ const body = new PassThrough();
488
+ body.pipe(res, { end: false });
489
+ body.on('end', () => {
490
+ // Styles collected after the shell (Suspense boundaries) + class list
491
+ const css = collector.flushCSS();
492
+ if (css) res.write(`<style data-tasty-ssr>${css}</style>`);
493
+
494
+ const classNames = collector.getRenderedClassNames();
495
+ res.end(
496
+ `</div><script>(window.__TASTY__=window.__TASTY__||[]).push(${JSON.stringify(classNames)})</script></body></html>`,
497
+ );
498
+ });
499
+
500
+ runWithCollector(collector, () => {
501
+ const { pipe } = renderToPipeableStream(<App />, {
476
502
  onShellReady() {
477
- // Flush styles collected so far
503
+ // Styles collected for the shell
478
504
  const css = collector.flushCSS();
479
- res.write(`<style data-tasty-ssr>${css}</style>`);
480
- stream.pipe(res);
481
- },
482
- onAllReady() {
483
- // Flush any remaining styles + class list
484
- const css = collector.flushCSS();
485
- if (css) res.write(`<style data-tasty-ssr>${css}</style>`);
486
-
487
- const classNames = collector.getRenderedClassNames();
488
505
  res.write(
489
- `<script>(window.__TASTY__=window.__TASTY__||[]).push(${JSON.stringify(classNames)})</script>`,
506
+ `<!DOCTYPE html><html><head><style data-tasty-ssr>${css}</style></head><body><div id="root">`,
490
507
  );
508
+ pipe(body);
491
509
  },
492
- }),
493
- );
510
+ });
511
+ });
494
512
  ```
495
513
 
496
514
  ---
@@ -499,14 +517,15 @@ const stream = await runWithCollector(collector, () =>
499
517
 
500
518
  ### Entry points
501
519
 
502
- | Import path | Description |
503
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
504
- | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `hydrateTastyClasses` |
505
- | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
506
- | `@tenphi/tasty/ssr/next-config` | Next.js config wrapper: shared, content-hashed global stylesheet |
507
- | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
508
- | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
509
- | `@tenphi/tasty/ssr/astro-middleware`<br />`@tenphi/tasty/ssr/astro-middleware-static`<br />`@tenphi/tasty/ssr/astro-middleware-extract`<br />`@tenphi/tasty/ssr/astro-middleware-extract-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
520
+ | Import path | Description |
521
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
522
+ | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `getSSRCollector`, `hydrateTastyClasses`; types `ServerStyleArtifact`, `ServerStyleArtifactKind`. Safe to import in a browser bundle; `runWithCollector()` needs the server |
523
+ | `@tenphi/tasty/ssr/client` | Browser-safe client hydration for any framework: `hydrateTastyClasses`, no import side effects |
524
+ | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
525
+ | `@tenphi/tasty/ssr/next-config` | Next.js config wrapper: shared, content-hashed global stylesheet |
526
+ | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
527
+ | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
528
+ | `@tenphi/tasty/ssr/astro-middleware`<br />`@tenphi/tasty/ssr/astro-middleware-static`<br />`@tenphi/tasty/ssr/astro-middleware-extract`<br />`@tenphi/tasty/ssr/astro-middleware-extract-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
510
529
 
511
530
  ### `ServerStyleCollector`
512
531
 
@@ -522,14 +541,16 @@ Constructor: `new ServerStyleCollector(namePrefix?)`, or use the `createServerSt
522
541
  | `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
523
542
  | `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
524
543
  | `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
525
- | `collectCounterStyle(name, css)` | Record a `@counter-style` rule. Deduplicated by name. |
544
+ | `collectCounterStyle(name, css, options?)` | Record a `@counter-style` rule, keyed by name. A different rule under the same name replaces the earlier one (and is re-flushed when streaming); `{ weak: true }`, used for `configure()` definitions, never replaces. |
545
+ | `collectFunction(name, css, options?)` | Record a CSS `@function` rule (from `useFunction` or `@function` styles). Same replace/`weak` semantics as `collectCounterStyle`. |
526
546
  | `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
527
547
  | `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
528
548
  | `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
529
549
  | `collectInternals()` | Collect eager configured globals: `@property`, `:root` tokens and presets, `@font-face`, `@counter-style`, `@function`, and `globalStyles`. Called automatically on first chunk collection; idempotent. |
530
550
  | `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
531
551
  | `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
532
- | `getRenderedClassNames()` | Get the list of class names rendered so far. Serialized to `window.__TASTY__` for client hydration via `hydrateTastyClasses()`. |
552
+ | `getRenderedClassNames()` | Get the class names allocated since the previous call (the first call returns all so far). Push each batch to `window.__TASTY__` for hydration via `hydrateTastyClasses()`. |
553
+ | `getArtifacts()` | Return the collected CSS as ordered, typed artifacts (`property`, `font-face`, `counter-style`, `function`, `raw`, `global`, `chunk`, `keyframes`). |
533
554
 
534
555
  ### `TastyRegistry`
535
556
 
@@ -552,9 +573,10 @@ into the Next config. Route-specific CSS continues through `TastyRegistry`.
552
573
 
553
574
  Astro integration factory. Registers middleware and optionally injects client hydration.
554
575
 
555
- | Option | Type | Default | Description |
556
- | --------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
557
- | `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
576
+ | Option | Type | Default | Description |
577
+ | --------- | ---------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
578
+ | `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
579
+ | `css` | `{ mode?: 'inline' \| 'extract' }` | `{ mode: 'inline' }` | `'extract'` writes content-hashed shared and per-page stylesheets for prerendered pages (Astro 5+). See [Build-wide CSS extraction](#build-wide-css-extraction). |
558
580
 
559
581
  ### `tastyMiddleware(options?)`
560
582
 
@@ -566,11 +588,11 @@ Astro middleware factory. Use for manual middleware composition.
566
588
 
567
589
  ### `hydrateTastyClasses(classes?)`
568
590
 
569
- Pre-populate the client injector's rules map with class names rendered on the server, marking them as already present in the DOM so `computeStyles()` skips re-injection during hydration. When called without arguments, reads the class list from `window.__TASTY__` (populated by the streaming `<script>` tags emitted during SSR).
591
+ Exported from `@tenphi/tasty/ssr/client` and `@tenphi/tasty/ssr`. Pre-populate the client injector's rules map with class names rendered on the server, marking them as already present in the DOM so `computeStyles()` skips re-injection during hydration. When called without arguments, reads the class list from `window.__TASTY__` (populated by the streaming `<script>` tags emitted during SSR).
570
592
 
571
593
  ### `runWithCollector(collector, fn)`
572
594
 
573
- Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All style function calls within `fn` (and async continuations) — including `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()` — will find this collector.
595
+ Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All style function calls within `fn` (and async continuations) — including `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — will find this collector. Returns `fn`'s return value, synchronously unless `fn` returns a promise. Throws where `AsyncLocalStorage` is unavailable, such as in a browser.
574
596
 
575
597
  ---
576
598
 
@@ -582,7 +604,7 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
582
604
 
583
605
  ### Hydration mismatch warnings
584
606
 
585
- Class names are deterministic for the same render order. If you see mismatches, ensure `hydrateTastyClasses()` runs before React hydration. For Next.js, this is automatic. For Astro with `tastyIntegration()`, this is also automatic. For manual Astro middleware setups, import `@tenphi/tasty/ssr/astro-client` in your island components. For custom setups, call `hydrateTastyClasses()` before `hydrateRoot()`.
607
+ Class names are content hashes of the resolved styles, so render order does not affect them. Unnamed `useKeyframes()` and `useCounterStyle()` names are counters (`tk0`, `tc0`) on both server and client; pass `name` if the two sides can call them in a different order. If you see mismatches, ensure `hydrateTastyClasses()` runs before React hydration. For Next.js, this is automatic. For Astro with `tastyIntegration()`, this is also automatic. For manual Astro middleware setups, import `@tenphi/tasty/ssr/astro-client` in your island components. For custom setups, call `hydrateTastyClasses()` from `@tenphi/tasty/ssr/client` before `hydrateRoot()`.
586
608
 
587
609
  Class names are also derived from the _resolved_ styles, so the server and the client must configure Tasty identically. Anything that changes what a component's styles resolve to will produce a mismatch if it is registered on only one side — `namePrefix`, `recipes`, `handlers`, and the `propHandlers` / `baseStyleProps` extension points described in [Plugins](plugins.md). Call the same `configure()` on both; global CSS is deduplicated automatically, so no `typeof window` guard is needed.
588
610
 
@@ -593,7 +615,7 @@ Class names are also derived from the _resolved_ styles, so the server and the c
593
615
  **Component CSS**: SSR `<style data-tasty-ssr>` tags remain in the DOM. The client injector creates separate `<style>` elements for any new styles. SSR styles are never modified or removed by the client. If this is a concern for very large apps, you can remove the SSR style tags and hydration scripts manually after hydration:
594
616
 
595
617
  ```tsx
596
- import { hydrateTastyClasses } from '@tenphi/tasty/ssr';
618
+ import { hydrateTastyClasses } from '@tenphi/tasty/ssr/client';
597
619
 
598
620
  hydrateTastyClasses();
599
621
  hydrateRoot(root, <App />);
@@ -607,4 +629,8 @@ document.querySelectorAll('script').forEach((el) => {
607
629
 
608
630
  ### `AsyncLocalStorage` not available
609
631
 
610
- The `@tenphi/tasty/ssr` entry point imports from `node:async_hooks`. This is excluded from client bundles by the build configuration. If you see import errors on the client, ensure your bundler treats `node:async_hooks` as external or use the `@tenphi/tasty/ssr/next` entry point (which does not use ALS).
632
+ `runWithCollector()` throws `runWithCollector() needs AsyncLocalStorage from node:async_hooks` where the runtime has no `AsyncLocalStorage`, such as in a browser bundle. Call it on the server. The rest of `@tenphi/tasty/ssr` is safe to import in a browser bundle; on the client, call `hydrateTastyClasses()` from `@tenphi/tasty/ssr/client`, or use `@tenphi/tasty/ssr/next`, which hydrates automatically.
633
+
634
+ ### "Server rendering without a style collector" warning
635
+
636
+ Tasty rendered on the server with no collector and no React Server Components request scope: Astro without `tastyIntegration()`, a client component server-rendered without `TastyRegistry`, or a custom setup that skips `runWithCollector()`. Each `tasty()` element then inlines all of its CSS, and configured globals (tokens, the default `@property` set, `globalStyles`, `@font-face`) and the CSS of the standalone style functions are not emitted. Add the integration for your framework. The warning is shown once, in development only.