@tenphi/tasty 3.3.1 → 3.5.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.
Files changed (83) hide show
  1. package/README.md +147 -129
  2. package/dist/astro-ib7E7V4Y.js +307 -0
  3. package/dist/astro-ib7E7V4Y.js.map +1 -0
  4. package/dist/{babel-BUQGeOXA.d.ts → babel-DWCsafYw.d.ts} +2 -2
  5. package/dist/{collector-DUaHCcTS.d.ts → collector-BTVXj2hS.d.ts} +21 -3
  6. package/dist/{collector-BKqNBmzA.js → collector-C6TtL8HJ.js} +36 -13
  7. package/dist/collector-C6TtL8HJ.js.map +1 -0
  8. package/dist/{config-BCdCTIED.js → config-B3gPdCqd.js} +344 -209
  9. package/dist/config-B3gPdCqd.js.map +1 -0
  10. package/dist/{config-YsxGv4tq.d.ts → config-LfIDmVHx.d.ts} +132 -32
  11. package/dist/core/index.d.ts +5 -5
  12. package/dist/core/index.js +6 -6
  13. package/dist/{core-wxP3GHQu.js → core-Bq7w2kti.js} +219 -98
  14. package/dist/core-Bq7w2kti.js.map +1 -0
  15. package/dist/{css-writer-D64NY9AX.js → css-writer-Bgowobbx.js} +3 -3
  16. package/dist/{css-writer-D64NY9AX.js.map → css-writer-Bgowobbx.js.map} +1 -1
  17. package/dist/{format-rules-Bo_e2u7r.js → format-rules-rCZ37rqY.js} +2 -2
  18. package/dist/format-rules-rCZ37rqY.js.map +1 -0
  19. package/dist/{hydrate-CMKOuKAx.js → hydrate-CNOmZprz.js} +3 -3
  20. package/dist/{hydrate-CMKOuKAx.js.map → hydrate-CNOmZprz.js.map} +1 -1
  21. package/dist/{index-DhhUI0yi.d.ts → index-Bm9ltvdH.d.ts} +45 -16
  22. package/dist/{index-Cd45t5NM.d.ts → index-BmogEzTo.d.ts} +69 -15
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +9 -8
  25. package/dist/index.js.map +1 -1
  26. package/dist/{keyframes-D737PShJ.js → keyframes-DE-OE76F.js} +132 -21
  27. package/dist/keyframes-DE-OE76F.js.map +1 -0
  28. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BcLl1u_d.d.ts} +2 -2
  29. package/dist/{merge-styles-CUIQcs5v.js → merge-styles-DuoZEsm9.js} +2 -2
  30. package/dist/{merge-styles-CUIQcs5v.js.map → merge-styles-DuoZEsm9.js.map} +1 -1
  31. package/dist/{resolve-recipes-Df1Ta-Q0.js → resolve-recipes-H9NqOQuP.js} +3 -3
  32. package/dist/{resolve-recipes-Df1Ta-Q0.js.map → resolve-recipes-H9NqOQuP.js.map} +1 -1
  33. package/dist/ssr/astro-client.js +1 -1
  34. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  35. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  36. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  37. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  38. package/dist/ssr/astro-middleware-extract.js +9 -0
  39. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  40. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  41. package/dist/ssr/astro-middleware-static.js +1 -1
  42. package/dist/ssr/astro-middleware.d.ts +3 -1
  43. package/dist/ssr/astro-middleware.js +1 -1
  44. package/dist/ssr/astro.d.ts +37 -3
  45. package/dist/ssr/astro.js +1 -156
  46. package/dist/ssr/index.d.ts +2 -2
  47. package/dist/ssr/index.js +3 -3
  48. package/dist/ssr/index.js.map +1 -1
  49. package/dist/ssr/next.d.ts +1 -1
  50. package/dist/ssr/next.js +4 -4
  51. package/dist/static/index.d.ts +2 -2
  52. package/dist/static/index.js +1 -1
  53. package/dist/zero/babel.d.ts +1 -1
  54. package/dist/zero/babel.js +4 -4
  55. package/dist/zero/babel.js.map +1 -1
  56. package/dist/zero/index.d.ts +1 -1
  57. package/dist/zero/index.js +1 -1
  58. package/dist/zero/next.d.ts +1 -1
  59. package/docs/README.md +13 -8
  60. package/docs/adoption.md +56 -31
  61. package/docs/ai-agents.md +78 -77
  62. package/docs/comparison.md +69 -40
  63. package/docs/configuration.md +97 -87
  64. package/docs/debug.md +4 -2
  65. package/docs/design-system.md +41 -22
  66. package/docs/dsl.md +225 -195
  67. package/docs/getting-started.md +60 -39
  68. package/docs/injector.md +63 -26
  69. package/docs/methodology.md +42 -36
  70. package/docs/migration-v3.md +49 -49
  71. package/docs/plugins.md +37 -33
  72. package/docs/react-api.md +65 -44
  73. package/docs/runtime-benchmarks.md +178 -0
  74. package/docs/ssr.md +99 -61
  75. package/docs/styles.md +1 -1
  76. package/docs/tasty-static.md +137 -104
  77. package/package.json +26 -8
  78. package/dist/collector-BKqNBmzA.js.map +0 -1
  79. package/dist/config-BCdCTIED.js.map +0 -1
  80. package/dist/core-wxP3GHQu.js.map +0 -1
  81. package/dist/format-rules-Bo_e2u7r.js.map +0 -1
  82. package/dist/keyframes-D737PShJ.js.map +0 -1
  83. package/dist/ssr/astro.js.map +0 -1
