@tenphi/tasty 0.0.0-snapshot.27b6708 → 0.0.0-snapshot.28eb83c

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 (106) hide show
  1. package/README.md +38 -54
  2. package/dist/{babel-BUQGeOXA.d.ts → babel-D6lOuSwk.d.ts} +2 -2
  3. package/dist/chunks/async-storage-DKK-wTD4.js.map +1 -0
  4. package/dist/chunks/build-config-BBThSdlo.js +45 -0
  5. package/dist/chunks/build-config-BBThSdlo.js.map +1 -0
  6. package/dist/{collector-DTahQUiV.js → chunks/collector-DcViW2ZQ.js} +28 -14
  7. package/dist/chunks/collector-DcViW2ZQ.js.map +1 -0
  8. package/dist/chunks/config-engine-DQVQK4NU.js +403 -0
  9. package/dist/chunks/config-engine-DQVQK4NU.js.map +1 -0
  10. package/dist/chunks/css-definitions-BZ9vm0ci.js +1284 -0
  11. package/dist/chunks/css-definitions-BZ9vm0ci.js.map +1 -0
  12. package/dist/chunks/css-resources-Cyl_axbI.js +149 -0
  13. package/dist/chunks/css-resources-Cyl_axbI.js.map +1 -0
  14. package/dist/chunks/debug-Vyauml7U.js +583 -0
  15. package/dist/chunks/debug-Vyauml7U.js.map +1 -0
  16. package/dist/chunks/dsl-CXvoaLnj.js +2097 -0
  17. package/dist/chunks/dsl-CXvoaLnj.js.map +1 -0
  18. package/dist/chunks/hydration-CLdVKcH2.js +59 -0
  19. package/dist/chunks/hydration-CLdVKcH2.js.map +1 -0
  20. package/dist/chunks/react-runtime-so_X_yMo.js +1526 -0
  21. package/dist/chunks/react-runtime-so_X_yMo.js.map +1 -0
  22. package/dist/chunks/runtime-engine-DQ5laRou.js +3056 -0
  23. package/dist/chunks/runtime-engine-DQ5laRou.js.map +1 -0
  24. package/dist/{merge-styles-oklji0KB.js → chunks/shared-utils-OIg0N_dp.js} +42 -3
  25. package/dist/chunks/shared-utils-OIg0N_dp.js.map +1 -0
  26. package/dist/{config-B5kHzuNz.js → chunks/style-engine-CBuOt1e4.js} +2387 -7442
  27. package/dist/chunks/style-engine-CBuOt1e4.js.map +1 -0
  28. package/dist/{css-writer-B-J87ncv.js → chunks/zero-engine-BFh7gKbK.js} +3 -3
  29. package/dist/chunks/zero-engine-BFh7gKbK.js.map +1 -0
  30. package/dist/{collector-DUaHCcTS.d.ts → collector-CAmndLXe.d.ts} +23 -3
  31. package/dist/{config-YsxGv4tq.d.ts → config-BWYrv_Aa.d.ts} +141 -37
  32. package/dist/core/index.d.ts +5 -5
  33. package/dist/core/index.js +9 -6
  34. package/dist/{index-Cd45t5NM.d.ts → index-BzAC3p06.d.ts} +118 -23
  35. package/dist/{index-PqN-DIpn.d.ts → index-D4kRLj2o.d.ts} +128 -68
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +10 -922
  38. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BFktNP8J.d.ts} +2 -2
  39. package/dist/ssr/astro-client.js +1 -1
  40. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  41. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  42. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  43. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  44. package/dist/ssr/astro-middleware-extract.js +9 -0
  45. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  46. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  47. package/dist/ssr/astro-middleware.d.ts +3 -1
  48. package/dist/ssr/astro.d.ts +45 -3
  49. package/dist/ssr/astro.js +157 -14
  50. package/dist/ssr/astro.js.map +1 -1
  51. package/dist/ssr/index.d.ts +7 -7
  52. package/dist/ssr/index.js +4 -4
  53. package/dist/ssr/index.js.map +1 -1
  54. package/dist/ssr/next-config.d.ts +66 -0
  55. package/dist/ssr/next-config.js +135 -0
  56. package/dist/ssr/next-config.js.map +1 -0
  57. package/dist/ssr/next.d.ts +9 -2
  58. package/dist/ssr/next.js +26 -10
  59. package/dist/ssr/next.js.map +1 -1
  60. package/dist/static/index.d.ts +2 -2
  61. package/dist/static/index.js +1 -1
  62. package/dist/zero/babel.d.ts +1 -1
  63. package/dist/zero/babel.js +25 -24
  64. package/dist/zero/babel.js.map +1 -1
  65. package/dist/zero/index.d.ts +1 -1
  66. package/dist/zero/index.js +1 -1
  67. package/dist/zero/next.d.ts +1 -1
  68. package/docs/README.md +5 -3
  69. package/docs/adoption.md +3 -3
  70. package/docs/ai-agents.md +83 -78
  71. package/docs/comparison.md +12 -12
  72. package/docs/configuration.md +47 -47
  73. package/docs/debug.md +19 -7
  74. package/docs/dsl.md +3 -3
  75. package/docs/getting-started.md +10 -10
  76. package/docs/injector.md +62 -25
  77. package/docs/methodology.md +3 -3
  78. package/docs/migration-v3.md +49 -49
  79. package/docs/plugins.md +37 -33
  80. package/docs/react-api.md +2 -2
  81. package/docs/runtime-benchmarks.md +351 -0
  82. package/docs/ssr.md +219 -61
  83. package/docs/styles.md +43 -11
  84. package/docs/tasty-static.md +136 -103
  85. package/package.json +80 -11
  86. package/dist/async-storage-DKK-wTD4.js.map +0 -1
  87. package/dist/collector-DTahQUiV.js.map +0 -1
  88. package/dist/config-B5kHzuNz.js.map +0 -1
  89. package/dist/context-CA8YKeMn.js +0 -24
  90. package/dist/context-CA8YKeMn.js.map +0 -1
  91. package/dist/core-Dr4u1NVD.js +0 -1566
  92. package/dist/core-Dr4u1NVD.js.map +0 -1
  93. package/dist/css-writer-B-J87ncv.js.map +0 -1
  94. package/dist/format-global-rules-DklyaXv-.js +0 -22
  95. package/dist/format-global-rules-DklyaXv-.js.map +0 -1
  96. package/dist/format-rules-DKOA-6qu.js +0 -130
  97. package/dist/format-rules-DKOA-6qu.js.map +0 -1
  98. package/dist/hydrate-OeMX99We.js +0 -37
  99. package/dist/hydrate-OeMX99We.js.map +0 -1
  100. package/dist/index.js.map +0 -1
  101. package/dist/keyframes-CV8azJf3.js +0 -493
  102. package/dist/keyframes-CV8azJf3.js.map +0 -1
  103. package/dist/merge-styles-oklji0KB.js.map +0 -1
  104. package/dist/resolve-recipes-DTG81rzl.js +0 -144
  105. package/dist/resolve-recipes-DTG81rzl.js.map +0 -1
  106. /package/dist/{async-storage-DKK-wTD4.js → chunks/async-storage-DKK-wTD4.js} +0 -0
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
 
