@tenphi/tasty 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +399 -380
- package/dist/{babel-types-C_7kRP38.d.ts → babel-types-BKDTT4--.d.ts} +2 -2
- package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
- package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
- package/dist/chunks/{build-config-l2Ys-AJr.js → build-config-5d8WlQbq.js} +5 -5
- package/dist/chunks/{build-config-l2Ys-AJr.js.map → build-config-5d8WlQbq.js.map} +1 -1
- package/dist/chunks/{collector-CaNtsRtn.js → collector-CjCak4iE.js} +4 -4
- package/dist/chunks/{collector-CaNtsRtn.js.map → collector-CjCak4iE.js.map} +1 -1
- package/dist/chunks/{config-engine-CpExZO_T.js → config-engine-uOFYZidB.js} +4 -4
- package/dist/chunks/{config-engine-CpExZO_T.js.map → config-engine-uOFYZidB.js.map} +1 -1
- package/dist/chunks/config-file-DlZXJMj8.js +25 -0
- package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
- package/dist/chunks/{css-definitions-DAIJxzFY.js → css-definitions-ubrHP97m.js} +88 -77
- package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
- package/dist/chunks/{debug-CKy49GEr.js → debug-CJ31U_WE.js} +6 -6
- package/dist/chunks/{debug-CKy49GEr.js.map → debug-CJ31U_WE.js.map} +1 -1
- package/dist/chunks/{dsl-DRhFWQzo.js → dsl-DYSJc1-T.js} +11 -6
- package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
- package/dist/chunks/{react-runtime-oi6i3ELR.js → react-runtime-CyyCI02N.js} +168 -61
- package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
- package/dist/chunks/{runtime-engine-DoULyhPt.js → runtime-engine-BQmOu5-W.js} +53 -36
- package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
- package/dist/chunks/{runtime-state-CZ2J5QiZ.js → runtime-state-DEsyUMU-.js} +14 -3
- package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
- package/dist/chunks/{style-engine-CeKsjtIr.js → style-engine-BX2LshDZ.js} +116 -56
- package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
- package/dist/chunks/{zero-engine-fxKu8Jbn.js → zero-engine-Dw0P4yGM.js} +22 -7
- package/dist/chunks/zero-engine-Dw0P4yGM.js.map +1 -0
- package/dist/{collector-w4mBj5gd.d.ts → collector-CTU-nl0g.d.ts} +2 -2
- package/dist/{config-CrZOfNJ5.d.ts → config-CcxqSZI2.d.ts} +28 -9
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +7 -7
- package/dist/hydrate-CRyjjAyl.d.ts +22 -0
- package/dist/{index-CYJtAI9a.d.ts → index-xtey-KDu.d.ts} +43 -13
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/{merge-styles-VXwJ0Frr.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -2
- package/dist/ssr/astro.js +4 -4
- package/dist/ssr/client.d.ts +2 -0
- package/dist/ssr/client.js +2 -0
- package/dist/ssr/index.d.ts +3 -22
- package/dist/ssr/index.js +4 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next-config.d.ts +1 -1
- package/dist/ssr/next-config.js +6 -17
- package/dist/ssr/next-config.js.map +1 -1
- package/dist/ssr/next.d.ts +1 -1
- package/dist/ssr/next.js +3 -3
- package/dist/static/index.d.ts +2 -2
- package/dist/{types-JEY8PVYP.d.ts → types-DJEM_BOJ.d.ts} +6 -6
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +30 -19
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +24 -6
- package/dist/zero/index.js +4 -4
- package/dist/zero/index.js.map +1 -1
- package/dist/zero/next.d.ts +1 -1
- package/dist/zero/next.js +3 -7
- package/dist/zero/next.js.map +1 -1
- package/docs/README.md +4 -1
- package/docs/adoption.md +25 -17
- package/docs/ai-agents.md +37 -34
- package/docs/comparison.md +9 -8
- package/docs/configuration.md +173 -58
- package/docs/debug.md +3 -2
- package/docs/design-system.md +117 -15
- package/docs/dsl.md +120 -81
- package/docs/getting-started.md +81 -24
- package/docs/injector.md +39 -40
- package/docs/methodology.md +45 -20
- package/docs/migration-v3.md +2 -2
- package/docs/pipeline.md +19 -16
- package/docs/plugins.md +20 -40
- package/docs/react-api.md +21 -8
- package/docs/runtime-benchmarks.md +4 -4
- package/docs/ssr.md +83 -63
- package/docs/styles.md +27 -17
- package/docs/tasty-static.md +110 -56
- package/package.json +23 -9
- package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
- package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
- package/dist/chunks/css-definitions-DAIJxzFY.js.map +0 -1
- package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
- package/dist/chunks/react-runtime-oi6i3ELR.js.map +0 -1
- package/dist/chunks/runtime-engine-DoULyhPt.js.map +0 -1
- package/dist/chunks/runtime-state-CZ2J5QiZ.js.map +0 -1
- package/dist/chunks/style-engine-CeKsjtIr.js.map +0 -1
- package/dist/chunks/zero-engine-fxKu8Jbn.js.map +0 -1
package/docs/plugins.md
CHANGED
|
@@ -37,7 +37,7 @@ configure({ plugins: [spacingPlugin()] });
|
|
|
37
37
|
|
|
38
38
|
**Configuration is locked after the first render.** `configure()` warns and does nothing once any style has been generated, so it must run before your first component renders — at module scope in your entry file, or in a design-system module every component imports. `resetConfig()` reopens it, but it exists for tests.
|
|
39
39
|
|
|
40
|
-
**Repeated `configure()` calls merge
|
|
40
|
+
**Repeated `configure()` calls merge.** Each call adds to what earlier calls registered, key by key, in every registry (`states`, `units`, `functions`, `handlers`, `propHandlers`, `baseStyleProps`, `tokens`, `replaceTokens`, `recipes`, `keyframes`, `properties`, `fontFaces`, `counterStyles`); `globalStyles` merges per selector. A later value for the same key wins.
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -108,6 +108,7 @@ configure({
|
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
110
|
import { configure, mergeStyles } from '@tenphi/tasty';
|
|
111
|
+
import type { Styles } from '@tenphi/tasty';
|
|
111
112
|
|
|
112
113
|
configure({
|
|
113
114
|
propHandlers: {
|
|
@@ -115,7 +116,11 @@ configure({
|
|
|
115
116
|
const { glaze, ...rest } = props;
|
|
116
117
|
if (!glaze) return rest;
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
// glazeStyles: see the worked example below
|
|
120
|
+
return {
|
|
121
|
+
...rest,
|
|
122
|
+
styles: mergeStyles(glazeStyles(glaze), rest.styles as Styles),
|
|
123
|
+
};
|
|
119
124
|
},
|
|
120
125
|
},
|
|
121
126
|
});
|
|
@@ -159,7 +164,7 @@ configure({ baseStyleProps: ['radius', 'shadow'] });
|
|
|
159
164
|
<Card radius="1r" shadow />
|
|
160
165
|
```
|
|
161
166
|
|
|
162
|
-
Names must
|
|
167
|
+
Names must start with a lowercase letter and contain only letters and digits, and must not collide with a prop `tasty()` consumes itself (`as`, `styles`, `variant`, `mods`, `tokens`, …); invalid entries are dropped with a development warning. Names are not checked against the style registry, so a misspelled name is still harvested as a style prop.
|
|
163
168
|
|
|
164
169
|
`configure()` may run _after_ your components are defined — each factory resolves its prop list lazily and refreshes when the registry changes.
|
|
165
170
|
|
|
@@ -169,39 +174,9 @@ Names must be real style properties, must start with a lowercase letter, and mus
|
|
|
169
174
|
|
|
170
175
|
## Typing your extension
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
// tasty.d.ts
|
|
176
|
-
import type { StylePropValue } from '@tenphi/tasty';
|
|
177
|
-
|
|
178
|
-
declare module '@tenphi/tasty' {
|
|
179
|
-
// configure({ handlers }) — a new style property
|
|
180
|
-
interface StylesInterface {
|
|
181
|
-
stripe?: StylePropValue<string>;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// configure({ propHandlers }) — a new component prop
|
|
185
|
-
interface TastyCustomProps {
|
|
186
|
-
glaze: 'soft' | 'strong' | { tone: string; intensity?: number };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// configure({ baseStyleProps }) — promoted style names, typed like the style
|
|
190
|
-
interface TastyBaseStylePropNames {
|
|
191
|
-
radius: true;
|
|
192
|
-
shadow: true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// configure({ tokens }) / recipes / presets — autocomplete for names
|
|
196
|
-
interface TastyNamedColors {
|
|
197
|
-
'glaze-bg': true;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
177
|
+
Every extension point has a matching augmentation interface: `StylesInterface` for `handlers`, `TastyCustomProps` for `propHandlers`, `TastyBaseStylePropNames` for `baseStyleProps`, and `TastyNamedColors`, `TastyPresetNames`, and `TastyThemeNames` for the names your `tokens` and `presets` define. [Configuration → TypeScript](configuration.md#typescript) covers all six and the rules that go with them.
|
|
201
178
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
**Also update `tasty.config.ts`.** The ESLint plugin and VS Code extension validate style and token names against it, and neither is derived from the runtime registries:
|
|
179
|
+
**Also update `tasty.config.ts`.** The ESLint plugin and VS Code extension validate style and token names against it, and neither reads the runtime registries or your augmentations:
|
|
205
180
|
|
|
206
181
|
```ts
|
|
207
182
|
// tasty.config.ts
|
|
@@ -299,6 +274,8 @@ configure({ plugins: [glazePlugin()] });
|
|
|
299
274
|
|
|
300
275
|
```ts
|
|
301
276
|
// tasty.d.ts
|
|
277
|
+
import '@tenphi/tasty';
|
|
278
|
+
|
|
302
279
|
declare module '@tenphi/tasty' {
|
|
303
280
|
interface TastyCustomProps {
|
|
304
281
|
glaze: import('./glaze-plugin').GlazeConfig | string;
|
|
@@ -327,17 +304,20 @@ The object value is unambiguous here precisely because `glaze` is a **prop**. A
|
|
|
327
304
|
## Testing a plugin
|
|
328
305
|
|
|
329
306
|
```ts
|
|
330
|
-
import {
|
|
307
|
+
import { resetConfig, renderStyles } from '@tenphi/tasty';
|
|
308
|
+
import type { PropHandler, Styles } from '@tenphi/tasty';
|
|
331
309
|
|
|
332
310
|
describe('glaze plugin', () => {
|
|
333
311
|
beforeEach(() => resetConfig());
|
|
334
312
|
afterEach(() => resetConfig());
|
|
335
313
|
|
|
336
|
-
it('
|
|
337
|
-
|
|
314
|
+
it('turns the glaze prop into styles', () => {
|
|
315
|
+
const glaze = glazePlugin().propHandlers!.glaze as PropHandler;
|
|
316
|
+
const out = glaze({ glaze: 'purple' })!;
|
|
338
317
|
|
|
339
|
-
|
|
318
|
+
expect(out).not.toHaveProperty('glaze');
|
|
340
319
|
|
|
320
|
+
const rules = renderStyles(out.styles as Styles, '.test');
|
|
341
321
|
expect(rules[0].declarations).toContain('--glaze-bg-color');
|
|
342
322
|
});
|
|
343
323
|
});
|
|
@@ -345,7 +325,7 @@ describe('glaze plugin', () => {
|
|
|
345
325
|
|
|
346
326
|
Two things to know:
|
|
347
327
|
|
|
348
|
-
- **Create components inside each test.** `resetConfig()` does not clear
|
|
328
|
+
- **Create components inside each test.** `resetConfig()` does not clear a component's own class-name cache, which `tasty()` keeps in DOM environments, so a module-scope component can keep returning a class whose CSS the reset removed.
|
|
349
329
|
- **Dev-mode warnings may not fire.** `isDevEnv()` reports `false` for `NODE_ENV=test`. Warnings that check it lazily can be enabled with `vi.stubEnv('NODE_ENV', 'development')`; some older ones capture it at module load and cannot be triggered from a test at all.
|
|
350
330
|
|
|
351
331
|
---
|
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
|
-
|
|
5
|
+
Every Tasty style function except `useStyles()` — `tasty()` components, `computeStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — is hook-free and works in React Server Components with no `'use client'` directive. `useStyles()` reads the `TastyRegistry` collector from React context, so it is a real hook; in Server Components and 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,8 @@ 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 forwards props; it does not re-run the factory. `styleProps`, `modProps`, `tokenProps`, `variants`, and `elements` passed to `tasty(Component, …)` are ignored even though they type-check, and the base's sub-components are not copied (`tasty(Card, …).Title` is `undefined`). Declare them on the base component, or create the component with `tasty({ … })` directly.
|
|
48
|
+
|
|
47
49
|
`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
50
|
|
|
49
51
|
```jsx
|
|
@@ -68,7 +70,7 @@ const Span = tasty({
|
|
|
68
70
|
</Link>;
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty
|
|
73
|
+
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
74
|
|
|
73
75
|
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
76
|
|
|
@@ -112,12 +114,12 @@ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteS
|
|
|
112
114
|
|
|
113
115
|
## `Element`
|
|
114
116
|
|
|
115
|
-
`Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts
|
|
117
|
+
`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
118
|
|
|
117
119
|
```jsx
|
|
118
120
|
import { Element } from '@tenphi/tasty';
|
|
119
121
|
|
|
120
|
-
<Element as="section"
|
|
122
|
+
<Element as="section" display="grid" preset="t2" styles={{ padding: '4x' }}>
|
|
121
123
|
Content
|
|
122
124
|
</Element>;
|
|
123
125
|
```
|
|
@@ -144,6 +146,7 @@ List modifier key names. Types default to `ModValue` (`boolean | string | number
|
|
|
144
146
|
|
|
145
147
|
```jsx
|
|
146
148
|
const Button = tasty({
|
|
149
|
+
as: 'button',
|
|
147
150
|
modProps: ['isLoading', 'isSelected'] as const,
|
|
148
151
|
styles: {
|
|
149
152
|
fill: { '': '#surface', isLoading: '#surface.5' },
|
|
@@ -161,6 +164,7 @@ Map modifier names to type descriptors for precise TypeScript types:
|
|
|
161
164
|
|
|
162
165
|
```tsx
|
|
163
166
|
const Button = tasty({
|
|
167
|
+
as: 'button',
|
|
164
168
|
modProps: {
|
|
165
169
|
isLoading: Boolean, // isLoading?: boolean
|
|
166
170
|
isSelected: Boolean, // isSelected?: boolean
|
|
@@ -297,6 +301,8 @@ const Button = tasty({
|
|
|
297
301
|
<Button variant="danger">Delete</Button>;
|
|
298
302
|
```
|
|
299
303
|
|
|
304
|
+
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.
|
|
305
|
+
|
|
300
306
|
### Extending Variants with Base State Maps
|
|
301
307
|
|
|
302
308
|
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 +407,19 @@ const { className } = computeStyles({
|
|
|
401
407
|
});
|
|
402
408
|
```
|
|
403
409
|
|
|
404
|
-
On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS
|
|
410
|
+
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
411
|
|
|
406
412
|
---
|
|
407
413
|
|
|
408
414
|
## Style Functions
|
|
409
415
|
|
|
410
|
-
All style functions below are plain functions
|
|
416
|
+
All style functions below except `useStyles()` are plain functions, not React hooks, and work in client components, SSR with a `ServerStyleCollector`, and React Server Components. They keep the `use` prefix for backward compatibility.
|
|
411
417
|
|
|
412
418
|
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
419
|
|
|
414
420
|
### useStyles
|
|
415
421
|
|
|
416
|
-
Generate a className from a style object.
|
|
422
|
+
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: call it inside a component, and not in a Server Component — use `computeStyles()` there.
|
|
417
423
|
|
|
418
424
|
```tsx
|
|
419
425
|
import { useStyles } from '@tenphi/tasty';
|
|
@@ -482,6 +488,8 @@ function GlobalReset() {
|
|
|
482
488
|
An `id` slot holds one injection per `root`. Without an `id` the CSS is deduped
|
|
483
489
|
by content and permanent — there is nothing to replace it with later.
|
|
484
490
|
|
|
491
|
+
It also accepts a factory and deps — `useRawCSS(() => css, [deps], { id })` — and skips the factory while the deps for that `id` are unchanged.
|
|
492
|
+
|
|
485
493
|
### useKeyframes
|
|
486
494
|
|
|
487
495
|
Inject `@keyframes` rules and return the generated animation name:
|
|
@@ -591,7 +599,11 @@ useFontFace('Brand Sans', [
|
|
|
591
599
|
Signature:
|
|
592
600
|
|
|
593
601
|
```ts
|
|
594
|
-
function useFontFace(
|
|
602
|
+
function useFontFace(
|
|
603
|
+
family: string,
|
|
604
|
+
input: FontFaceInput,
|
|
605
|
+
options?: { root?: Document | ShadowRoot },
|
|
606
|
+
): void;
|
|
595
607
|
```
|
|
596
608
|
|
|
597
609
|
### useCounterStyle
|
|
@@ -684,6 +696,7 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
|
|
|
684
696
|
- **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
|
|
685
697
|
- **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
|
|
686
698
|
- **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
|
|
699
|
+
- **[Configuration → TypeScript](configuration.md#typescript)** — How props are typed and how to register your names
|
|
687
700
|
- **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
|
|
688
701
|
- **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin
|
|
689
702
|
- **[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
|
|
201
|
-
|
|
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
|
|
375
|
-
|
|
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` | >=
|
|
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`)
|
|
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({
|
|
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
|
|
151
|
-
when a route references
|
|
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 `
|
|
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
|
-
|
|
190
|
+
Every Tasty style function except `useStyles()` is hook-free and 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
|
-
- `
|
|
189
|
-
- `
|
|
193
|
+
- `computeStyles()` — the hook-free equivalent of `useStyles()`, which reads React context and cannot run in a Server Component
|
|
194
|
+
- `useGlobalStyles()`, `useRawCSS()` — inject styles by selector
|
|
195
|
+
- `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, `useFunction()` — inject ancillary CSS rules
|
|
190
196
|
|
|
191
|
-
|
|
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 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/
|
|
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
|
|
220
|
-
| ---------------------------------------------------------------- | ------------- |
|
|
221
|
-
| Zero setup | None |
|
|
222
|
-
| `tastyIntegration({ islands: false })` | One line | Cross-tree
|
|
223
|
-
| `tastyIntegration()` | One line | Cross-tree
|
|
224
|
-
| `tastyIntegration({ css: { mode: 'extract' } })` | One line | Cross-tree and cross-page
|
|
225
|
-
| `tastyIntegration({ islands: false, css: { mode: 'extract' } })` | One line | Cross-tree and cross-page
|
|
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
|
|
|
@@ -257,9 +263,9 @@ import Card from '../components/Card.tsx';
|
|
|
257
263
|
</html>
|
|
258
264
|
```
|
|
259
265
|
|
|
260
|
-
**Trade-offs**:
|
|
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.
|
|
261
267
|
|
|
262
|
-
Best for
|
|
268
|
+
Best for trying Tasty out with unconfigured styles. Use `tastyIntegration()` for anything that relies on `configure()`.
|
|
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
|
|
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 =
|
|
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 ──────────────────────────────────────────────
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
466
|
+
// Browser-safe: runs hydrateTastyClasses() with window.__TASTY__ on import
|
|
467
|
+
import '@tenphi/tasty/ssr/astro-client';
|
|
468
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
463
469
|
|
|
464
470
|
hydrateRoot(document.getElementById('root'), <App />);
|
|
465
471
|
```
|
|
466
472
|
|
|
473
|
+
`@tenphi/tasty/ssr` is server-only: it imports `node:async_hooks`, and in a browser bundle rendering throws `AsyncLocalStorage is not a constructor`. Despite its name, `@tenphi/tasty/ssr/astro-client` works with any framework.
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
//
|
|
478
|
-
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
|
|
503
|
+
// Styles collected for the shell
|
|
484
504
|
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
|
-
|
|
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,13 +517,13 @@ const stream = await runWithCollector(collector, () =>
|
|
|
499
517
|
|
|
500
518
|
### Entry points
|
|
501
519
|
|
|
502
|
-
| Import path
|
|
503
|
-
|
|
|
504
|
-
| `@tenphi/tasty/ssr`
|
|
505
|
-
| `@tenphi/tasty/ssr/next`
|
|
506
|
-
| `@tenphi/tasty/ssr/next-config`
|
|
507
|
-
| `@tenphi/tasty/ssr/astro`
|
|
508
|
-
| `@tenphi/tasty/ssr/astro-client`
|
|
520
|
+
| Import path | Description |
|
|
521
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
522
|
+
| `@tenphi/tasty/ssr` | Core SSR API (server-only): `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `getSSRCollector`, `hydrateTastyClasses`; types `ServerStyleArtifact`, `ServerStyleArtifactKind` |
|
|
523
|
+
| `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
|
|
524
|
+
| `@tenphi/tasty/ssr/next-config` | Next.js config wrapper: shared, content-hashed global stylesheet |
|
|
525
|
+
| `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
|
|
526
|
+
| `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
|
|
509
527
|
| `@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
528
|
|
|
511
529
|
### `ServerStyleCollector`
|
|
@@ -522,14 +540,16 @@ Constructor: `new ServerStyleCollector(namePrefix?)`, or use the `createServerSt
|
|
|
522
540
|
| `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
|
|
523
541
|
| `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
|
|
524
542
|
| `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
|
|
525
|
-
| `collectCounterStyle(name, css)`
|
|
543
|
+
| `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. |
|
|
544
|
+
| `collectFunction(name, css, options?)` | Record a CSS `@function` rule (from `useFunction` or `@function` styles). Same replace/`weak` semantics as `collectCounterStyle`. |
|
|
526
545
|
| `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
|
|
527
546
|
| `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
|
|
528
547
|
| `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
|
|
529
548
|
| `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
549
|
| `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
|
|
531
550
|
| `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
|
|
532
|
-
| `getRenderedClassNames()` | Get the
|
|
551
|
+
| `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()`. |
|
|
552
|
+
| `getArtifacts()` | Return the collected CSS as ordered, typed artifacts (`property`, `font-face`, `counter-style`, `function`, `raw`, `global`, `chunk`, `keyframes`). |
|
|
533
553
|
|
|
534
554
|
### `TastyRegistry`
|
|
535
555
|
|
|
@@ -552,9 +572,10 @@ into the Next config. Route-specific CSS continues through `TastyRegistry`.
|
|
|
552
572
|
|
|
553
573
|
Astro integration factory. Registers middleware and optionally injects client hydration.
|
|
554
574
|
|
|
555
|
-
| Option | Type
|
|
556
|
-
| --------- |
|
|
557
|
-
| `islands` | `boolean`
|
|
575
|
+
| Option | Type | Default | Description |
|
|
576
|
+
| --------- | ---------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
577
|
+
| `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
|
|
578
|
+
| `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
579
|
|
|
559
580
|
### `tastyMiddleware(options?)`
|
|
560
581
|
|
|
@@ -570,7 +591,7 @@ Pre-populate the client injector's rules map with class names rendered on the se
|
|
|
570
591
|
|
|
571
592
|
### `runWithCollector(collector, fn)`
|
|
572
593
|
|
|
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 `
|
|
594
|
+
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.
|
|
574
595
|
|
|
575
596
|
---
|
|
576
597
|
|
|
@@ -582,7 +603,7 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
|
|
|
582
603
|
|
|
583
604
|
### Hydration mismatch warnings
|
|
584
605
|
|
|
585
|
-
Class names are
|
|
606
|
+
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, import `@tenphi/tasty/ssr/astro-client` before `hydrateRoot()`.
|
|
586
607
|
|
|
587
608
|
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
609
|
|
|
@@ -593,9 +614,8 @@ Class names are also derived from the _resolved_ styles, so the server and the c
|
|
|
593
614
|
**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
615
|
|
|
595
616
|
```tsx
|
|
596
|
-
import
|
|
617
|
+
import '@tenphi/tasty/ssr/astro-client'; // runs hydrateTastyClasses()
|
|
597
618
|
|
|
598
|
-
hydrateTastyClasses();
|
|
599
619
|
hydrateRoot(root, <App />);
|
|
600
620
|
|
|
601
621
|
// Optional: remove SSR style tags and class-list scripts after hydration
|
|
@@ -607,4 +627,4 @@ document.querySelectorAll('script').forEach((el) => {
|
|
|
607
627
|
|
|
608
628
|
### `AsyncLocalStorage` not available
|
|
609
629
|
|
|
610
|
-
|
|
630
|
+
`@tenphi/tasty/ssr` imports `node:async_hooks` and is server-only. In a browser bundle the module is stubbed and rendering throws `AsyncLocalStorage is not a constructor`. On the client, import `@tenphi/tasty/ssr/astro-client` (it works with any framework) or use `@tenphi/tasty/ssr/next`, which hydrates automatically.
|