@@ -0,0 +1,178 @@
1
+ # Runtime Benchmarks
2
+
3
+ Tasty keeps its performance claims in reproducible benchmarks rather than
4
+ combining unlike measurements into one score. The repository measures three
5
+ different costs:
6
+
7
+ 1. Style parsing and generation in Node.
8
+ 2. The React overhead of an empty `tasty({})` wrapper.
9
+ 3. Cold browser generation and injection compared with equivalent CSS that is
10
+ already on the page.
11
+
12
+ These are focused microbenchmarks, not page-load or interaction scores. Run
13
+ them several times on an otherwise idle machine and use a production profile
14
+ to decide whether any cost matters in an application.
15
+
16
+ ## Reproducing the Results
17
+
18
+ ```bash
19
+ pnpm bench
20
+ pnpm bench:overhead
21
+ pnpm bench:injection
22
+ ```
23
+
24
+ `pnpm bench` runs the core pipeline benchmarks in Node. The other two commands
25
+ use production code paths in headless Chromium. The first checkout may require
26
+ `pnpm test:setup` to download Chromium.
27
+
28
+ Run the Node and browser suites separately so they do not compete for CPU. The
29
+ browser timer has 0.1 ms resolution, so both browser benchmarks perform many
30
+ matched operations per sample and divide the absolute difference by the number
31
+ of elements or rules. Machine load, browser versions, and CPU power will move
32
+ the results.
33
+
34
+ ## Core Style Pipeline
35
+
36
+ The following numbers are single-call throughput measured on an Apple M1 Max
37
+ with Node 22:
38
+
39
+ | Operation | ops/sec | Latency (mean) |
40
+ | ----------------------------------------------------------- | -------------------: | -------------: |
41
+ | `renderStyles` — 5 flat properties (cold) | ~60,000 | ~17 us |
42
+ | `renderStyles` — state map with media/hover/modifier (cold) | ~18,500 | ~54 us |
43
+ | `renderStyles` — same styles (cached) | ~5,800,000 | ~0.17 us |
44
+ | `parseStateKey` — simple key like `:hover` (cold) | ~790,000 | ~1.3 us |
45
+ | `parseStateKey` — complex OR/AND/NOT key (cold) | ~140,000 | ~7 us |
46
+ | `parseStateKey` — any key (cached) | ~3,400,000–8,300,000 | ~0.1–0.3 us |
47
+ | `parseStyle` — value tokens like `2x 4x` (cold) | ~344,000 | ~2.9 us |
48
+ | `parseStyle` — color tokens (cold) | ~567,000 | ~1.8 us |
49
+ | `parseStyle` — any value (cached) | ~15,250,000 | ~0.07 us |
50
+
51
+ “Cold” cases use unique inputs to bypass the relevant caches. Cached cases
52
+ reuse one input and measure the LRU hot path. Expect roughly ±10% between runs.
53
+ These benchmarks do not include React, DOM work, stylesheet injection, style
54
+ resolution, layout, or paint.
55
+
56
+ The benchmark sources are colocated with the code they exercise:
57
+ [`pipeline.bench.ts`](../src/pipeline/pipeline.bench.ts),
58
+ [`parseStateKey.bench.ts`](../src/pipeline/parseStateKey.bench.ts), and the
59
+ parser benchmark files under [`src/parser`](../src/parser).
60
+
61
+ ## Empty Wrapper Overhead
62
+
63
+ Skipping the style pipeline does not make a `tasty()` component free. Even
64
+ `tasty({})` is a React component between its parent and the host element. React
65
+ tracks another fiber, and Tasty still processes and forwards the element's
66
+ props.
67
+
68
+ [`tasty-overhead.bench.tsx`](../src/tasty-overhead.bench.tsx) compares 10,000
69
+ raw `<div className>` siblings with 10,000 instances of one module-scoped
70
+ `tasty({})` component. Both receive the same props, and the benchmark fails if
71
+ they do not produce equivalent DOM.
72
+
73
+ The benchmark uses production React in headless Chromium. The factory is
74
+ created and its empty class-name cache is warmed before timing, so factory
75
+ creation, style generation, and injection are excluded. A detached container
76
+ excludes layout, paint, and stylesheet matching. Every commit is wrapped in
77
+ `flushSync`, keeping its synchronous reconciliation and commit inside the
78
+ sample. This does not estimate React's concurrent scheduling latency.
79
+
80
+ On an Apple M3 Pro with React 19.2.4 and Chromium 151, three consecutive runs
81
+ produced these ranges:
82
+
83
+ | Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
84
+ | ----------------------------------- | -----------: | -----------: | ------------------------: |
85
+ | Mount + remove | 4.5–4.9 ms | 14.0–14.9 ms | 0.95–1.00 us |
86
+ | Rerender, same host props | 1.3–1.4 ms | 10.9–11.4 ms | 0.96–1.00 us |
87
+ | Rerender, change one host attribute | 2.8–3.4 ms | 15.0–15.7 ms | 1.21–1.27 us |
88
+
89
+ The useful result is the raw/Tasty time difference divided by 10,000, not the
90
+ ratio between the two times. The ratio becomes large because the raw baseline
91
+ is tiny. In this synthetic workload, an empty wrapper adds roughly 1 us per
92
+ participating element, or 1.2–1.3 us when React also changes a DOM attribute.
93
+
94
+ This is the floor Tasty consumes when it has no styling job. It is not a
95
+ page-level score. Real trees include application components, effects, layout,
96
+ paint, and usually far fewer simultaneous styled-element updates. The benchmark
97
+ also does not measure retained memory; that requires a matched-tree heap
98
+ snapshot experiment with controlled garbage collection.
99
+
100
+ ## Cold Generation and Injection
101
+
102
+ [`tasty-injection.bench.ts`](../src/tasty-injection.bench.ts) measures the extra
103
+ work when Tasty must generate and inject CSS that an otherwise equivalent page
104
+ already has. It does not compare different stylesheet insertion techniques.
105
+
106
+ The benchmark covers two useful workloads:
107
+
108
+ - **One new rule:** add one rule to an existing stylesheet, append its one
109
+ element, and immediately read its computed style. Each timed sample performs
110
+ 50 independent one-rule transactions and reports their total; dividing the
111
+ raw/Tasty difference by 50 produces a stable per-rule result despite
112
+ Chromium's 0.1 ms timer resolution.
113
+ - **1,000 new rules together:** generate and insert all 1,000 rules into one
114
+ stylesheet, append all 1,000 elements, then read every computed color without
115
+ another write in between. This gives the browser one style-resolution
116
+ boundary for the group.
117
+
118
+ For every transaction, the existing-CSS control has the equivalent stylesheet
119
+ parsed, adopted, and attached before timing. The runtime root has a Tasty
120
+ stylesheet pre-created with an unrelated sentinel rule, but not the measured
121
+ rules. Both paths perform the same class assignment, DOM commit, and
122
+ computed-style reads. Only the runtime path calls `computeStyles()` and inserts
123
+ the new rules.
124
+
125
+ Preparation and cleanup happen outside the sample timer. Every runtime style
126
+ value is unique within a cycle, the relevant caches are cleared between cycles,
127
+ and a guard verifies that both paths resolve to the same color. React and the
128
+ `tasty()` wrapper are absent so their independently measured costs do not enter
129
+ the result. Pre-creating both stylesheets also excludes one-time sheet creation
130
+ and adoption from the subtraction.
131
+
132
+ On an Apple M3 Pro with Chromium 151, three consecutive runs produced these
133
+ ranges:
134
+
135
+ | Workload | CSS already present | Tasty runtime | Incremental Tasty cost |
136
+ | ---------------------------------------------------- | ------------------: | -------------: | ---------------------: |
137
+ | One new rule + immediate resolution, per transaction | 2.8–3.3 us | 110.3–113.8 us | 107.3–111.0 us |
138
+ | 1,000 new rules + one resolution | 1.86–2.06 ms | 9.01–9.98 ms | 7.13–7.92 ms |
139
+ | 1,000-rule workload, incremental cost per rule | — | — | 7.1–7.9 us |
140
+
141
+ Directly compared, injecting 1,000 rules before one resolution boundary cost
142
+ about 66–71 times as much in total as injecting one rule and resolving it—not
143
+ 1,000 times as much. Its average incremental cost per rule was about 14–16
144
+ times lower. This is the same Tasty generation and injection path in both cases;
145
+ the group amortizes fixed transaction work and lets the browser resolve all the
146
+ stylesheet writes together.
147
+
148
+ The subtraction is the meaningful result. It includes Tasty's cold style
149
+ generation, cache and injector bookkeeping, rule insertion, and any additional
150
+ style invalidation exposed by that workload's resolution boundary. It does not
151
+ pretend to isolate `insertRule()` from the system that calls it.
152
+
153
+ This is a deliberately cold workload. Reused styles resolve from cache and do
154
+ not inject another rule. Different rule complexity, DOM shape, stylesheet size,
155
+ browser, and hardware will change the number. The single-rule and 1,000-rule
156
+ results are not interchangeable: the first crosses the injection-to-resolution
157
+ boundary once per rule, while the second lets the browser resolve 1,000 writes
158
+ together. Because the same resolution pattern is present in each workload's
159
+ control, the difference answers the narrower delivery question: how much extra
160
+ work did Tasty perform when the same CSS was not already there?
161
+
162
+ ## Reading the Results Together
163
+
164
+ Do not add all three benchmark numbers to estimate an application blindly.
165
+ They describe different paths:
166
+
167
+ - A stable `tasty()` factory can skip the style pipeline on later renders, but
168
+ its React wrapper still participates in reconciliation.
169
+ - A cached style avoids cold parsing and generation and does not insert a new
170
+ rule.
171
+ - A genuinely new style pays generation and injection once, then becomes
172
+ reusable.
173
+ - Browser style resolution, layout, and paint depend on the actual document and
174
+ need application-level profiling.
175
+
176
+ The practical optimization target is therefore repeated work: keep style input
177
+ stable when possible, reuse generated chunks, and generate CSS at build or
178
+ server time when runtime flexibility is unnecessary.
package/docs/ssr.md CHANGED
@@ -1,16 +1,22 @@
1
1
  # Server-Side Rendering (SSR)