@@ -80,6 +86,92 @@ export default function RootLayout({
80
86
 
81
87
  That's it. All `tasty()` components inside the tree automatically get SSR support. No per-component changes needed.
82
88
 
89
+ ### Optional shared globals stylesheet
90
+
91
+ By default, configured global CSS is included in the streamed Tasty style tag
92
+ for every route. `withTastyNext()` can move that stable CSS into one
93
+ content-hashed stylesheet shared by all routes while leaving component and hook
94
+ styles in the normal streaming path.
95
+
96
+ Keep the Tasty config in a side-effect-free module:
97
+
98
+ ```ts
99
+ // app/tasty.config.ts
100
+ import type { TastyConfig } from '@tenphi/tasty';
101
+
102
+ const config: TastyConfig = {
103
+ tokens: { $gap: '8px', '#brand': 'rebeccapurple' },
104
+ globalStyles: { body: { margin: '0', color: '#brand' } },
105
+ fontFaces: {
106
+ Brand: { src: 'url("/fonts/brand.woff2") format("woff2")' },
107
+ },
108
+ };
109
+
110
+ export default config;
111
+ ```
112
+
113
+ Use it from the Next config:
114
+
115
+ ```ts
116
+ // next.config.ts
117
+ import { withTastyNext } from '@tenphi/tasty/ssr/next-config';
118
+ import config from './app/tasty.config';
119
+
120
+ export default withTastyNext({
121
+ config,
122
+ })({
123
+ // your Next.js config
124
+ });
125
+ ```
126
+
127
+ The runtime must receive the same config. Import and configure it before the
128
+ registry renders:
129
+
130
+ ```tsx
131
+ // app/tasty-registry.tsx
132
+ 'use client';
133
+
134
+ import { configure } from '@tenphi/tasty';
135
+ import { TastyRegistry } from '@tenphi/tasty/ssr/next';
136
+ import config from './tasty.config';
137
+
138
+ configure(config);
139
+
140
+ export default function TastyStyleRegistry({ children }) {
141
+ return <TastyRegistry>{children}</TastyRegistry>;
142
+ }
143
+ ```
144
+
145
+ The generated file contains eager configuration artifacts: built-in and custom
146
+ `@property` rules, tokens and presets, `@font-face`, `@counter-style`, native
147
+ CSS `@function` definitions, and `globalStyles`. Route-dependent component
148
+ rules and calls to `useGlobalStyles`, `useRawCSS`, `useKeyframes`,
149
+ `useProperty`, `useFontFace`, `useCounterStyle`, and `useFunction` remain
150
+ route-specific. Configured keyframes also remain lazy and are streamed only
151
+ when a route references them.
152
+
153
+ The default output directory is `public/_tasty`. The wrapper adds the generated
154
+ URL to `TastyRegistry`, respects `basePath`, preserves existing `env` and
155
+ `headers` config, and serves the content-hashed file with an immutable one-year
156
+ cache header on Next server deployments. Static exports keep the content-hashed
157
+ URL and leave cache headers to the hosting provider. Older hashes are not
158
+ deleted automatically, so rolling deployments cannot break pages from the
159
+ previous build; clean the generated directory as part of a clean deployment if
160
+ needed. Page-relative CSS resources such as
161
+ `url(../fonts/brand.woff2)` are rejected because moving them would change their
162
+ meaning; use root-relative, absolute, or data URLs.
163
+
164
+ `withTastyNext()` options:
165
+
166
+ | Option | Type | Default | Description |
167
+ | ------------ | ------------- | ------------------------- | ---------------------------------------------------------------------- |
168
+ | `config` | `TastyConfig` | — | Config object; takes precedence over `configFile` |
169
+ | `configFile` | `string` | — | Project-relative config module path; requires the optional `jiti` peer |
170
+ | `rootDir` | `string` | current directory | Next app root when the build runs from a monorepo root |
171
+ | `outputDir` | `string` | `public/_tasty` | Filesystem output directory |
172
+ | `publicPath` | `string` | inferred from `outputDir` | Root-relative URL; required when output is outside `public` |
173
+ | `enabled` | `boolean` | `true` | Disable generation without changing wrapper composition |
174
+
83
175
  ### How it works
84
176
 
85
177
  - `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.
@@ -122,13 +214,15 @@ The nonce is automatically applied to all `<style>` and `<script>` tags injected
122
214
 
123
215
  ## Astro
124
216
 
125
- Tasty offers three levels of Astro integration. Choose the one that matches your needs:
217
+ Tasty offers several levels of Astro integration. Choose the one that matches your needs:
126
218
 
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 |
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 |
132
226
 
133
227
  ### Zero setup (static pages)
134
228
 
@@ -225,6 +319,63 @@ export default defineConfig({
225
319
 
226
320
  This gives the same middleware deduplication and hook support, but ships zero client-side JavaScript. No class-list `<script>` is emitted.
227
321
 
322
+ #### Build-wide CSS extraction
323
+
324
+ Static Astro builds can move Tasty CSS into content-hashed, browser-cacheable
325
+ shared and page assets:
326
+
327
+ ```ts
328
+ export default defineConfig({
329
+ integrations: [
330
+ react(),
331
+ tastyIntegration({
332
+ islands: false,
333
+ css: {
334
+ mode: 'extract',
335
+ },
336
+ }),
337
+ ],
338
+ });
339
+ ```
340
+
341
+ `css.mode` defaults to `'inline'`, so existing projects keep their current
342
+ output. Extraction requires Astro 5 or newer and only applies to prerendered
343
+ production pages. Development, preview-time SSR, and on-demand routes continue
344
+ to receive the normal inline `<style data-tasty-ssr>` output.
345
+
346
+ Extraction writes every artifact emitted by all styled pages to a shared
347
+ stylesheet. Each page's strict set difference is written to a separate page
348
+ stylesheet. The shared link comes first and the page link follows, so shared
349
+ styles form the base cascade and page-only styles can override them. A fully
350
+ shared page omits the empty page stylesheet. If generated pages have no common
351
+ artifacts, each page receives only its page stylesheet.
352
+
353
+ The shared-base/page-override order is the extraction-mode cascade contract.
354
+ It does not preserve an inline artifact order where a page-only rule originally
355
+ appeared before a shared rule. Use shared styles for defaults and page-only
356
+ styles for overrides.
357
+
358
+ Extracted CSS preserves resource URLs verbatim. Relative URLs in an inline
359
+ style resolve from the page, but in an extracted stylesheet they resolve from
360
+ the asset directory. Use absolute URLs or data URLs when extraction is enabled.
361
+ Root-relative URLs such as `url(/fonts/brand.woff2)` are also safe while the
362
+ stylesheet stays on the page's origin. The build fails with a clear error if an
363
+ artifact contains a page-relative or fragment-only URL, including URL strings
364
+ in `image-set()`, `image()`, `src()`, and `@import`.
365
+
366
+ Assets are written under Astro's configured `build.assets` directory (for
367
+ example, `/_astro/tasty.shared.a1b2c3.css` and
368
+ `/_astro/tasty.page.d4e5f6.css`). Links include the configured Astro `base`, so
369
+ nested routes do not need relative-path handling. Content hashes and output are
370
+ deterministic for identical builds. If `build.assetsPrefix` is configured,
371
+ Tasty uses its CSS-specific prefix (or `fallback`) just like Astro-generated
372
+ stylesheets. When that prefix points to a different origin, root-relative
373
+ resources would resolve against the asset origin rather than the page's origin,
374
+ so the build rejects them as well. Tasty compares the prefix with Astro's `site`
375
+ when it is configured; without `site`, an absolute or protocol-relative prefix
376
+ is treated conservatively as cross-origin. Use a fully absolute resource URL in
377
+ that configuration.
378
+
228
379
  ### Manual middleware (advanced)
229
380
 
230
381
  If you need to compose Tasty's middleware with other middleware (e.g., via `sequence()`), use `tastyMiddleware()` directly:
@@ -234,10 +385,7 @@ If you need to compose Tasty's middleware with other middleware (e.g., via `sequ
234
385
  import { sequence } from 'astro:middleware';
235
386
  import { tastyMiddleware } from '@tenphi/tasty/ssr/astro';
236
387
 
237
- export const onRequest = sequence(
238
- tastyMiddleware(),
239
- myOtherMiddleware,
240
- );
388
+ export const onRequest = sequence(tastyMiddleware(), myOtherMiddleware);
241
389
  ```
242
390
 
243
391
  For island hydration with manual middleware, import the client module in a shared entry point or in each island:
@@ -260,10 +408,15 @@ Astro's `@astrojs/react` renderer calls `renderToString()` for each React compon
260
408
  - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>` as a single `<style>` tag. No JavaScript is shipped.
261
409
  - **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
410
  - The middleware reads the full response body, then injects the collected CSS into `</head>` before sending the final HTML.
411
+ - In extraction mode, prerendered responses also carry temporary structured
412
+ artifact metadata. `astro:build:done` uses those collector-provided
413
+ boundaries to write shared and page assets and rewrite generated HTML, then
414
+ removes the metadata. Artifact boundaries are never inferred by splitting or
415
+ reparsing the generated CSS.
263
416
 
264
417
  ### CSP nonce
265
418
 
266
- Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags.
419
+ Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags. In extraction mode, the external stylesheet links retain the nonce.
267
420
 
268
421
  ---
269
422
 
@@ -285,9 +438,7 @@ import { hydrateRoot } from 'react-dom/client';
285
438
 
286
439
  const collector = createServerStyleCollector();
287
440
 
288
- const html = await runWithCollector(collector, () =>
289
- renderToString(<App />)
290
- );
441
+ const html = await runWithCollector(collector, () => renderToString(<App />));
291
442
 
292
443
  const css = collector.getCSS();
293
444
  const classNames = collector.getRenderedClassNames();
@@ -338,7 +489,7 @@ const stream = await runWithCollector(collector, () =>
338
489
  `<script>(window.__TASTY__=window.__TASTY__||[]).push(${JSON.stringify(classNames)})</script>`,
339
490
  );
340
491
  },
341
- })
492
+ }),
342
493
  );
