@tenphi/tasty 3.11.0 → 3.11.2

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 (48) hide show
  1. package/README.md +13 -9
  2. package/dist/chunks/{build-config-5d8WlQbq.js → build-config-DlxpZKk_.js} +4 -4
  3. package/dist/chunks/{build-config-5d8WlQbq.js.map → build-config-DlxpZKk_.js.map} +1 -1
  4. package/dist/chunks/{collector-CjCak4iE.js → collector-B4-yqFEs.js} +4 -4
  5. package/dist/chunks/{collector-CjCak4iE.js.map → collector-B4-yqFEs.js.map} +1 -1
  6. package/dist/chunks/{config-engine-uOFYZidB.js → config-engine-DJsb5WYu.js} +3 -3
  7. package/dist/chunks/{config-engine-uOFYZidB.js.map → config-engine-DJsb5WYu.js.map} +1 -1
  8. package/dist/chunks/{css-definitions-ubrHP97m.js → css-definitions-DW1TGGiy.js} +2 -2
  9. package/dist/chunks/{css-definitions-ubrHP97m.js.map → css-definitions-DW1TGGiy.js.map} +1 -1
  10. package/dist/chunks/{debug-CJ31U_WE.js → debug-DMMwe1rY.js} +4 -4
  11. package/dist/chunks/{debug-CJ31U_WE.js.map → debug-DMMwe1rY.js.map} +1 -1
  12. package/dist/chunks/{react-runtime-CyyCI02N.js → react-runtime-DhPtFlZs.js} +5 -5
  13. package/dist/chunks/react-runtime-DhPtFlZs.js.map +1 -0
  14. package/dist/chunks/{runtime-engine-BQmOu5-W.js → runtime-engine-BlrVXv8l.js} +5 -5
  15. package/dist/chunks/{runtime-engine-BQmOu5-W.js.map → runtime-engine-BlrVXv8l.js.map} +1 -1
  16. package/dist/chunks/{runtime-state-DEsyUMU-.js → runtime-state-BLyMRYFF.js} +2 -2
  17. package/dist/chunks/{runtime-state-DEsyUMU-.js.map → runtime-state-BLyMRYFF.js.map} +1 -1
  18. package/dist/chunks/{style-engine-BX2LshDZ.js → style-engine-of5Xbc27.js} +139 -34
  19. package/dist/chunks/style-engine-of5Xbc27.js.map +1 -0
  20. package/dist/chunks/{zero-engine-Dw0P4yGM.js → zero-engine-D2jPSpWA.js} +5 -5
  21. package/dist/chunks/{zero-engine-Dw0P4yGM.js.map → zero-engine-D2jPSpWA.js.map} +1 -1
  22. package/dist/core/index.d.ts +1 -1
  23. package/dist/core/index.js +6 -6
  24. package/dist/{index-xtey-KDu.d.ts → index--GdqCPlp.d.ts} +16 -4
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +7 -7
  27. package/dist/ssr/astro.js +2 -2
  28. package/dist/ssr/index.js +2 -2
  29. package/dist/ssr/next-config.js +3 -3
  30. package/dist/ssr/next.js +3 -3
  31. package/dist/zero/babel.js +4 -4
  32. package/dist/zero/index.js +3 -3
  33. package/docs/ai-agents.md +5 -1
  34. package/docs/configuration.md +2 -1
  35. package/docs/design-system.md +1 -1
  36. package/docs/dsl.md +42 -3
  37. package/docs/injector.md +3 -1
  38. package/docs/methodology.md +7 -7
  39. package/docs/migration-v3.md +1 -1
  40. package/docs/pipeline.md +1 -1
  41. package/docs/plugins.md +1 -1
  42. package/docs/react-api.md +51 -8
  43. package/docs/ssr.md +29 -23
  44. package/docs/styles.md +6 -0
  45. package/docs/tasty-static.md +33 -16
  46. package/package.json +5 -5
  47. package/dist/chunks/react-runtime-CyyCI02N.js.map +0 -1
  48. package/dist/chunks/style-engine-BX2LshDZ.js.map +0 -1
package/docs/injector.md CHANGED
@@ -269,6 +269,8 @@ Dispose, ref-counted cleanup and GC therefore behave identically in every mode.
269
269
 
270
270
  `gc({ force: true })` and `cleanup()` take bands 4 and 5 together, ignoring capacity. Band 3 is spared even then: an explicit cleanup is still no reason to take rules from a render that has not committed yet.
271
271
 
272
+ `tasty()` components cache the class names they computed, so a re-render with the same styles skips the pipeline. Whenever `gc()`, `cleanup()` or `destroy()` deletes rules, those caches are dropped too, so a component whose CSS was collected injects it again on its next render.
273
+
272
274
  > **Why band 3 exists.** Rendering is not commit-aware: a render can resolve a class and commit it a little later, and in between nothing on the page carries it. From outside React that is indistinguishable from a class that is finished, so collection does not try to tell them apart — it leaves alone anything only just noticed to be cold. A render would have to stay pending for the whole window to lose its rules, and it gets them back on its next render.
273
275
  >
274
276
  > The clock starts when a sweep *notices*, not when the element actually left — nothing observes that moment, so starting it at the sighting is what gives every class the same full window however long ago it went.
@@ -353,7 +355,7 @@ const button2 = inject([rule], { cacheKey: 'button' });
353
355
  console.log(button1.className === button2.className); // true
354
356
  ```
355
357
 
356
- Without `cacheKey` the class name still matches (it hashes the rules), but the rule is inserted again and the pin count restarts at 1.
358
+ Without `cacheKey` the class name is a hash of the rules, so injecting the same rules again reuses the class the same way: the rules are written once, and each call adds a pin that its own `dispose()` releases.
357
359
 
358
360
  ### Pinning
359
361
 
@@ -242,7 +242,7 @@ const Button = tasty({
242
242
  <Button variant="danger" size="sm" isLoading />;
243
243
  ```
244
244
 