2
2
 
3
- Tasty supports server-side rendering with zero-cost client hydration. This does **not** introduce a separate styling engine: SSR uses the same runtime `tasty()` pipeline you already use on the client, then adds server-side CSS collection and client-side cache hydration. Your existing `tasty()` components work unchanged, and SSR remains opt-in with no per-component modifications. For the broader docs map, see the [Docs Hub](README.md).
3
+ Tasty supports server-side rendering with zero-cost client hydration. This does **not** introduce a separate styling engine: `tasty()` uses the same rendering pipeline on the server and in the browser, while the SSR integrations add server-side CSS collection and client-side cache hydration. Your existing `tasty()` components work unchanged, and SSR remains opt-in with no per-component modifications. For the broader docs map, see the [Docs Hub](README.md).
4
+
5
+ ## Zero-runtime terminology
6
+
7
+ Zero-runtime delivery is an outcome, not an alias for `tastyStatic()`. When `tasty()` components render only on the server, their CSS is delivered with the HTML and no Tasty styling runtime is shipped to the browser. Astro's `tastyIntegration({ islands: false })` is the explicit integration for this setup. Server-only Next.js React Server Components follow the same architecture, although you should verify the generated output for your deployment.
8
+
9
+ `tastyStatic()` reaches the same client-side outcome by extracting CSS during the build instead of during React rendering. Use it when extraction must happen before rendering or when the consumer is not React; see [Build-Time Extraction](tasty-static.md).
4
10
 