343
494
  ```
344
495
 
@@ -348,13 +499,14 @@ const stream = await runWithCollector(collector, () =>
348
499
 
349
500
  ### Entry points
350
501
 
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()`. |
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()`. |
358
510
 
359
511
  ### `ServerStyleCollector`
360
512
 
@@ -362,47 +514,55 @@ Server-safe style collector. One instance per request.
362
514
 
363
515
  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
516
 
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()`. |
517
+ | Method | Description |
518
+ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
519
+ | `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 }`. |
520
+ | `collectChunk(cacheKey, className, rules)` | Record CSS rules for a chunk. Deduplicated by `cacheKey`. |
521
+ | `collectKeyframes(name, css)` | Record a `@keyframes` rule. Deduplicated by name. |
522
+ | `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
523
+ | `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
524
+ | `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
525
+ | `collectCounterStyle(name, css)` | Record a `@counter-style` rule. Deduplicated by name. |
526
+ | `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
527
+ | `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
528
+ | `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
529
+ | `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
+ | `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
531
+ | `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()`. |
381
533
 
382
534
  ### `TastyRegistry`
383
535
 
384
536
  Next.js App Router component. Props:
385
537
 
386
- | Prop | Type | Default | Description |
387
- |---|---|---|---|
388
- | `children` | `ReactNode` | required | Application tree |
389
- | `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
538
+ | Prop | Type | Default | Description |
539
+ | ------------------ | ----------------- | --------- | ------------------------------------------------------------------------- |
540
+ | `children` | `ReactNode` | required | Application tree |
541
+ | `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
542
+ | `sharedStylesheet` | `string \| false` | generated | Override the shared URL, or disable generated shared CSS for the registry |
543
+
544
+ ### `withTastyNext(options)`
545
+
546
+ Next.js configuration wrapper exported from
547
+ `@tenphi/tasty/ssr/next-config`. It generates a shared stylesheet from eager
548
+ Tasty configuration artifacts and wires its URL and immutable cache header
549
+ into the Next config. Route-specific CSS continues through `TastyRegistry`.
390
550
 
