@tenphi/tasty 1.4.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +8 -7
  2. package/dist/compute-styles.js +6 -28
  3. package/dist/compute-styles.js.map +1 -1
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +2 -2
  8. package/dist/hooks/useCounterStyle.d.ts +3 -17
  9. package/dist/hooks/useCounterStyle.js +54 -35
  10. package/dist/hooks/useCounterStyle.js.map +1 -1
  11. package/dist/hooks/useFontFace.d.ts +3 -1
  12. package/dist/hooks/useFontFace.js +21 -24
  13. package/dist/hooks/useFontFace.js.map +1 -1
  14. package/dist/hooks/useGlobalStyles.d.ts +18 -2
  15. package/dist/hooks/useGlobalStyles.js +51 -40
  16. package/dist/hooks/useGlobalStyles.js.map +1 -1
  17. package/dist/hooks/useKeyframes.d.ts +4 -2
  18. package/dist/hooks/useKeyframes.js +41 -50
  19. package/dist/hooks/useKeyframes.js.map +1 -1
  20. package/dist/hooks/useProperty.d.ts +4 -2
  21. package/dist/hooks/useProperty.js +29 -41
  22. package/dist/hooks/useProperty.js.map +1 -1
  23. package/dist/hooks/useRawCSS.d.ts +13 -44
  24. package/dist/hooks/useRawCSS.js +90 -21
  25. package/dist/hooks/useRawCSS.js.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +2 -2
  28. package/dist/injector/index.d.ts +5 -10
  29. package/dist/injector/index.js +5 -12
  30. package/dist/injector/index.js.map +1 -1
  31. package/dist/injector/injector.d.ts +11 -13
  32. package/dist/injector/injector.js +50 -73
  33. package/dist/injector/injector.js.map +1 -1
  34. package/dist/injector/sheet-manager.js +2 -1
  35. package/dist/injector/sheet-manager.js.map +1 -1
  36. package/dist/injector/types.d.ts +21 -28
  37. package/dist/rsc-cache.js +81 -0
  38. package/dist/rsc-cache.js.map +1 -0
  39. package/dist/ssr/astro-client.d.ts +1 -0
  40. package/dist/ssr/astro-client.js +24 -0
  41. package/dist/ssr/astro-client.js.map +1 -0
  42. package/dist/ssr/astro-middleware.d.ts +15 -0
  43. package/dist/ssr/astro-middleware.js +19 -0
  44. package/dist/ssr/astro-middleware.js.map +1 -0
  45. package/dist/ssr/astro.d.ts +85 -8
  46. package/dist/ssr/astro.js +110 -20
  47. package/dist/ssr/astro.js.map +1 -1
  48. package/dist/ssr/async-storage.js +14 -4
  49. package/dist/ssr/async-storage.js.map +1 -1
  50. package/dist/ssr/collect-auto-properties.js +28 -9
  51. package/dist/ssr/collect-auto-properties.js.map +1 -1
  52. package/dist/ssr/index.d.ts +1 -1
  53. package/dist/ssr/ssr-collector-ref.js +4 -3
  54. package/dist/ssr/ssr-collector-ref.js.map +1 -1
  55. package/dist/tasty.d.ts +1 -1
  56. package/dist/utils/deps-equal.js +15 -0
  57. package/dist/utils/deps-equal.js.map +1 -0
  58. package/dist/utils/hash.js +14 -0
  59. package/dist/utils/hash.js.map +1 -0
  60. package/docs/adoption.md +1 -1
  61. package/docs/comparison.md +1 -1
  62. package/docs/configuration.md +1 -1
  63. package/docs/design-system.md +1 -1
  64. package/docs/dsl.md +21 -6
  65. package/docs/getting-started.md +1 -1
  66. package/docs/injector.md +25 -24
  67. package/docs/methodology.md +1 -1
  68. package/docs/runtime.md +12 -31
  69. package/docs/ssr.md +117 -36
  70. package/docs/tasty-static.md +1 -1
  71. package/package.json +8 -2
@@ -418,7 +418,7 @@ The key principle: `config.ts` imports tokens and recipes, calls `configure()`,
418
418
  - **[Methodology](methodology.md)** — The recommended patterns for structuring Tasty components
419
419
  - **[Getting Started](getting-started.md)** — Installation, first component, tooling setup