5
11
  ---
6
12
 
7
13
  ## Requirements
8
14
 
9
- | Dependency | Version | Required for |
10
- |---|---|---|
11
- | `react` | >= 18 | All SSR entry points (matches the current peer dependency of `@tenphi/tasty`) |
12
- | `next` | >= 13 | Next.js integration (`@tenphi/tasty/ssr/next`) — App Router with `useServerInsertedHTML` |
13
- | Node.js | >= 20 | Generic / streaming SSR (`@tenphi/tasty/ssr`) — uses `node:async_hooks` for `AsyncLocalStorage` |
15
+ | Dependency | Version | Required for |
16
+ | ---------- | ------- | ----------------------------------------------------------------------------------------------- |
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` |
19
+ | Node.js | >= 20 | Generic / streaming SSR (`@tenphi/tasty/ssr`) — uses `node:async_hooks` for `AsyncLocalStorage` |
14
20
 
15
21
  The Astro integration (`@tenphi/tasty/ssr/astro`) has no additional dependencies beyond `react`.
16
22
 
@@ -122,13 +128,15 @@ The nonce is automatically applied to all `<style>` and `<script>` tags injected
122
128
 
123
129
  ## Astro
124
130
 
125
- Tasty offers three levels of Astro integration. Choose the one that matches your needs:
131
+ Tasty offers several levels of Astro integration. Choose the one that matches your needs:
126
132
 
127
- | Setup | Config needed | Deduplication | Hooks work | Client JS |
128
- |---|---|---|---|---|
129
- | Zero setup | None | Per render tree | Yes (within each tree) | None |
130
- | `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
131
- | `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
133
+ | Setup | Config needed | Deduplication | Hooks work | Client JS |
134
+ | --------------------------------------------------------- | ------------- | ----------------------------- | ---------------------- | -------------- |
135
+ | Zero setup | None | Per render tree | Yes (within each tree) | None |
136
+ | `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
137
+ | `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
138
+ | `tastyIntegration({ css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | Auto-hydration |
139
+ | `tastyIntegration({ islands: false, css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | None |
132
140
 
133
141
  ### Zero setup (static pages)
134
142
 
@@ -225,6 +233,39 @@ export default defineConfig({
225
233
 
226
234
  This gives the same middleware deduplication and hook support, but ships zero client-side JavaScript. No class-list `<script>` is emitted.
227
235
 
236
+ #### Build-wide CSS extraction
237
+
238
+ Static Astro builds can move repeated Tasty CSS into a content-hashed,
239
+ browser-cacheable asset:
240
+
241
+ ```ts
242
+ export default defineConfig({
243
+ integrations: [
244
+ react(),
245
+ tastyIntegration({
246
+ islands: false,
247
+ css: {
248
+ mode: 'extract',
249
+ },
250
+ }),
251
+ ],
252
+ });
253
+ ```
254
+
255
+ `css.mode` defaults to `'inline'`, so existing projects keep their current
256
+ output. Extraction requires Astro 5 or newer and only applies to prerendered
257
+ production pages. Development, preview-time SSR, and on-demand routes continue
258
+ to receive the normal inline `<style data-tasty-ssr>` output.
259
+
260
+ Extraction moves the largest common block whose position can be preserved on
261
+ every generated page into one stylesheet. Page-only component, global, and raw
262
+ CSS stays inline, so unvisited routes do not receive unrelated styles.
263
+
264
+ The asset is written under Astro's configured `build.assets` directory (for
265
+ example, `/_astro/tasty.a1b2c3.css`). Links include the configured Astro
266
+ `base`, so nested routes do not need relative-path handling. The content hash
267
+ and output are deterministic for identical builds.
268
+
228
269
  ### Manual middleware (advanced)
229
270
 
230
271
  If you need to compose Tasty's middleware with other middleware (e.g., via `sequence()`), use `tastyMiddleware()` directly:
@@ -234,10 +275,7 @@ If you need to compose Tasty's middleware with other middleware (e.g., via `sequ
234
275
  import { sequence } from 'astro:middleware';
235
276
  import { tastyMiddleware } from '@tenphi/tasty/ssr/astro';
236
277
 
237
- export const onRequest = sequence(
238
- tastyMiddleware(),
239
- myOtherMiddleware,
240
- );
278
+ export const onRequest = sequence(tastyMiddleware(), myOtherMiddleware);
241
279
  ```
242
280
 
243
281
  For island hydration with manual middleware, import the client module in a shared entry point or in each island:
@@ -260,10 +298,14 @@ Astro's `@astrojs/react` renderer calls `renderToString()` for each React compon
260
298
  - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>` as a single `<style>` tag. No JavaScript is shipped.
261
299
  - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, the hydration script (auto-injected by `tastyIntegration()` or manually via `@tenphi/tasty/ssr/astro-client`) reads the class list from `window.__TASTY__` and pre-populates the injector's rules map. The island's `computeStyles()` calls see the class names as already registered and skip the pipeline during hydration.
262
300
  - The middleware reads the full response body, then injects the collected CSS into `</head>` before sending the final HTML.
301
+ - In extraction mode, prerendered responses also carry temporary structured
302
+ artifact metadata. `astro:build:done` uses those collector-provided
303
+ boundaries to write the shared asset and rewrite generated HTML, then removes
304
+ the metadata. CSS is never split on newlines or parsed heuristically.
263
305
 
264
306
  ### CSP nonce
265
307
 
266
- Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags.
308
+ Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags. In extraction mode, page-local inline style tags and the external stylesheet link retain the nonce.
267
309
 
268
310
  ---
269
311
 
@@ -285,9 +327,7 @@ import { hydrateRoot } from 'react-dom/client';
285
327
 
286
328
  const collector = createServerStyleCollector();
287
329
 
288
- const html = await runWithCollector(collector, () =>
289
- renderToString(<App />)
290
- );
330
+ const html = await runWithCollector(collector, () => renderToString(<App />));
291
331
 
292
332
  const css = collector.getCSS();
293
333
  const classNames = collector.getRenderedClassNames();
@@ -338,7 +378,7 @@ const stream = await runWithCollector(collector, () =>
338
378
  `<script>(window.__TASTY__=window.__TASTY__||[]).push(${JSON.stringify(classNames)})</script>`,
339
379
  );
340
380
  },
341
- })
381
+ }),
342
382
  );