391
551
  ### `tastyIntegration(options?)`
392
552
 
393
553
  Astro integration factory. Registers middleware and optionally injects client hydration.
394
554
 
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. |
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. |
398
558
 
399
559
  ### `tastyMiddleware(options?)`
400
560
 
401
561
  Astro middleware factory. Use for manual middleware composition.
402
562
 
403
- | Option | Type | Default | Description |
404
- |---|---|---|---|
405
- | `transferCache` | `boolean` | `true` | Embed cache state script for island hydration |
563
+ | Option | Type | Default | Description |
564
+ | --------------- | --------- | ------- | --------------------------------------------- |
565
+ | `transferCache` | `boolean` | `true` | Embed cache state script for island hydration |
406
566
 
407
567
  ### `hydrateTastyClasses(classes?)`
408
568
 
@@ -424,11 +584,11 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
424
584
 
425
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()`.
426
586
 
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.
587
+ 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
588
 
429
589
  ### Styles duplicated after hydration
430
590
 
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.
591
+ **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
592
 
433
593
  **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
594
 
@@ -440,11 +600,9 @@ hydrateRoot(root, <App />);
440
600
 
441
601
  // Optional: remove SSR style tags and class-list scripts after hydration
442
602
  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
- });
603
+ document.querySelectorAll('script').forEach((el) => {
604
+ if (el.textContent?.includes('__TASTY__')) el.remove();
605
+ });
448
606
  ```