420
420
  - **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
421
- - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, hooks
421
+ - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, style functions
422
422
  - **[Configuration](configuration.md)** — Full `configure()` API: tokens, recipes, custom units, style handlers
423
423
  - **[Adoption Guide](adoption.md)** — Who should adopt Tasty, incremental phases, what changes for product engineers
424
424
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
package/docs/dsl.md CHANGED
@@ -25,7 +25,21 @@ fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
25
25
  | Pseudo-class | `:hover` | `:hover` |
26
26
  | Class selector | `.active` | `.active` |
27
27
  | Attribute selector | `[aria-expanded="true"]` | `[aria-expanded="true"]` |
28
- | Combined | `hovered & .active` | `[data-hovered].active` |
28
+ | Combined (AND) | `hovered & .active` | `[data-hovered].active` |
29
+ | Combined (OR) | `hovered \| focused` | `[data-hovered], [data-focused]` |
30
+ | Negated (NOT) | `!disabled` | `:not([data-disabled])` |
31
+ | Exclusive (XOR) | `hovered ^ focused` | `[data-hovered]:not([data-focused]), :not([data-hovered])[data-focused]` |
32
+
33
+ Operator precedence (highest to lowest): `!` (NOT) > `^` (XOR) > `|` (OR) > `&` (AND). Use parentheses to override: `hovered & (pressed ^ focused)`.
34
+
35
+ `^` (XOR) means "exactly one of" — `A ^ B` expands to `(A & !B) | (!A & B)`. This is useful for mutually exclusive states where exactly one should be active:
36
+
37
+ ```jsx
38
+ fill: {
39
+ '': '#surface',
40
+ 'hovered ^ focused': '#accent', // active when hovered OR focused, but not both
41
+ }
42
+ ```
29
43
 
30
44
  ### Sub-element
31
45
 