343
383
  ```
344
384
 
@@ -348,13 +388,13 @@ const stream = await runWithCollector(collector, () =>
348
388
 
349
389
  ### Entry points
350
390
 
351
- | Import path | Description |
352
- |---|---|
353
- | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `hydrateTastyClasses` |
354
- | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
355
- | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
356
- | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
357
- | `@tenphi/tasty/ssr/astro-middleware`<br>`@tenphi/tasty/ssr/astro-middleware-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()`. |
391
+ | Import path | Description |
392
+ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
393
+ | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `hydrateTastyClasses` |
394
+ | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
395
+ | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
396
+ | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
397
+ | `@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()`. |
358
398
 
359
399
  ### `ServerStyleCollector`
360
400
 
@@ -362,47 +402,47 @@ Server-safe style collector. One instance per request.
362
402
 
363
403
  Constructor: `new ServerStyleCollector(namePrefix?)`, or use the `createServerStyleCollector(namePrefix?)` factory. The optional `namePrefix` overrides the value from `configure({ namePrefix })`; in normal usage you pass nothing and let the global config drive it. See [Configuration: Name prefix](configuration.md#name-prefix).
364
404
 
365
- | Method | Description |
366
- |---|---|
367
- | `allocateClassName(cacheKey)` | Allocate a deterministic, content-hashed class name for a cache key (e.g. `t1a2b3` with the default prefix). The same `cacheKey` always produces the same class name on server and client when both share the same `namePrefix`. Returns `{ className, isNewAllocation }`. |
368
- | `collectChunk(cacheKey, className, rules)` | Record CSS rules for a chunk. Deduplicated by `cacheKey`. |
369
- | `collectKeyframes(name, css)` | Record a `@keyframes` rule. Deduplicated by name. |
370
- | `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
371
- | `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
372
- | `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
373
- | `collectCounterStyle(name, css)` | Record a `@counter-style` rule. Deduplicated by name. |
374
- | `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
375
- | `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
376
- | `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
377
- | `collectInternals()` | Collect internal `@property` rules, `:root` token defaults, `@font-face`, and `@counter-style` rules from the global config. Called automatically on first chunk collection; idempotent. |
378
- | `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
379
- | `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
380
- | `getRenderedClassNames()` | Get the list of class names rendered so far. Serialized to `window.__TASTY__` for client hydration via `hydrateTastyClasses()`. |
405
+ | Method | Description |
406
+ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
407
+ | `allocateClassName(cacheKey)` | Allocate a deterministic, content-hashed class name for a cache key (e.g. `t1a2b3` with the default prefix). The same `cacheKey` always produces the same class name on server and client when both share the same `namePrefix`. Returns `{ className, isNewAllocation }`. |
408
+ | `collectChunk(cacheKey, className, rules)` | Record CSS rules for a chunk. Deduplicated by `cacheKey`. |
409
+ | `collectKeyframes(name, css)` | Record a `@keyframes` rule. Deduplicated by name. |
410
+ | `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
411
+ | `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
412
+ | `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
413
+ | `collectCounterStyle(name, css)` | Record a `@counter-style` rule. Deduplicated by name. |
414
+ | `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
415
+ | `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
416
+ | `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
417
+ | `collectInternals()` | Collect internal `@property` rules, `:root` token defaults, `@font-face`, and `@counter-style` rules from the global config. Called automatically on first chunk collection; idempotent. |
418
+ | `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
419
+ | `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
420
+ | `getRenderedClassNames()` | Get the list of class names rendered so far. Serialized to `window.__TASTY__` for client hydration via `hydrateTastyClasses()`. |
381
421
 
382
422
  ### `TastyRegistry`
383
423
 
384
424
  Next.js App Router component. Props:
385
425
 
386
- | Prop | Type | Default | Description |
387
- |---|---|---|---|
388
- | `children` | `ReactNode` | required | Application tree |
389
- | `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
426
+ | Prop | Type | Default | Description |
427
+ | --------------- | ----------- | -------- | ------------------------------------------------ |
428
+ | `children` | `ReactNode` | required | Application tree |
429
+ | `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
390
430
 
391
431
  ### `tastyIntegration(options?)`
392
432
 
393
433
  Astro integration factory. Registers middleware and optionally injects client hydration.
394
434
 
395
- | Option | Type | Default | Description |
396
- |---|---|---|---|
397
- | `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
435
+ | Option | Type | Default | Description |
436
+ | --------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
437
+ | `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
398
438
 
399
439
  ### `tastyMiddleware(options?)`
400
440
 
401
441
  Astro middleware factory. Use for manual middleware composition.
402
442
 
403
- | Option | Type | Default | Description |
404
- |---|---|---|---|
405
- | `transferCache` | `boolean` | `true` | Embed cache state script for island hydration |
443
+ | Option | Type | Default | Description |
444
+ | --------------- | --------- | ------- | --------------------------------------------- |
445
+ | `transferCache` | `boolean` | `true` | Embed cache state script for island hydration |
406
446
 
407
447
  ### `hydrateTastyClasses(classes?)`
408
448
 
@@ -424,11 +464,11 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
424
464
 
425
465
  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()`.