449
607
 
450
608
  ### `AsyncLocalStorage` not available
package/docs/styles.md CHANGED
@@ -20,9 +20,10 @@ Use these instead of their raw CSS counterparts:
20
20
  |-----|------------|
21
21
  | `fill` | `backgroundColor`, `background` |
22
22
  | `image` | `backgroundImage` |
23
- | `padding` (with direction modifiers) | `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` |
24
- | `margin` (with direction modifiers) | `marginTop`, `marginRight`, `marginBottom`, `marginLeft` |
25
- | `width` (with `min`/`max` modifiers) | `minWidth`, `maxWidth` |
23
+ | `padding` (with physical direction modifiers) | `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` |
24
+ | `margin` (with physical direction modifiers) | `marginTop`, `marginRight`, `marginBottom`, `marginLeft` |
25
+ | Logical block/inline category styles (with `start`/`end` modifiers) | Native logical axis/edge declarations |
26
+ | `width` / `blockSize` / `inlineSize` (with `min`/`max` modifiers) | Their separate min/max declarations when one state map is sufficient |
26
27
  | `height` (with `min`/`max` modifiers) | `minHeight`, `maxHeight` |
27
28
  | `border` | `borderColor`, `borderWidth`, `borderStyle` |
28
29
  | `radius` | `borderRadius` |