@@ -430,7 +444,7 @@ const FadeIn = tasty({
430
444
 
431
445
  ### `@parent(...)` — Parent Element States
432
446
 
433
- Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`) is supported inside `@parent()`.
447
+ Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
434
448
 
435
449
  ```jsx
436
450
  const Highlight = tasty({
@@ -508,12 +522,13 @@ const Card = tasty({
508
522
  | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
509
523
  | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
510
524
 
511
- Combine with other states using boolean logic:
525
+ Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
512
526
 
513
527
  ```jsx
514
- ':has(> Icon) & hovered' // structural + data attribute
515
- '@parent(hovered) & :has(> Icon)' // parent check + structural
528
+ ':has(> Icon) & hovered' // AND: structural + data attribute
529
+ '@parent(hovered) & :has(> Icon)' // AND: parent check + structural
516
530
  ':has(> Icon) | :has(> Button)' // OR: either sub-element present
531
+ ':has(> Icon) ^ :has(> Button)' // XOR: exactly one present
517
532
  ```
518
533
 
519
534
  > **Nesting limit:** The state key parser supports up to 2 levels of nested parentheses inside `:is()`, `:has()`, `:not()`, and `:where()` — e.g. `:has(Input:not(:disabled))` works, but 3+ levels like `:has(:is(:not(:hover)))` will not be tokenized correctly. This covers virtually all practical use cases.
@@ -666,7 +681,7 @@ For a complete reference of all enhanced style properties — syntax, values, mo
666
681
 
667
682
  ## Learn more
668
683
 
669
- - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, hooks
684
+ - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, style functions
670
685
  - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
671
686
  - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
672
687
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
@@ -200,7 +200,7 @@ Both share the same DSL, tokens, units, and state mappings.
200
200
  - **[Docs Hub](README.md)** — Pick the next guide by role, styling approach, or task
201
201
  - **[Methodology](methodology.md)** — The recommended patterns for structuring Tasty components: sub-elements, styleProps, tokens, extension
202
202
  - **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
203
- - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, hooks
203
+ - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, style functions
204
204
  - **[Building a Design System](design-system.md)** — Practical guide to building a DS layer with Tasty: tokens, recipes, primitives, compound components
205
205
  - **[Adoption Guide](adoption.md)** — Roll out Tasty inside an existing design system or platform team
206
206
  - **[Comparison](comparison.md)** — Evaluate Tasty against other styling systems
package/docs/injector.md CHANGED
@@ -115,11 +115,13 @@ dispose();
115
115
 
116
116
  ### `useRawCSS(css, options?)` or `useRawCSS(factory, deps, options?)`
117
117
 
118
- React hook for injecting raw CSS. Uses `useInsertionEffect` for proper timing and cleanup.
118
+ Inject raw CSS without parsing. Hook-free works in client components, SSR, and React Server Components.
119
119
 
120
120
  Supports two overloads:
121
- - **Static CSS**: `useRawCSS(cssString, options?)`
122
- - **Factory function**: `useRawCSS(() => cssString, deps, options?)` - re-evaluates when deps change (like `useMemo`)
121
+ - **Static CSS**: `useRawCSS(cssString, options?)` — content-based deduplication
122
+ - **Factory function**: `useRawCSS(() => cssString, deps, options?)` factory called on every invocation, dedup handled internally
123
+
124
+ Use the `id` option for update tracking — when the CSS changes for the same id, the previous injection is replaced:
123
125
 
124
126
  ```tsx
125
127
  import { useRawCSS } from '@tenphi/tasty';
@@ -132,7 +134,7 @@ function GlobalReset() {
132
134
  return null;
133
135
  }
134
136
 
135
- // Dynamic CSS with factory function (like useMemo)
137
+ // Dynamic CSS with factory function and update tracking
136
138
  function ThemeStyles({ theme }: { theme: 'dark' | 'light' }) {
137
139
  useRawCSS(() => `
138
140
  body {
@@ -140,7 +142,7 @@ function ThemeStyles({ theme }: { theme: 'dark' | 'light' }) {
140
142
  background: ${theme === 'dark' ? '#000' : '#fff'};
141
143
  color: ${theme === 'dark' ? '#fff' : '#000'};
142
144
  }
143
- `, [theme]);
145
+ `, [theme], { id: 'theme-body' });
144
146
 
145
147
  return null;
146
148
  }
@@ -210,11 +212,8 @@ configure({
210
212
  forceTextInjection: false, // Force textContent insertion (auto-detected for tests)
211
213
  nonce: 'csp-nonce', // CSP nonce for security
212
214
  gc: { // Garbage collection for unused styles
213
- auto: true, // Enable automatic background sweep
214
- baseMaxAge: 60000, // Base TTL (ms) for single-use styles
215
- cooldown: 30000, // Minimum time between GC runs
216
- autoInterval: 300000, // Background sweep interval (ms)
217
- cacheCapacity: 5000, // Hard cap on cached styles (optional)
215
+ touchInterval: 1000, // Touch events between GC cycles (default: 1000)
216
+ capacity: 1000, // Max unused styles to retain (default: 1000)
218
217
  },
219
218
  states: { // Global predefined states for advanced state mapping
220
219
  '@mobile': '@media(w < 768px)',
@@ -231,9 +230,8 @@ configure({
231
230
  - Most options have sensible defaults and auto-detection
232
231
  - `configure()` is optional - the injector works with defaults
233
232
  - **Configuration is locked after styles are generated** - calling `configure()` after first render will emit a warning and be ignored
234
- - `gc.baseMaxAge`: Base TTL for a style rendered only once. Popular styles get longer TTLs via logarithmic scaling (`baseMaxAge * log2(hitCount + 1)`).
235
- - `gc.auto`: When true, runs a periodic background sweep at `gc.autoInterval` intervals using `requestIdleCallback`.
236
- - `gc.cooldown`: Minimum time between GC runs to avoid thrashing.
233
+ - `gc.touchInterval`: Number of touch events between GC cycles. Each style render counts as a touch. When the counter reaches this value, GC is scheduled via `requestIdleCallback`.
234
+ - `gc.capacity`: Maximum number of unused styles (refCount = 0, not in DOM) to retain. When exceeded, the oldest are evicted first. Actively referenced styles don't count against this limit.
237
235
 
238
236
  ---
239
237
 
@@ -298,28 +296,31 @@ comp3.dispose(); // refCount: 1 → 0, eligible for bulk cleanup
298
296
  ### Garbage Collection
299
297
 
300
298
  ```typescript
301
- import { configure, gc, maybeGC } from '@tenphi/tasty';
299
+ import { configure, gc } from '@tenphi/tasty';
302
300
 
303
301
  // Keyframes: Disposed immediately when refCount = 0 (safer for global scope)
304
- // CSS rules: Tracked by popularity and cleaned up via gc()
302
+ // CSS rules: Tracked by touch count and cleaned up via gc()
305
303
 
306
304
  configure({
307
305
  gc: {
308
- auto: true, // Enable background sweep
309
- baseMaxAge: 60000, // 1-minute base TTL
310
- cooldown: 30000, // 30s between runs
306
+ touchInterval: 1000, // Schedule GC every 1000 touches
307
+ capacity: 1000, // Max unused styles to retain
311
308
  },
312
309
  });
313
310
 
314
311
  // Manual GC (synchronous, returns number of swept styles):
315
312
  gc();
316
313
 
317
- // Event-driven GC with cooldown (e.g. on route change):
318
- maybeGC();
314
+ // Force-remove ALL unused styles (e.g. on route change or test teardown):
315
+ gc({ force: true });
316
+
317
+ // GC is also triggered automatically by touch count during rendering.
318
+ // Every `touchInterval` touches, GC is scheduled via requestIdleCallback.
319
319
 
320
320
  // Benefits:
321
- // - Popularity-aware: frequently used styles survive longer
321
+ // - Activity-proportional: busy apps trigger GC more often
322
322
  // - DOM-safe: styles currently in the DOM are never evicted
323
+ // - Oldest-first: least recently used styles are evicted first
323
324
  // - Keyframes: Immediate cleanup prevents global namespace pollution
324
325
  // - Unused styles can be instantly reactivated (just increment refCount)
325
326
  ```
@@ -481,8 +482,8 @@ import { configure } from '@tenphi/tasty';
481
482
 
482
483
  configure({
483
484
  gc: {
484
- auto: true, // Enable background sweep for long-lived pages
485
- baseMaxAge: 60000, // Default base TTL (adjust based on app size)
485
+ touchInterval: 1000, // Schedule GC every 1000 style touches
486
+ capacity: 1000, // Max unused styles to retain
486
487
  },
487
488
  });
488
489
  ```
@@ -495,7 +496,7 @@ configure({
495
496
  // 1. Hash-based deduplication - same CSS = same className
496
497
  // 2. Reference counting - styles stay alive while in use (refCount > 0)
497
498
  // 3. Immediate keyframes cleanup - disposed instantly when refCount = 0
498
- // 4. Popularity-aware GC - unused CSS rules are scored by hitCount and age
499
+ // 4. Touch-count GC - unused CSS rules are evicted oldest-first when over capacity
499
500
  // 5. DOM safety guard - styles visible in the DOM are never evicted
500
501
 
501
502
  // Manual cleanup is rarely needed but available:
@@ -610,7 +610,7 @@ The `elements` prop gives you typed sub-components with automatic `data-element`
610
610
  - **[Getting Started](getting-started.md)** — Installation, first component, tooling setup
611
611
  - **[Building a Design System](design-system.md)** — Practical guide to building a DS layer with Tasty
612
612
  - **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
613
- - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, hooks
613
+ - **[Runtime API](runtime.md)** — `tasty()` factory, component props, variants, sub-elements, style functions
614
614
  - **[Configuration](configuration.md)** — Full `configure()` API: tokens, recipes, custom units, style handlers
615
615
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
616
616
  - **[Adoption Guide](adoption.md)** — Who should adopt Tasty, incremental phases, what changes for product engineers
package/docs/runtime.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime API
2
2
 
3
- The React-specific `tasty()` component factory, component props, and hooks. `tasty()` components are hook-free and compatible with React Server Components no `'use client'` directive needed. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
3
+ The React-specific `tasty()` component factory, component props, and style functions. All Tasty style functions — `tasty()` components, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()` — are hook-free and compatible with React Server Components. No `'use client'` directive needed. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
4
4
 
5
5
  ---
6
6
 
@@ -351,11 +351,13 @@ On the client, CSS is injected synchronously into the DOM (idempotent via the in
351
351
 
352
352
  ---
353
353
 
354
- ## Hooks
354
+ ## Style Functions
355
+
356
+ All style functions below are plain functions (not React hooks) and can be used in any environment: client components, SSR with a `ServerStyleCollector`, and React Server Components. They retain their `use` prefix for backward compatibility, but do not use any React hooks internally.
355
357
 
356
358
  ### useStyles
357
359
 
358
- Generate a className from a style object. Thin wrapper around `computeStyles()` that adds React context-based SSR collector discovery for backward compatibility:
360
+ Generate a className from a style object. Thin wrapper around `computeStyles()`:
359
361
 
360
362
  ```tsx
361
363
  import { useStyles } from '@tenphi/tasty';
@@ -373,7 +375,7 @@ function MyComponent() {
373
375
 
374
376
  ### useGlobalStyles
375
377
 
376
- Inject global styles for a CSS selector:
378
+ Inject global styles for a CSS selector. Accepts an optional third argument with an `id` for update tracking — when the styles change, the previous injection is disposed and the new one is injected:
377
379
 
378
380
  ```tsx
379
381
  import { useGlobalStyles } from '@tenphi/tasty';
@@ -391,7 +393,7 @@ function ThemeStyles() {
391
393
 
392
394
  ### useRawCSS
393
395
 
394
- Inject raw CSS strings:
396
+ Inject raw CSS strings. Accepts an optional `id` in the options for update tracking — when the CSS changes for the same id, the previous injection is replaced:
395
397
 
396
398
  ```tsx
397
399
  import { useRawCSS } from '@tenphi/tasty';
@@ -425,7 +427,7 @@ function Spinner() {
425
427
  }
426
428
  ```
427
429
 
428
- `useKeyframes()` also supports a factory function with dependencies:
430
+ `useKeyframes()` also supports a factory function. The deps array is accepted for backward compatibility but the factory is called on every invocation — deduplication is handled internally by content hash:
429
431
 
430
432
  ```tsx
431
433
  function Pulse({ scale }: { scale: number }) {
@@ -521,43 +523,22 @@ function EmojiList() {
521
523
  }
522
524
  ```
523
525
 
524
- Factory form with dependencies:
525
-
526
- ```tsx
527
- function DynamicList({ marker }: { marker: string }) {
528
- const styleName = useCounterStyle(
529
- () => ({
530
- system: 'cyclic',
531
- symbols: `"${marker}"`,
532
- suffix: '" "',
533
- }),
534
- [marker],
535
- );
536
-
537
- return <ol style={{ listStyleType: styleName }}>...</ol>;
538
- }
539
- ```
540
-
541
- Signatures:
526
+ Signature:
542
527
 
543
528
  ```ts
544
529
  function useCounterStyle(
545
530
  descriptors: CounterStyleDescriptors,
546
531
  options?: { name?: string; root?: Document | ShadowRoot },
547
532
  ): string;
548
-
549
- function useCounterStyle(
550
- factory: () => CounterStyleDescriptors,
551
- deps: readonly unknown[],
552
- options?: { name?: string; root?: Document | ShadowRoot },
553
- ): string;
554
533
  ```
555
534
 
556
535
  ### Troubleshooting
557
536
 
558
537
  - Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
559
- - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. `computeStyles()` discovers the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
538
+ - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. All style functions discover the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
560
539
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
540
+ - For dynamic styles that change over the component lifecycle, use the `id` option in `useGlobalStyles()` and `useRawCSS()` to enable update tracking.
541
+ - RSC inline mode: CSS accumulated by standalone style functions (`useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
561
542
 
562
543
  ---
563
544
 
package/docs/ssr.md CHANGED
@@ -83,14 +83,20 @@ That's it. All `tasty()` components inside the tree automatically get SSR suppor
83
83
  ### How it works
84
84
 
85
85
  - `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.
86
- - During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level global getter. All style computationwhether from `tasty()` components, `computeStyles()`, `useStyles()`, or other hooks like `useGlobalStyles()` — discovers the collector through this single global getter. No React context is involved.
86
+ - During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level global getter. All style functions — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()`discover the collector through this single global getter. No React context is involved.
87
87
  - `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.
88
88
  - A companion `<script>` tag transfers the `cacheKey → className` mapping to the client.
89
89
  - When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `computeStyles()` hits the cache and skips the entire pipeline.
90
90
 
91
- ### Using tasty() in Server Components
91
+ ### Using Tasty in Server Components
92
92
 
93
- `tasty()` components are hook-free and do not require `'use client'`. They can be used directly in React Server Components. Dynamic `styleProps` like `<Grid flow="column">` work normally in server components. During SSR, `computeStyles()` discovers the collector via the same global getter registered by `TastyRegistry` — no React context or client boundary needed for this path.
93
+ All Tasty style functions are hook-free and do not require `'use client'`. They can be used directly in React Server Components:
94
+
95
+ - `tasty()` components — dynamic `styleProps` like `<Grid flow="column">` work normally
96
+ - `useStyles()`, `useGlobalStyles()`, `useRawCSS()` — inject styles by class or selector
97
+ - `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()` — inject ancillary CSS rules
98
+
99
+ During SSR, all functions discover the collector via the same global getter registered by `TastyRegistry` — no React context or client boundary needed. In RSC mode without a collector (e.g., Astro zero-setup), CSS is accumulated in a per-request cache and flushed into an inline `<style>` tag by the next `tasty()` component in the tree. Ensure at least one `tasty()` component is present in every RSC render tree — standalone style functions alone cannot emit their CSS without a `tasty()` component to trigger the flush.
94
100
 
95
101
  ### Options
96
102
 
@@ -116,18 +122,17 @@ The nonce is automatically applied to all `<style>` and `<script>` tags injected
116
122
 
117
123
  ## Astro
118
124
 
119
- ### 1. Add the middleware
125
+ Tasty offers three levels of Astro integration. Choose the one that matches your needs:
120
126
 
121
- Create or update your Astro middleware:
127
+ | Setup | Config needed | Deduplication | Hooks work | Client JS |
128
+ |---|---|---|---|---|
129
+ | Zero setup | None | Per render tree | No | None |
130
+ | `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
131
+ | `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
122
132
 
123
- ```ts
124
- // src/middleware.ts
125
- import { tastyMiddleware } from '@tenphi/tasty/ssr/astro';
126
-
127
- export const onRequest = tastyMiddleware();
128
- ```
133
+ ### Zero setup (static pages)
129
134
 
130
- ### 2. Use tasty() components as normal
135
+ `tasty()` components work in Astro with **no configuration**. Each component emits its own inline `<style>` tag during server rendering via the RSC inline path. Just import and use:
131
136
 
132
137
  ```tsx
133
138
  // src/components/Card.tsx
@@ -153,51 +158,118 @@ import Card from '../components/Card.tsx';
153
158
 
154
159
  <html>
155
160
  <body>
156
- <Card>Static card styles collected by middleware</Card>
157
- <Card client:load>Island card — styles hydrated on client</Card>
161
+ <Card>Styled with zero setup</Card>
158
162
  </body>
159
163
  </html>
160
164
  ```
161
165
 
162
- ### How it works
166
+ **Trade-offs**: Styles are deduplicated within each React render tree, but Astro renders separate component trees independently, so shared CSS (tokens, `@property` rules) may appear more than once. All style functions (`useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`) work in zero-setup mode — their CSS is accumulated in the RSC cache and flushed by the next `tasty()` component in the tree.
163
167
 
164
- Astro's `@astrojs/react` renderer calls `renderToString()` for each React component without wrapping the tree in a provider. The middleware uses `AsyncLocalStorage` to make the collector available to all `computeStyles()` calls within the request.
168
+ Best for quick prototyping, small static sites, or trying Tasty out in Astro.
165
169
 
166
- - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>`. No JavaScript is shipped for these components.
167
- - **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, importing `@tenphi/tasty/ssr/astro` auto-hydrates the cache from `<script data-tasty-cache>`. The island's `computeStyles()` calls hit the cache during hydration.
170
+ ### Astro Integration (recommended)
168
171
 
169
- ### Client-side hydration for islands
172
+ For production use, add `tastyIntegration()` to your Astro config. This registers middleware automatically and, by default, injects client-side hydration for islands.
170
173
 
171
- The `@tenphi/tasty/ssr/astro` module auto-hydrates when imported on the client. To ensure the cache is warm before any island renders, import it in a shared entry point or in each island component:
174
+ #### With islands (default)
172
175
 
173
- ```tsx
174
- // src/components/MyIsland.tsx
175
- import '@tenphi/tasty/ssr/astro'; // auto-hydrates cache on import
176
- import { tasty } from '@tenphi/tasty';
176
+ ```ts
177
+ // astro.config.mjs
178
+ import { defineConfig } from 'astro/config';
179
+ import react from '@astrojs/react';
180
+ import { tastyIntegration } from '@tenphi/tasty/ssr/astro';
177
181
 
178
- const MyIsland = tasty({
179
- styles: { padding: '2x', fill: '#blue' },
182
+ export default defineConfig({
183
+ integrations: [react(), tastyIntegration()],
180
184
  });
185
+ ```
186
+
187
+ This gives you:
188
+
189
+ - A `ServerStyleCollector` per request via `AsyncLocalStorage`, deduplicating CSS across all React trees on the page
190
+ - A single consolidated `<style data-tasty-ssr>` injected into `</head>`
191
+ - A `<script data-tasty-cache>` tag with the `cacheKey -> className` map for client hydration
192
+ - Auto-injected client hydration script (via `injectScript('before-hydration')`) so islands skip the style pipeline during hydration -- no need to import anything manually in each island component
181
193
 
182
- export default MyIsland;
194
+ All style functions (`useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`) work on the server.
195
+
196
+ ```astro
197
+ ---
198
+ // src/pages/index.astro
199
+ import Card from '../components/Card.tsx';
200
+ import Interactive from '../components/Interactive.tsx';
201
+ ---
202
+
203
+ <html>
204
+ <body>
205
+ <Card>Static -- styles in <style data-tasty-ssr></Card>
206
+ <Interactive client:load>Island -- cache hydrated automatically</Interactive>
207
+ </body>
208
+ </html>
183
209
  ```
184
210
 
185
- ### Options
211
+ #### Static only (no client JS)
212
+
213
+ If your site has no `client:*` islands, skip the hydration script and cache transfer:
214
+
215
+ ```ts
216
+ // astro.config.mjs
217
+ import { defineConfig } from 'astro/config';
218
+ import react from '@astrojs/react';
219
+ import { tastyIntegration } from '@tenphi/tasty/ssr/astro';
220
+
221
+ export default defineConfig({
222
+ integrations: [react(), tastyIntegration({ islands: false })],
223
+ });
224
+ ```
225
+
226
+ This gives the same middleware deduplication and hook support, but ships zero client-side JavaScript. No `<script data-tasty-cache>` is emitted.
227
+
228
+ ### Manual middleware (advanced)
229
+
230
+ If you need to compose Tasty's middleware with other middleware (e.g., via `sequence()`), use `tastyMiddleware()` directly:
231
+
232
+ ```ts
233
+ // src/middleware.ts
234
+ import { sequence } from 'astro:middleware';
235
+ import { tastyMiddleware } from '@tenphi/tasty/ssr/astro';
236
+
237
+ export const onRequest = sequence(
238
+ tastyMiddleware(),
239
+ myOtherMiddleware,
240
+ );
241
+ ```
242
+
243
+ For island hydration with manual middleware, import the client module in a shared entry point or in each island:
244
+
245
+ ```tsx
246
+ import '@tenphi/tasty/ssr/astro-client';
247
+ ```
248
+
249
+ #### Options
186
250
 
187
251
  ```ts
188
- // Skip cache state transfer
252
+ // Skip cache state transfer (static-only, no islands)
189
253
  export const onRequest = tastyMiddleware({ transferCache: false });
190
254
  ```
191
255
 
256
+ ### How it works
257
+
258
+ 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.
259
+
260
+ - **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>` as a single `<style>` tag. No JavaScript is shipped.
261
+ - **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 cache state from `<script data-tasty-cache>` and pre-populates the injector. The island's `computeStyles()` calls hit the cache during hydration.
262
+ - The middleware uses streaming-compatible `TransformStream` processing to inject CSS into the response without buffering the entire HTML.
263
+
192
264
  ### CSP nonce
193
265
 
194
- Same as Next.js -- call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected tags.
266
+ Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags.
195
267
 
196
268
  ---
197
269
 
198
270
  ## Generic Framework Integration
199
271
 
200
- Any React-based framework can integrate using `runWithCollector`, which binds a `ServerStyleCollector` to the current async context via `AsyncLocalStorage`. All `computeStyles()` and hook calls within the render automatically discover the collector.
272
+ 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.
201
273
 
202
274
  ```tsx
203
275
  import {
@@ -279,7 +351,8 @@ const stream = await runWithCollector(collector, () =>
279
351
  |---|---|
280
352
  | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `runWithCollector`, `hydrateTastyCache` |
281
353
  | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
282
- | `@tenphi/tasty/ssr/astro` | Astro: `tastyMiddleware`, auto-hydration on import |
354
+ | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
355
+ | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
283
356
 
284
357
  ### `ServerStyleCollector`
285
358
 
@@ -306,9 +379,17 @@ Next.js App Router component. Props:
306
379
  | `children` | `ReactNode` | required | Application tree |
307
380
  | `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
308
381
 
382
+ ### `tastyIntegration(options?)`
383
+
384
+ Astro integration factory. Registers middleware and optionally injects client hydration.
385
+
386
+ | Option | Type | Default | Description |
387
+ |---|---|---|---|
388
+ | `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
389
+
309
390
  ### `tastyMiddleware(options?)`
310
391
 
311
- Astro middleware factory. Options:
392
+ Astro middleware factory. Use for manual middleware composition.
312
393
 
313
394
  | Option | Type | Default | Description |
314
395
  |---|---|---|---|
@@ -320,7 +401,7 @@ Pre-populate the client injector cache. When called without arguments, reads fro
320
401
 
321
402
  ### `runWithCollector(collector, fn)`
322
403
 
323
- Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All `computeStyles()` and `useStyles()` calls within `fn` (and async continuations) will find this collector.
404
+ Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All style function calls within `fn` (and async continuations) — including `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()` will find this collector.
324
405
 
325
406
  ---
326
407
 
@@ -328,11 +409,11 @@ Run a function with a `ServerStyleCollector` bound to the current async context
328
409
 
329
410
  ### Styles flash on page load (FOUC)
330
411
 
331
- The `TastyRegistry` or `tastyMiddleware` is missing. Ensure your layout wraps the app with `TastyRegistry` (Next.js) or the middleware is registered (Astro).
412
+ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps the app with `TastyRegistry` (Next.js) or that `tastyIntegration()` is in your Astro config (or `tastyMiddleware()` is registered manually).
332
413
 
333
414
  ### Hydration mismatch warnings
334
415
 
335
- Class names are deterministic for the same render order. If you see mismatches, ensure `hydrateTastyCache()` runs before React hydration. For Next.js, this is automatic. For Astro, import `@tenphi/tasty/ssr/astro` in your island components. For custom setups, call `hydrateTastyCache()` before `hydrateRoot()`.
416
+ Class names are deterministic for the same render order. If you see mismatches, ensure `hydrateTastyCache()` 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 `hydrateTastyCache()` before `hydrateRoot()`.
336
417
 
337
418
  ### Styles duplicated after hydration
338
419
 
@@ -516,5 +516,5 @@ const card = tastyStatic({
516
516
 
517
517
  - [Docs Hub](README.md) — Choose the right guide by task or rendering mode
518
518
  - [Style DSL](dsl.md) — State maps, tokens, units, extending semantics (shared by runtime and static)
519
- - [Runtime API](runtime.md) — Runtime styling: `tasty()` factory, component props, variants, sub-elements, hooks
519
+ - [Runtime API](runtime.md) — Runtime styling: `tasty()` factory, component props, variants, sub-elements, style functions
520
520
  - [Configuration](configuration.md) — Global configuration: tokens, recipes, custom units, and style handlers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/tasty",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
4
4
  "description": "A design-system-integrated styling system and DSL for concise, state-aware UI styling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,6 +57,11 @@
57
57
  "import": "./dist/ssr/astro.js",
58
58
  "default": "./dist/ssr/astro.js"
59
59
  },
60
+ "./ssr/astro-client": {
61
+ "types": "./dist/ssr/astro-client.d.ts",
62
+ "import": "./dist/ssr/astro-client.js",
63
+ "default": "./dist/ssr/astro-client.js"
64
+ },
60
65
  "./tasty.config": "./tasty.config.ts"
61
66
  },
62
67
  "files": [
@@ -67,7 +72,8 @@
67
72
  "sideEffects": [
68
73
  "./dist/ssr/index.js",
69
74
  "./dist/ssr/next.js",
70
- "./dist/ssr/astro.js"
75
+ "./dist/ssr/astro.js",
76
+ "./dist/ssr/astro-client.js"
71
77
  ],
72
78
  "engines": {
73
79
  "node": ">=20"