426
466
 
427
- 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.
467
+ 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.
428
468
 
429
469
  ### Styles duplicated after hydration
430
470
 
431
- **Global CSS** (`:root` tokens, `@property`, `globalStyles`, `@font-face`, `@counter-style`) configured via `configure()` is automatically deduplicated. When Tasty detects `<style data-tasty-ssr>` in the document, it skips client-side injection of globals that were already rendered by the SSR collector. This means `configure()` can be called with the full config on both server and client — no `typeof window === 'undefined'` guard is needed.
471
+ **Global CSS** (`:root` tokens, `@property`, `globalStyles`, `@font-face`, `@counter-style`, `@function`) configured via `configure()` is automatically deduplicated. When Tasty detects an inline or extracted `[data-tasty-ssr]` stylesheet in the document, it skips client-side injection of globals that were already rendered by the SSR collector. This means `configure()` can be called with the full config on both server and client — no `typeof window === 'undefined'` guard is needed.
432
472
 
433
473
  **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:
434
474
 
@@ -440,11 +480,9 @@ hydrateRoot(root, <App />);
440
480
 
441
481
  // Optional: remove SSR style tags and class-list scripts after hydration
442
482
  document.querySelectorAll('style[data-tasty-ssr]').forEach((el) => el.remove());
443
- document
444
- .querySelectorAll('script')
445
- .forEach((el) => {
446
- if (el.textContent?.includes('__TASTY__')) el.remove();
447
- });
483
+ document.querySelectorAll('script').forEach((el) => {
484
+ if (el.textContent?.includes('__TASTY__')) el.remove();
485
+ });
448
486
  ```
449
487
 
450
488
  ### `AsyncLocalStorage` not available
package/docs/styles.md CHANGED
@@ -305,7 +305,7 @@ SVG fill color. Same color token syntax as `fill` and `color`. Outputs the nativ
305
305
 
306
306
  ```jsx
307
307
  svgFill="#purple.10"
308
- svgFill="currentColor"
308
+ svgFill="#current"
309
309
  ```
310
310
 
311
311
  ---