@@ -125,9 +126,7 @@ A group that names *no* direction keeps plain CSS shorthand order, which is unam
125
126
 
126
127
  Later comma-separated groups override earlier groups for conflicting directions.
127
128
 
128
- Individual props `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `paddingBlock`, `paddingInline` are supported but `padding` with modifiers is recommended.
129
-
130
- **Priority:** `padding` < `paddingBlock`/`paddingInline` < `paddingTop`/`paddingRight`/`paddingBottom`/`paddingLeft`
129
+ Individual physical props `paddingTop`, `paddingRight`, `paddingBottom`, and `paddingLeft` are supported, but `padding` with modifiers is recommended. Use `blockPadding` or `inlinePadding` for an enhanced logical-axis shorthand; native declarations such as `paddingInlineStart` remain ordinary CSS properties. See [Logical properties](#logical-properties).
131
130
 
132
131
  ### `margin`
133
132
 
@@ -153,9 +152,7 @@ The [one-value-per-directional-group rule](#padding) from `padding` applies here
153
152
 
154
153
  Later comma-separated groups override earlier groups for conflicting directions.
155
154
 
156
- Individual props `marginTop`, `marginRight`, `marginBottom`, `marginLeft`, `marginBlock`, `marginInline` are supported but `margin` with modifiers is recommended.
157
-
158
- **Priority:** `margin` < `marginBlock`/`marginInline` < `marginTop`/`marginRight`/`marginBottom`/`marginLeft`
155
+ Individual physical props `marginTop`, `marginRight`, `marginBottom`, and `marginLeft` are supported, but `margin` with modifiers is recommended. Use `blockMargin` or `inlineMargin` for an enhanced logical-axis shorthand; native declarations such as `marginInlineStart` remain ordinary CSS properties. See [Logical properties](#logical-properties).
159
156
 
160
157
  ### `width`
161
158
 
@@ -234,9 +231,44 @@ second to every side they span.
234
231
 
235
232
  Later comma-separated groups override earlier groups for conflicting directions.
236
233
 
237
- Individual props `top`, `right`, `bottom`, `left`, `insetBlock`, `insetInline` are supported. When only individual direction props are used (without `inset`), individual CSS properties are output for correct cascade behavior with state overrides.
234
+ Individual physical props `top`, `right`, `bottom`, and `left` are supported. When only those props are used (without `inset`), individual CSS properties are output for correct cascade behavior with state overrides. Use `blockInset` or `inlineInset` for logical axes; native declarations such as `insetInlineStart` remain ordinary CSS properties.
235
+
236
+ ### Logical properties
237
+
238
+ Tasty provides one enhanced handler for each logical axis/category pair. Each handler emits native logical CSS instead of converting values to `top`, `right`, `bottom`, `left`, `width`, or `height`. The browser therefore resolves `block`, `inline`, `start`, and `end` from the element's `writingMode` and `direction`.
239
+
240
+ ```jsx
241
+ styles: {
242
+ direction: 'rtl',
243
+ inlinePadding: '1x start, 2x end',
244
+ inlineBorder: '1bw solid #accent start',
245
+ blockInset: '0 end',
246
+ }
247
+ ```
248
+
249
+ An axis handler accepts one value for both edges or two values in logical **start/end** order. Use `start` and `end` modifiers, with comma-separated groups for different edge values. Unspecified padding, margin, and scroll edges reset to `0`; unspecified inset edges reset to `auto`. The `longhand` modifier emits the two native start/end declarations.
250
+
251
+ | Category | Block axis | Inline axis |
252
+ |----------|------------|-------------|
253
+ | Size | `blockSize` | `inlineSize` |
254
+ | Padding | `blockPadding` | `inlinePadding` |
255
+ | Margin | `blockMargin` | `inlineMargin` |
256
+ | Inset | `blockInset` | `inlineInset` |
257
+ | Scroll margin | `blockScrollMargin` | `inlineScrollMargin` |
258
+ | Scroll padding | `blockScrollPadding` | `inlineScrollPadding` |
259
+ | Border | `blockBorder` | `inlineBorder` |
260
+
261
+ `blockSize` and `inlineSize` use the same enhanced one/two/three-value and `min`/`max`/`fixed` syntax as `width`. Separate `minBlockSize`, `maxBlockSize`, `minInlineSize`, and `maxInlineSize` declarations override constraints produced by that shorthand.
262
+
263
+ `blockBorder` and `inlineBorder` use the same width/style/color parsing and defaults as `border`, including `1bw`, color tokens, and `true`. Logical padding, margin, scroll margin, and scroll padding default to `1x` for `true`; logical inset defaults to `0`.
264
+
265
+ Native logical CSS names—such as `paddingInlineStart`, `borderBlockColor`, and `borderStartStartRadius`—remain available as ordinary CSS properties. They support Tasty tokens and units through the normal value parser, but do not get category defaults or directional shorthand behavior.
266
+
267
+ The physical `scrollMargin` and `scrollPadding` shorthands support the same values and physical direction modifiers as `padding`. Their logical counterparts stay in separate axis handlers.
268
+
269
+ `writingMode`, `direction`, and `textOrientation` are standard CSS control properties. They use Tasty's normal value parser and need no special handler.
238
270
 
239
- **Priority:** `inset` < `insetBlock`/`insetInline` < `top`/`right`/`bottom`/`left`
271
+ Avoid setting physical and logical declarations that resolve to the same edge in one style block. Tasty keeps them separate and lets the native CSS cascade resolve the overlap, whose result also depends on writing mode and direction.
240
272
 
241
273
  ---
242
274