245
- `variant` is typed from the variant keys, the `default` variant applies when no `variant` prop is passed, and only variants that render emit CSS. See [React API — Variants](react-api.md#variants).
245
+ `variant` is typed from the variant keys, the `default` variant applies when no `variant` prop is passed (set the `variant` option, as in `variant: 'danger'`, to fall back to another one), and only variants that render emit CSS. See [React API — Variants](react-api.md#variants).
246
246
 
247
247
  ---
248
248
 
@@ -359,9 +359,9 @@ const LargeCard = tasty(Card, {
359
359
 
360
360
  Why? Styled wrappers are:
361
361
 
362
- - **Stable** — the style object is defined once at module level, not recreated on every render
362
+ - **Stable** — the style object is defined once at module level, so Tasty merges and computes it once instead of on every render
363
363
  - **Composable** — another engineer can extend `LargeCard` further
364
- - **Inspectable** — give it a `qa` (`tasty(Card, { qa: 'LargeCard', styles })`) and it appears as `TastyWrappedComponent(LargeCard)` in React DevTools and as `data-qa="LargeCard"` in the DOM
364
+ - **Inspectable** — give it a `qa` (`tasty(Card, { qa: 'LargeCard', styles })`) and it appears as `data-qa="LargeCard"` in the DOM; React DevTools shows it as `TastyWrappedComponent(…)` around the base's name
365
365
  - **Lint-friendly** — the ESLint plugin's `no-styles-prop` rule flags direct usage
366
366
 
367
367
  The `styles` prop exists as an escape hatch — for prototyping, one-off overrides during development, or cases where wrapping is impractical. It should not be the default way product engineers customize components.
@@ -382,7 +382,7 @@ See [Best practices](#best-practices) below for the full list of do's and don'ts
382
382
 
383
383
  ## Wrapping and extension
384
384
 
385
- `tasty(Base, { styles })` is the primary extension mechanism. It creates a new component whose styles are merged with the base component's styles.
385
+ `tasty(Base, { styles })` is the primary extension mechanism. It creates a new component whose styles are merged with the base component's styles. The extension can also declare its own `styleProps`, `modProps`, `tokenProps`, `variants`, and `elements`, and it keeps the base's sub-elements — see [React API — Extend an existing component](react-api.md#extend-an-existing-component).
386
386
 
387
387
  ```tsx
388
388
  import { Button } from 'my-ds';
@@ -478,10 +478,10 @@ This is preferred over `<Card styles={{ border: '1bw solid #danger' }}>` because
478
478
 
479
479
  1. The styles live in one module-level definition instead of being rebuilt inside render
480
480
  2. `DangerCard` can be extended further by others
481
- 3. It is searchable in code, and named in DevTools once you give it a `qa`
481
+ 3. It is searchable in code, and carries `data-qa` in the DOM once you give it a `qa`
482
482
  4. The ESLint `no-styles-prop` rule encourages this pattern
483
483
 
484
- A wrapper does not re-expose the base's sub-element components: `DangerCard.Title` is `undefined` and not part of its type. Keep using the base's parts inside it — `<DangerCard><Card.Title>…</Card.Title></DangerCard>`. The wrapper's `Title` styles still apply, because they target `[data-element="Title"]`.
484
+ A wrapper keeps the base's sub-element components, typed: `DangerCard.Title` is `Card.Title`, so `<DangerCard><DangerCard.Title>…</DangerCard.Title></DangerCard>` works. The wrapper's `Title` styles apply to it, because they target `[data-element="Title"]`.
485
485
 
486
486
  ---
487
487
 
@@ -618,7 +618,7 @@ const Card = tasty({
618
618
  });
619
619
  ```
620
620
 
621
- Modifiers are compiled into exclusive selectors once. Changing `styles` at runtime forces Tasty to regenerate and re-inject CSS. `tasty/no-styles-prop` and `tasty/no-runtime-styles-mutation` flag these patterns.
621
+ Modifiers are compiled into exclusive selectors once. A `styles` object that stays the same between renders (a module-level constant or a memoized object) is merged and computed once, but a new object on every render is merged and computed on every render, and each new value generates and injects new CSS. `tasty/no-styles-prop` and `tasty/no-runtime-styles-mutation` flag these patterns.
622
622
 
623
623
  ### Overusing style prop
624
624
 
@@ -204,7 +204,7 @@ The build-time extraction Next.js wrapper already lived under the `zero` namespa
204
204
 
205
205
  **The `theme` prop is now implemented.** It maps to a `data-theme` attribute on the rendered element; previously it fell through and was spread onto the DOM raw. Augment `TastyThemeNames` for autocomplete.
206
206
 
207
- **`tasty(Component, options)` no longer leaks factory-only options.** `variants`, `elements`, `styleProps`, `modProps`, and `tokenProps` are stripped instead of being passed to the wrapped component as runtime props, matching the element factory.
207
+ **`tasty(Component, options)` no longer leaks factory-only options.** `variants`, `elements`, `styleProps`, `modProps`, and `tokenProps` are stripped instead of being passed to the wrapped component as runtime props, matching the element factory. Since 3.11 the extension form applies them as the element factory does; see [Extend an existing component](react-api.md#extend-an-existing-component).
208
208
 
209
209
  **Replacing a built-in style handler now warns.** Built-in handlers are shared across style names, so `configure({ handlers: { fill } })` also takes over `image` and the whole `background-*` family, and `configure({ handlers: { display } })` takes down `flow`, `gap`, `hide`, `overflow`, `whiteSpace`, and `textOverflow`. This was always true; what's new is that a development-mode warning now names exactly what was displaced. If you see it, either declare the whole group and delegate the rest to `styleHandlers.*`, or pick a narrower name. See [Plugins](plugins.md#custom-style-handlers).
210
210
 
package/docs/pipeline.md CHANGED
@@ -522,7 +522,7 @@ Converts condition trees into actual CSS selectors and at-rules.
522
522
 
523
523
  5. **`:where()` specificity equalization**: Every stateful part of the selector is wrapped in `:where(...)` so it contributes **zero specificity**:
524
524
  - the flat root modifiers + pseudos and the root element's `:is()`/`:not()` groups are combined into one `:where(...)` (`buildSelectorFromVariant`);
525
- - the sub-element (`@own`) groups are wrapped in a **separate** `:where(...)` so the sub-element's structural `[data-element="X"]` specificity is preserved while its states are zeroed;
525
+ - the sub-element (`@own`) groups are wrapped in a **separate** `:where(...)` so the sub-element's structural `[data-element="X"]` specificity is preserved while its states are zeroed. When the suffix ends in a pseudo-element, these are the pseudo-element's own states and stay after it (`a::before:where(:hover)`). States a pseudo-element can't have (attributes, modifiers, classes, tree-structural pseudo-classes) are dropped with a `PSEUDO_ELEMENT_STATE_IGNORED` warning before Stage 0, using the CSS language alone, never per-browser support (`pseudo-element-states.ts`). A direct selector that ends in a pseudo-element (`.card::before`) is rendered separately: its root states are checked the same way and go after the pseudo-element, its `@parent` segments before it, at the `pseudoElementAt` each `CSSRule` records;
526
526
  - `@parent` ancestors (`parentGroupsToCSS`) become `:where(... *)` / `:where(:not(... *))`;
527
527
  - the `@root` context prefix (`rootStateToPrefix`) becomes `:where(:root...)` (zeroing the `:root` pseudo too). When a direct selector can only match the root itself (`:root`, `html`, `:where(:root)`), the state segment joins it instead — `:root:where([data-schema="dark"])` — since an ancestor prefix would ask for a `:root` inside the root.
528
528
 
package/docs/plugins.md CHANGED
@@ -69,7 +69,7 @@ Two distinctions do most of the work:
69
69
  A handler receives a map of the style properties it declared and returns CSS declarations. See [Configuration → Custom Style Handlers](configuration.md#custom-style-handlers) for the definition forms, return shape, the shared-handler groups you must be careful of, and chunk membership. The essentials:
70
70
 
71
71
  - Values arrive **state-resolved but unparsed** — the raw authored DSL string (`'2x'`, `'#purple.5'`, `true`, `4`). Call the exported `parseStyle()` / `parseColor()` yourself.
72
- - Return a `CSSMap` with **kebab-case** keys, an array of them, or nothing. `$` is a selector suffix, not a property.
72
+ - Return a `CSSMap` with **kebab-case** keys, an array of them, or nothing. `$` is a selector suffix, not a property. A property whose value is an array gets one declaration per entry, for fallback chains.
73
73
  - Handlers must be pure and synchronous. They are called once per state combination and the results are cached.
74
74
  - Use `defineHandler(deps, fn)` for multi-dependency handlers so the dependency types are inferred.
75
75
 
package/docs/react-api.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use `tasty()` to turn state-aware style definitions into normal React components, extend existing Tasty components, and expose typed styling APIs to consumers.
4
4
 
5
- Every Tasty style function except `useStyles()` — `tasty()` components, `computeStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — is hook-free and works in React Server Components with no `'use client'` directive. `useStyles()` reads the `TastyRegistry` collector from React context, so it is a real hook; in Server Components and outside React, call `computeStyles()` instead. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
5
+ Every Tasty style function — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — works in React Server Components with no `'use client'` directive. All of them except `useStyles()` are hook-free. `useStyles()` reads the `TastyRegistry` collector from React context on the client, so it is a real hook there; outside React, call `computeStyles()` instead. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
6
6
 
7
7
  > **Note:** This file was previously named `runtime.md`. All functionality documented here works in both server and client contexts — "runtime" referred to style computation during React rendering, not to client-side JavaScript.
8
8
 
@@ -44,7 +44,29 @@ const PrimaryButton = tasty(Button, {
44
44
 
45
45
  Style maps merge intelligently — see [Style DSL — Extending vs. Replacing State Maps](dsl.md#extending-vs-replacing-state-maps) for extend mode, replace mode, `@inherit`, `null`, and `false` tombstones.
46
46
 
47
- The extension form forwards props; it does not re-run the factory. `styleProps`, `modProps`, `tokenProps`, `variants`, and `elements` passed to `tasty(Component, …)` are ignored even though they type-check, and the base's sub-components are not copied (`tasty(Card, …).Title` is `undefined`). Declare them on the base component, or create the component with `tasty({ … })` directly.
47
+ The extension form takes the same component options as `tasty({ … })`:
48
+
49
+ ```jsx
50
+ const ActionCard = tasty(Card, {
51
+ styles: { fill: { '': '#surface', isRaised: '#surface-raised' } },
52
+ styleProps: ['gap'] as const, // gap="2x" becomes a style
53
+ modProps: ['isRaised'] as const, // isRaised becomes a mod (data-is-raised)
54
+ tokenProps: ['accentColor'] as const, // accentColor sets the #accent token
55
+ variants: {
56
+ plain: { border: 0 },
57
+ framed: { border: '2bw #accent' },
58
+ },
59
+ variant: 'framed', // applied when no variant prop is passed
60
+ elements: { Footer: 'footer' },
61
+ });
62
+
63
+ <ActionCard gap="2x" isRaised accentColor="#purple">
64
+ <ActionCard.Title>Kept from Card</ActionCard.Title>
65
+ <ActionCard.Footer>Added here</ActionCard.Footer>
66
+ </ActionCard>;
67
+ ```
68
+
69
+ The extension turns its own `styleProps`, `modProps`, and `tokenProps` into the `styles`, `mods`, and `tokens` it passes to the base, which renders them as usual: mods as `data-*` attributes, tokens as inline custom properties. Its `variants` merge over its `styles`, and the `variant` prop is still forwarded, so it selects the base's own variants too and accepts either set's names. Both sets share that one prop, so a name only one of them defines leaves the other at its fallback: if `Card` had an `accent` variant, `variant="accent"` would apply it together with `framed`, and `variant="plain"` would leave `Card` on its default. For two independent choices, give one of them its own prop, such as a `modProps` value list. `elements` adds sub-components, and the base's are kept: `ActionCard.Title` is `Card.Title`. The returned component is typed with all of these props and sub-components.
48
70
 
49
71
  `tasty(Component, ...)` always wraps `Component` and forwards every prop to it, so `Component` must be Tasty-aware (i.e. it accepts `styles`, `mods`, `qa`, etc. and renders them through its own Tasty pipeline). To apply styles to a third-party component or a string DOM tag via `className`, use the options-only form with `as`:
50
72
 
@@ -303,6 +325,22 @@ const Button = tasty({
303
325
 
304
326
  With `variant` omitted, or set to a key `variants` does not define, the `default` variant applies; without a `default` key only the base `styles` apply. `variant` is typed from the variant keys.
305
327
 
328
+ To fall back to another variant instead, name it in the `variant` option:
329
+
330
+ ```jsx
331
+ const Button = tasty({
332
+ styles: { padding: '2x 4x' },
333
+ variants: {
334
+ primary: { fill: '#blue', color: '#white' },
335
+ danger: { fill: '#red', color: '#white' },
336
+ },
337
+ variant: 'primary',
338
+ });
339
+
340
+ <Button>Save</Button>; // primary
341
+ <Button variant="danger">Delete</Button>;
342
+ ```
343
+
306
344
  ### Extending Variants with Base State Maps
307
345
 
308
346
  When base `styles` contain an extend-mode state map (an object **without** a `''` key), it is applied **after** the variant merge. This lets you add or override states across all variants without repeating yourself:
@@ -413,13 +451,13 @@ On the client, CSS is injected synchronously into the DOM (idempotent via the in
413
451
 
414
452
  ## Style Functions
415
453
 
416
- All style functions below except `useStyles()` are plain functions, not React hooks, and work in client components, SSR with a `ServerStyleCollector`, and React Server Components. They keep the `use` prefix for backward compatibility.
454
+ All style functions below work in client components, SSR with a `ServerStyleCollector`, and React Server Components. All except `useStyles()` are plain functions, not React hooks; they keep the `use` prefix for backward compatibility.
417
455
 
418
456
  In server-only contexts, components that use only Tasty style functions ship no Tasty styling runtime. Astro without `client:*` directives produces no client JavaScript; server-only Next.js RSC follows the same Tasty architecture, while final output depends on the application deployment. Tasty never forces the `'use client'` boundary — that decision belongs to your component when it needs React interactivity (state, effects, event handlers).
419
457
 
420
458
  ### useStyles
421
459
 
422
- Generate a className from a style object. A thin wrapper around `computeStyles()` that also picks up the `TastyRegistry` collector through React context. That makes it a hook: call it inside a component, and not in a Server Component — use `computeStyles()` there.
460
+ Generate a className from a style object. A thin wrapper around `computeStyles()` that also picks up the `TastyRegistry` collector through React context. That makes it a hook on the client: call it inside a component, and use `computeStyles()` outside React. It also works in a Server Component, where it skips the context lookup; like the other style functions there, it cannot emit CSS itself, so the next `tasty()` element in the tree emits it.
423
461
 
424
462
  ```tsx
425
463
  import { useStyles } from '@tenphi/tasty';
@@ -456,9 +494,13 @@ function ThemeStyles() {
456
494
  The selector can be a list (`'.uni-checkbox, uni-checkbox'`) — every item gets
457
495
  the states and sub-elements, with no `:is()` wrapping needed. Root states
458
496
  (`@root(...)`) on a selector that is the root itself (`:root`, `html`) apply to
459
- that element. Local `@keyframes` are injected with the rules, under the same
460
- resolved names `tasty()` components use, and `@starting` states emit
461
- `@starting-style`.
497
+ that element. A selector that ends in a pseudo-element takes its states on the
498
+ pseudo-element (`'.card::before'` with `:hover` gives
499
+ `.card::before:where(:hover)`) and `@parent()` on the element before it.
500
+ Attribute and modifier states, which a pseudo-element can't have, are ignored
501
+ with a warning. Local `@keyframes` are injected with the rules,
502
+ under the same resolved names `tasty()` components use, and `@starting` states
503
+ emit `@starting-style`.
462
504
 
463
505
  A slot — the `id`, or the selector when no `id` is given — holds exactly one
464
506
  injection **per `root`**. So:
@@ -685,9 +727,10 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
685
727
 
686
728
  - 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).
687
729
  - 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`.
730
+ - "Server rendering without a style collector" in development: the server render has no request scope, so each `tasty()` element inlines its own CSS and configured globals are not emitted. Add `tastyIntegration()` (Astro), `TastyRegistry` (Next.js), or `runWithCollector()` — see the [SSR guide](ssr.md).
688
731
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
689
732
  - For dynamic styles that change over the component lifecycle, use the `id` option in `useGlobalStyles()` and `useRawCSS()` to enable update tracking.
690
- - 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.
733
+ - RSC inline mode: CSS accumulated by standalone style functions (`useStyles`, `useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
691
734
 
692
735
  ---
693
736
 
package/docs/ssr.md CHANGED
@@ -187,14 +187,14 @@ meaning; use root-relative, absolute, or data URLs.
187
187
 
188
188
  ### Using Tasty in Server Components
189
189
 
190
- Every Tasty style function except `useStyles()` is hook-free and needs no `'use client'`, so it can be used directly in React Server Components:
190
+ Every Tasty style function needs no `'use client'`, so it can be used directly in React Server Components:
191
191
 
192
192
  - `tasty()` components — dynamic `styleProps` like `<Grid flow="column">` work normally
193
- - `computeStyles()` — the hook-free equivalent of `useStyles()`, which reads React context and cannot run in a Server Component
193
+ - `useStyles()` and its hook-free equivalent `computeStyles()` — a class name for any element
194
194
  - `useGlobalStyles()`, `useRawCSS()` — inject styles by selector
195
195
  - `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, `useFunction()` — inject ancillary CSS rules
196
196
 
197
- Next.js renders Server Components in a separate module graph that cannot see the collector `TastyRegistry` registers, so their CSS takes the RSC inline path: it is accumulated in a per-request `React.cache` and emitted as an inline `<style>` tag by the next `tasty()` component in the tree. Standalone style functions cannot emit CSS on their own, so make sure a `tasty()` component renders after them in every RSC tree. The RSC path does not emit configured globals (tokens, `@property`, `globalStyles`); those come from `TastyRegistry`'s collector or the `withTastyNext()` shared stylesheet. For the same reason, import the module that calls `configure()` from `app/layout.tsx` as well as from the registry module, so the RSC graph sees the same states, recipes, and units.
197
+ Next.js renders Server Components in a separate module graph that cannot see the collector `TastyRegistry` registers, so their CSS takes the RSC inline path: it is accumulated in a per-request `React.cache` and emitted as an inline `<style>` tag by the next `tasty()` component in the tree. Standalone style functions, `useStyles()` included, cannot emit CSS on their own, so make sure a `tasty()` component renders after them in every RSC tree. The RSC path does not emit configured globals (tokens, `@property`, `globalStyles`); those come from `TastyRegistry`'s collector or the `withTastyNext()` shared stylesheet. For the same reason, import the module that calls `configure()` from `app/layout.tsx` as well as from the registry module, so the RSC graph sees the same states, recipes, and units.
198
198
 
199
199
  ### Options
200
200
 
@@ -241,9 +241,9 @@ import { tasty } from '@tenphi/tasty';
241
241
  const Card = tasty({
242
242
  styles: {
243
243
  padding: '4x',
244
- fill: '#surface',
244
+ fill: '#fff',
245
245
  radius: '1r',
246
- border: true,
246
+ border: '1bw solid #ddd',
247
247
  },
248
248
  });
249
249
 
@@ -263,9 +263,9 @@ import Card from '../components/Card.tsx';
263
263
  </html>
264
264
  ```
265
265
 
266
- **Trade-offs**: Astro renders React with the standard `react` build, where `React.cache` does nothing, so there is no per-request style cache. Every `tasty()` element inlines the full CSS for its classes, even when the same component renders twice. Configured globals (`:root` tokens, `@property` defaults, `globalStyles`, `@font-face`) are never emitted, because only the SSR collector emits them, and CSS from `useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`, and `useFunction` is dropped.
266
+ **Trade-offs**: Astro renders React with the standard `react` build, where `React.cache` does nothing, so there is no per-request style cache. Every `tasty()` element inlines the full CSS for its classes, even when the same component renders twice. Configured globals (`:root` tokens, `@property` defaults, `globalStyles`, `@font-face`) are never emitted, because only the SSR collector emits them, and CSS from `useGlobalStyles`, `useRawCSS`, `useKeyframes`, `useProperty`, `useFontFace`, `useCounterStyle`, and `useFunction` is dropped. Without the [default properties](dsl.md#default-properties), styles that read them resolve to nothing: `border: true` and `outline: true` draw nothing, and `transition` with no duration does not animate. In development, Tasty warns once that it is rendering without a style collector.
267
267
 
268
- Best for trying Tasty out with unconfigured styles. Use `tastyIntegration()` for anything that relies on `configure()`.
268
+ Best for trying Tasty out with plain values, as above. Use `tastyIntegration()` for anything that relies on `configure()` or on Tasty's default tokens.
269
269
 
270
270
  ### Astro Integration (recommended)
271
271
 
@@ -463,14 +463,14 @@ const fullHtml = `
463
463
 
464
464
  ```tsx
465
465
  // ── Client ──────────────────────────────────────────────
466
- // Browser-safe: runs hydrateTastyClasses() with window.__TASTY__ on import
467
- import '@tenphi/tasty/ssr/astro-client';
466
+ import { hydrateTastyClasses } from '@tenphi/tasty/ssr/client';
468
467
  import { hydrateRoot } from 'react-dom/client';
469
468
 
469
+ hydrateTastyClasses(); // reads window.__TASTY__
470
470
  hydrateRoot(document.getElementById('root'), <App />);
471
471
  ```
472
472
 
473
- `@tenphi/tasty/ssr` is server-only: it imports `node:async_hooks`, and in a browser bundle rendering throws `AsyncLocalStorage is not a constructor`. Despite its name, `@tenphi/tasty/ssr/astro-client` works with any framework.
473
+ `@tenphi/tasty/ssr/client` is the browser half: it exports `hydrateTastyClasses()` and has no import side effects. A module shared by the server and the client can import `@tenphi/tasty/ssr` too. It is safe in a browser bundle, where there is no server collector to find, and only `runWithCollector()` throws there.
474
474
 
475
475
  ### Streaming SSR
476
476
 
@@ -517,14 +517,15 @@ runWithCollector(collector, () => {
517
517
 
518
518
  ### Entry points
519
519
 
520
- | Import path | Description |
521
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
522
- | `@tenphi/tasty/ssr` | Core SSR API (server-only): `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `getSSRCollector`, `hydrateTastyClasses`; types `ServerStyleArtifact`, `ServerStyleArtifactKind` |
523
- | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
524
- | `@tenphi/tasty/ssr/next-config` | Next.js config wrapper: shared, content-hashed global stylesheet |
525
- | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
526
- | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
527
- | `@tenphi/tasty/ssr/astro-middleware`<br />`@tenphi/tasty/ssr/astro-middleware-static`<br />`@tenphi/tasty/ssr/astro-middleware-extract`<br />`@tenphi/tasty/ssr/astro-middleware-extract-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
520
+ | Import path | Description |
521
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
522
+ | `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `getSSRCollector`, `hydrateTastyClasses`; types `ServerStyleArtifact`, `ServerStyleArtifactKind`. Safe to import in a browser bundle; `runWithCollector()` needs the server |
523
+ | `@tenphi/tasty/ssr/client` | Browser-safe client hydration for any framework: `hydrateTastyClasses`, no import side effects |
524
+ | `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
525
+ | `@tenphi/tasty/ssr/next-config` | Next.js config wrapper: shared, content-hashed global stylesheet |
526
+ | `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
527
+ | `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
528
+ | `@tenphi/tasty/ssr/astro-middleware`<br />`@tenphi/tasty/ssr/astro-middleware-static`<br />`@tenphi/tasty/ssr/astro-middleware-extract`<br />`@tenphi/tasty/ssr/astro-middleware-extract-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
528
529
 
529
530
  ### `ServerStyleCollector`
530
531
 
@@ -587,11 +588,11 @@ Astro middleware factory. Use for manual middleware composition.
587
588
 
588
589
  ### `hydrateTastyClasses(classes?)`
589
590
 
590
- Pre-populate the client injector's rules map with class names rendered on the server, marking them as already present in the DOM so `computeStyles()` skips re-injection during hydration. When called without arguments, reads the class list from `window.__TASTY__` (populated by the streaming `<script>` tags emitted during SSR).
591
+ Exported from `@tenphi/tasty/ssr/client` and `@tenphi/tasty/ssr`. Pre-populate the client injector's rules map with class names rendered on the server, marking them as already present in the DOM so `computeStyles()` skips re-injection during hydration. When called without arguments, reads the class list from `window.__TASTY__` (populated by the streaming `<script>` tags emitted during SSR).
591
592
 
592
593
  ### `runWithCollector(collector, fn)`
593
594
 
594
- Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All style function calls within `fn` (and async continuations) — including `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — will find this collector. Returns `fn`'s return value, synchronously unless `fn` returns a promise.
595
+ Run a function with a `ServerStyleCollector` bound to the current async context via `AsyncLocalStorage`. All style function calls within `fn` (and async continuations) — including `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — will find this collector. Returns `fn`'s return value, synchronously unless `fn` returns a promise. Throws where `AsyncLocalStorage` is unavailable, such as in a browser.
595
596
 
596
597
  ---
597
598
 
@@ -603,7 +604,7 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
603
604
 
604
605
  ### Hydration mismatch warnings
605
606
 
606
- Class names are content hashes of the resolved styles, so render order does not affect them. Unnamed `useKeyframes()` and `useCounterStyle()` names are counters (`tk0`, `tc0`) on both server and client; pass `name` if the two sides can call them in a different order. If you see mismatches, ensure `hydrateTastyClasses()` runs before React hydration. For Next.js, this is automatic. For Astro with `tastyIntegration()`, this is also automatic. For manual Astro middleware setups, import `@tenphi/tasty/ssr/astro-client` in your island components. For custom setups, import `@tenphi/tasty/ssr/astro-client` before `hydrateRoot()`.
607
+ Class names are content hashes of the resolved styles, so render order does not affect them. Unnamed `useKeyframes()` and `useCounterStyle()` names are counters (`tk0`, `tc0`) on both server and client; pass `name` if the two sides can call them in a different order. If you see mismatches, ensure `hydrateTastyClasses()` runs before React hydration. For Next.js, this is automatic. For Astro with `tastyIntegration()`, this is also automatic. For manual Astro middleware setups, import `@tenphi/tasty/ssr/astro-client` in your island components. For custom setups, call `hydrateTastyClasses()` from `@tenphi/tasty/ssr/client` before `hydrateRoot()`.
607
608
 
608
609
  Class names are also derived from the _resolved_ styles, so the server and the client must configure Tasty identically. Anything that changes what a component's styles resolve to will produce a mismatch if it is registered on only one side — `namePrefix`, `recipes`, `handlers`, and the `propHandlers` / `baseStyleProps` extension points described in [Plugins](plugins.md). Call the same `configure()` on both; global CSS is deduplicated automatically, so no `typeof window` guard is needed.
609
610
 
@@ -614,8 +615,9 @@ Class names are also derived from the _resolved_ styles, so the server and the c
614
615
  **Component CSS**: SSR `<style data-tasty-ssr>` tags remain in the DOM. The client injector creates separate `<style>` elements for any new styles. SSR styles are never modified or removed by the client. If this is a concern for very large apps, you can remove the SSR style tags and hydration scripts manually after hydration:
615
616
 
616
617
  ```tsx
617
- import '@tenphi/tasty/ssr/astro-client'; // runs hydrateTastyClasses()
618
+ import { hydrateTastyClasses } from '@tenphi/tasty/ssr/client';
618
619
 
620
+ hydrateTastyClasses();
619
621
  hydrateRoot(root, <App />);
620
622
 
621
623
  // Optional: remove SSR style tags and class-list scripts after hydration
@@ -627,4 +629,8 @@ document.querySelectorAll('script').forEach((el) => {
627
629
 
628
630
  ### `AsyncLocalStorage` not available
629
631
 
630
- `@tenphi/tasty/ssr` imports `node:async_hooks` and is server-only. In a browser bundle the module is stubbed and rendering throws `AsyncLocalStorage is not a constructor`. On the client, import `@tenphi/tasty/ssr/astro-client` (it works with any framework) or use `@tenphi/tasty/ssr/next`, which hydrates automatically.
632
+ `runWithCollector()` throws `runWithCollector() needs AsyncLocalStorage from node:async_hooks` where the runtime has no `AsyncLocalStorage`, such as in a browser bundle. Call it on the server. The rest of `@tenphi/tasty/ssr` is safe to import in a browser bundle; on the client, call `hydrateTastyClasses()` from `@tenphi/tasty/ssr/client`, or use `@tenphi/tasty/ssr/next`, which hydrates automatically.
633
+
634
+ ### "Server rendering without a style collector" warning
635
+
636
+ Tasty rendered on the server with no collector and no React Server Components request scope: Astro without `tastyIntegration()`, a client component server-rendered without `TastyRegistry`, or a custom setup that skips `runWithCollector()`. Each `tasty()` element then inlines all of its CSS, and configured globals (tokens, the default `@property` set, `globalStyles`, `@font-face`) and the CSS of the standalone style functions are not emitted. Add the integration for your framework. The warning is shown once, in development only.
package/docs/styles.md CHANGED
@@ -197,6 +197,9 @@ Element width with min/max control. One, two, or three values set `min-width`, `
197
197
  | `"min 2x"` | `2x` | auto | initial |
198
198
  | `"max 100%"` | initial | auto | `100%` |
199
199
  | `"fixed 200px"` | `200px` | `200px` | `200px` |
200
+ | `"min"` | `1x` | auto | initial |
201
+ | `"max"` | initial | auto | `100%` |
202
+ | `"fixed"` | `max-content` | `max-content` | `max-content` |
200
203
 
201
204
  **Keywords:** `stretch`, `max-content`, `min-content`, `fit-content`
202
205
 
@@ -312,6 +315,8 @@ Background color with design token support. Preferred over `backgroundColor` and
312
315
 
313
316
  When two colors are provided, the first sets the background color and the second is applied as an overlay gradient layer. This enables independent CSS transitions on each color. With `image` or `backgroundImage` on the same element, the image is layered over the overlay color in one `background-image`.
314
317
 
318
+ There is no default fill color, so `fill: true` is ignored with a development warning. Use `false` for no fill.
319
+
315
320
  **Related deprecated props:** `backgroundColor` (use `fill`), `background` (overrides all background props when set — avoid).
316
321
 
317
322
  ### `image`
@@ -341,6 +346,7 @@ Text color with design token support.
341
346
  | `"#current.5"` | Current inherited color at 50% opacity |
342
347
  | `"(#primary, #secondary)"` | Fallback: use `#primary`, fall back to `#secondary` |
343
348
  | `"light-dark(#dark, #light)"` | CSS color function — see [CSS Color Functions](dsl.md#css-color-functions) |
349
+ | `"#ddd"` | Hex literal, read as the `#ddd` token with the literal as fallback: `var(--ddd-color, #ddd)`, as in every color style |
344
350
  | `true` | `currentColor` |
345
351
 
346
352
  Also sets `$current-color` to the same color, for anything that needs the
@@ -19,22 +19,25 @@ If the styles depend on props, keep `tasty()`: rendered only on the server, it a
19
19
 
20
20
  Build-time extraction is part of the main `@tenphi/tasty` package but requires additional peer dependencies depending on your setup:
21
21
 
22
- | Dependency | Version | Required for |
23
- | ---------------------------- | ------- | ------------------------------------------------------------------------------ |
24
- | `@babel/core` | >= 7.24 | Babel plugin (`@tenphi/tasty/babel-plugin`) |
25
- | `@babel/helper-plugin-utils` | >= 7.24 | Babel plugin |
26
- | `@babel/types` | >= 7.24 | Babel plugin |
27
- | `jiti` | >= 2.6 | Babel plugin and Next.js wrapper (imported on load, even without `configFile`) |
28
- | `babel-loader` | any | Next.js wrapper (`@tenphi/tasty/zero/next`) |
22
+ | Dependency | Version | Required for |
23
+ | ---------------------------- | ------- | ---------------------------------------------------- |
24
+ | `@babel/core` | >= 7.24 | Babel plugin (`@tenphi/tasty/babel-plugin`) |
25
+ | `@babel/helper-plugin-utils` | >= 7.24 | Babel plugin |
26
+ | `@babel/types` | >= 7.24 | Babel plugin |
27
+ | `jiti` | >= 2.6 | `configFile` only (Babel plugin and Next.js wrapper) |
28
+ | `babel-loader` | any | Next.js wrapper (`@tenphi/tasty/zero/next`) |
29
29
 
30
- The Babel packages and `jiti` are optional peer dependencies of `@tenphi/tasty`; `babel-loader` is resolved from your project. Install what your setup requires:
30
+ The Babel packages and `jiti` are optional peer dependencies of `@tenphi/tasty`; `babel-loader` is resolved from your project. `jiti` is loaded only to read a `configFile`, so a setup that passes `config` directly does not need it; without it, `configFile` fails with an error that says to install it. Install what your setup requires:
31
31
 
32
32
  ```bash
33
33
  # Any Babel-based setup (Vite, custom Babel config, etc.)
34
- pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types jiti
34
+ pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types
35
35
 
36
36
  # Next.js
37
- pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types jiti babel-loader
37
+ pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types babel-loader
38
+
39
+ # Either, with configFile
40
+ pnpm add -D jiti
38
41
  ```
39
42
 
40
43
  ---
@@ -173,7 +176,7 @@ module.exports = {
173
176
  | -------------------------- | ------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
174
177
  | `output` | `string` | `'tasty.css'` | Path for generated CSS file |
175
178
  | `mode` | `'file' \| 'inject'` | `'file'` | `'file'` writes CSS to disk; `'inject'` embeds CSS inline in JS (see [Inject Mode](#inject-mode)) |
176
- | `configFile` | `string` | — | Absolute path to a TS/JS module that default-exports a `TastyZeroConfig` object. JSON-serializable alternative to `config` — required for Turbopack. |
179
+ | `configFile` | `string` | — | Absolute path to a TS/JS module that default-exports a `TastyZeroConfig` object. JSON-serializable alternative to `config` — required for Turbopack. Loaded with the optional `jiti` peer. |
177
180
  | `config` | `TastyZeroConfig \| () => TastyZeroConfig` | `{}` | Inline config object or factory function. Takes precedence over `configFile`. |
178
181
  | `configDeps` | `string[]` | `[]` | Absolute file paths that affect config (for cache invalidation) |
179
182
  | `injectImport` | `boolean` | `true` | Replace `@tenphi/tasty/static` imports with an import of the generated CSS file. Set to `false` to manage CSS imports manually. |
@@ -185,7 +188,8 @@ module.exports = {
185
188
  | `config.keyframes` | `Record<string, KeyframesSteps>` | — | Global `@keyframes` definitions available to all `tastyStatic` calls |
186
189
  | `config.fontFaces` | `Record<string, FontFaceInput>` | — | Global `@font-face` definitions |
187
190
  | `config.counterStyles` | `Record<string, CounterStyleDescriptors>` | — | Global `@counter-style` definitions |
188
- | `config.units` | `Record<string, string \| UnitHandler>` | — | Custom units for the style parser (merged with built-ins). E.g. `{ em: 'em', vw: 'vw' }` |
191
+ | `config.properties` | `Record<string, PropertyDefinition>` | — | Global `@property` definitions, written with Tasty's [default properties](dsl.md#default-properties) |
192
+ | `config.units` | `Record<string, string \| UnitHandler>` | — | Custom units for the style parser (merged with built-ins). E.g. `{ u: 'var(--u)' }` makes `2u` compile to `calc(2 * var(--u))`. Native CSS units need no entry |
189
193
  | `config.functions` | `Record<string, FunctionDefinition \| Function>` | — | Custom functions (merged). Bare keys → parse functions; `$$name` keys → declarative CSS `@function` definitions |
190
194
  | `config.polyfills` | `{ functions?: boolean }` | `{}` | Opt-in polyfills. `functions: true` inlines `@function` calls into plain CSS at build time |
191
195
  | `config.plugins` | `TastyPlugin[]` | — | Plugins that extend tasty with custom functions, units, states, and handlers |
@@ -286,7 +290,7 @@ export default withTastyZero({
286
290
 
287
291
  Starting with Next.js 16, Turbopack is the default bundler. `withTastyZero` supports it out of the box by injecting `turbopack.rules` with `babel-loader` and JSON-serializable options.
288
292
 
289
- The `configFile` option is key for Turbopack — it passes a file path (JSON-serializable) instead of a function, and the Babel plugin loads the config internally via jiti.
293
+ The `configFile` option is key for Turbopack — it passes a file path (JSON-serializable) instead of a function, and the Babel plugin loads the config internally via `jiti`, which must be installed.
290
294
 
291
295
  **Requirements**: `babel-loader` must be installed in your project (see [Requirements](#requirements)):
292
296
 
@@ -331,7 +335,7 @@ extractStylesWithChunks({ display: 'flex', gap: '1u' });
331
335
 
332
336
  `configure()` takes the same options as the Babel plugin's `config` (see [Plugin Options](#plugin-options)) and replaces the previous configuration on each call; `resetConfig()` returns to the defaults. Options that shape the extracted rules apply — `states`, `units`, parse `functions`, `handlers`, `replaceTokens`, `recipes`, `plugins`, `namePrefix`. Definitions that become at-rules or global CSS of their own are not emitted with the rules: `tokens`, `globalStyles`, `fontFaces`, `counterStyles`, `properties` and `$$` `@function` definitions. Render tokens and global styles with `extractStylesForSelector(':root', tokens)` and `extractStylesForSelector(selector, styles)`.
333
337
 
334
- `extractConfigProperties()` returns the `@property` rules the Babel plugin writes at the top of its output: Tasty's default set (`#border`, `#outline`, `$gap`, `$radius`, `$transition`, …) overlaid with the configured `properties`. Emit them once per stylesheet. Style handlers read several of these tokens with no fallback of their own (`border: true` reads `#border`, `transition` reads `$transition`), so without them those declarations resolve to nothing.
338
+ `extractConfigProperties()` returns the `@property` rules the Babel plugin writes at the top of its output: Tasty's [default set](dsl.md#default-properties) (`#border`, `#outline`, `$gap`, `$radius`, `$transition`, …) overlaid with the configured `properties`, or with the `properties` map you pass it. Emit them once per stylesheet. Style handlers read several of these tokens with no fallback of their own (`border: true` reads `#border`, `transition` reads `$transition`), so without them those declarations resolve to nothing.
335
339
 
336
340
  The configuration is process-wide and shared with the Babel plugin and the runtime `configure()` from `@tenphi/tasty`. The extractors re-apply their own configuration when the Babel plugin replaced it, but not after a runtime `configure()` — don't mix the runtime and build-time configuration in one process.
337
341
 
@@ -438,13 +442,19 @@ The `@tenphi/tasty/static` import becomes an import of the generated file (relat
438
442
  ```css
439
443
  /* Generated by @tenphi/tasty/zero - DO NOT EDIT */
440
444
 
445
+ @property --tasty-second-fill-color {
446
+ syntax: '<color>';
447
+ inherits: false;
448
+ initial-value: transparent;
449
+ }
450
+
441
451
  @property --current-color {
442
452
  syntax: '<color>';
443
453
  inherits: true;
444
454
  initial-value: currentcolor;
445
455
  }
446
456
 
447
- /* …the rest of Tasty's default @property rules (--gap, --radius, --white-color, …) */
457
+ /* …the rest of Tasty's default @property rules (--white-color, --border-color, --gap, --transition, …), then config.properties */
448
458
 
449
459
  .ts3f783b.ts3f783b {
450
460
  color: var(--white-color);
@@ -465,7 +475,7 @@ The `@tenphi/tasty/static` import becomes an import of the generated file (relat
465
475
  }
466
476
  ```
467
477
 
468
- Styles are split into chunks (here appearance and spacing), each with its own class. Color tokens compile to custom properties: `#purple` becomes `var(--purple-color)`. Define it in `config.tokens` and the plugin writes it to `:root` at the top of the same file. Units are literal (`2x` is `16px`). Selector-mode rules use the selector as written, without the doubled specificity of class rules.
478
+ The file starts with Tasty's [default `@property` rules](dsl.md#default-properties) and your `config.properties`, written once. Styles are split into chunks (here appearance and spacing), each with its own class. Color tokens compile to custom properties: `#purple` becomes `var(--purple-color)`. Define it in `config.tokens` and the plugin writes it to `:root` at the top of the same file. Units are literal (`2x` is `16px`). Selector-mode rules use the selector as written, without the doubled specificity of class rules.
469
479
 
470
480
  ---
471
481
 
@@ -537,6 +547,11 @@ tastyStatic('.heading', { preset: 'h1' });
537
547
  ```tsx
538
548
  import { injectCSS as _$i } from '@tenphi/tasty/static/inject';
539
549
 
550
+ _$i(
551
+ '@property',
552
+ '@property --tasty-second-fill-color { … }\n@property --current-color { … }\n…',
553
+ );
554
+
540
555
  const button =
541
556
  (_$i(
542
557
  'tsa903d6 ts070801',
@@ -553,6 +568,8 @@ const button =
553
568
  _$i('.heading', '.heading { font-size: var(--h1-font-size, …); … }');
554
569
  ```
555
570
 
571
+ Each file that calls `tastyStatic()` starts by injecting Tasty's default `@property` rules and your `config.properties` under the `@property` id. `injectCSS()` skips an id it has already injected, so the page gets them once.
572
+
556
573
  ### Dev Mode / HMR
557
574
 
558
575
  Class names are content-hashed (`ts` + MD5). When styles change, a new hash produces a new `_$i` call that injects fresh CSS. The injector deduplicates by id, so unchanged styles are skipped. Old CSS stays in the DOM but is harmless since no elements reference those class names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/tasty",
3
- "version": "3.11.0",
3
+ "version": "3.11.2",
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",
@@ -211,7 +211,7 @@
211
211
  "name": "main (tasty)",
212
212
  "path": "dist/index.js",
213
213
  "import": "{ tasty }",
214
- "limit": "52.67 kB"
214
+ "limit": "53.84 kB"
215
215
  },
216
216
  {
217
217
  "name": "main (StyleParser)",
@@ -235,7 +235,7 @@
235
235
  "name": "core (import *)",
236
236
  "path": "dist/core/index.js",
237
237
  "import": "*",
238
- "limit": "55.23 kB"
238
+ "limit": "56.29 kB"
239
239
  },
240
240
  {
241
241
  "name": "static",
@@ -252,7 +252,7 @@
252
252
  "path",
253
253
  "crypto"
254
254
  ],
255
- "limit": "39.13 kB"
255
+ "limit": "40.16 kB"
256
256
  },
257
257
  {
258
258
  "name": "babel-plugin",
@@ -265,7 +265,7 @@
265
265
  "node:module",
266
266
  "node:path"
267
267
  ],
268
- "limit": "43.55 kB"
268
+ "limit": "44.79 kB"
269
269
  },
270
270
  {
271
271
  "name": "astro client",