@tenphi/tasty 0.0.0-snapshot.1d29633 → 0.0.0-snapshot.1f1c00a

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 (122) hide show
  1. package/README.md +152 -134
  2. package/dist/{babel-Cg-mJV_t.d.ts → babel-rZcEwPLM.d.ts} +2 -3
  3. package/dist/chunks/async-storage-CG5HnUT5.js +20 -0
  4. package/dist/chunks/async-storage-CG5HnUT5.js.map +1 -0
  5. package/dist/chunks/build-config-CL4eRu2z.js +42 -0
  6. package/dist/chunks/build-config-CL4eRu2z.js.map +1 -0
  7. package/dist/{collector-BMT05AmU.js → chunks/collector-D2ARGT2L.js} +21 -117
  8. package/dist/chunks/collector-D2ARGT2L.js.map +1 -0
  9. package/dist/chunks/config-engine-DEmmPPdp.js +272 -0
  10. package/dist/chunks/config-engine-DEmmPPdp.js.map +1 -0
  11. package/dist/chunks/css-definitions-DotDkIXt.js +876 -0
  12. package/dist/chunks/css-definitions-DotDkIXt.js.map +1 -0
  13. package/dist/chunks/css-resources-P7IBUUZs.js +142 -0
  14. package/dist/chunks/css-resources-P7IBUUZs.js.map +1 -0
  15. package/dist/chunks/debug-D8pg9RGI.js +502 -0
  16. package/dist/chunks/debug-D8pg9RGI.js.map +1 -0
  17. package/dist/chunks/dsl-DRhFWQzo.js +1378 -0
  18. package/dist/chunks/dsl-DRhFWQzo.js.map +1 -0
  19. package/dist/chunks/hydration-TRcz5vnk.js +41 -0
  20. package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
  21. package/dist/chunks/react-runtime-BpxlOTbO.js +562 -0
  22. package/dist/chunks/react-runtime-BpxlOTbO.js.map +1 -0
  23. package/dist/chunks/runtime-engine-CKFXUH-A.js +2416 -0
  24. package/dist/chunks/runtime-engine-CKFXUH-A.js.map +1 -0
  25. package/dist/chunks/runtime-state-BxaLhVdP.js +127 -0
  26. package/dist/chunks/runtime-state-BxaLhVdP.js.map +1 -0
  27. package/dist/{merge-styles-CkAcnd0Z.js → chunks/shared-utils-y-HjvPN4.js} +60 -55
  28. package/dist/chunks/shared-utils-y-HjvPN4.js.map +1 -0
  29. package/dist/chunks/style-engine-BBbBuSkT.js +5260 -0
  30. package/dist/chunks/style-engine-BBbBuSkT.js.map +1 -0
  31. package/dist/{css-writer-b02uBqNF.js → chunks/zero-engine-gJrKBRiw.js} +6 -121
  32. package/dist/chunks/zero-engine-gJrKBRiw.js.map +1 -0
  33. package/dist/{collector-BFuqdF2F.d.ts → collector-w4mBj5gd.d.ts} +23 -4
  34. package/dist/{config-DmFxnBxg.d.ts → config-CZWCzBr6.d.ts} +171 -72
  35. package/dist/core/index.d.ts +5 -5
  36. package/dist/core/index.js +9 -6
  37. package/dist/{index-D1nMkpUe.d.ts → index-G9JuwM9p.d.ts} +263 -247
  38. package/dist/index.d.ts +5 -5
  39. package/dist/index.js +11 -921
  40. package/dist/{merge-styles-Cx_6ySBv.d.ts → merge-styles-VXwJ0Frr.d.ts} +2 -3
  41. package/dist/ssr/astro-client.d.ts +1 -1
  42. package/dist/ssr/astro-client.js +1 -14
  43. package/dist/ssr/astro-client.js.map +1 -1
  44. package/dist/ssr/astro-middleware-extract-static.d.ts +10 -0
  45. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  46. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  47. package/dist/ssr/astro-middleware-extract.d.ts +10 -0
  48. package/dist/ssr/astro-middleware-extract.js +9 -0
  49. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  50. package/dist/ssr/astro-middleware-static.d.ts +3 -2
  51. package/dist/ssr/astro-middleware-static.js +0 -11
  52. package/dist/ssr/astro-middleware-static.js.map +1 -1
  53. package/dist/ssr/astro-middleware.d.ts +3 -2
  54. package/dist/ssr/astro-middleware.js +0 -12
  55. package/dist/ssr/astro-middleware.js.map +1 -1
  56. package/dist/ssr/astro.d.ts +40 -6
  57. package/dist/ssr/astro.js +157 -93
  58. package/dist/ssr/astro.js.map +1 -1
  59. package/dist/ssr/index.d.ts +10 -11
  60. package/dist/ssr/index.js +4 -4
  61. package/dist/ssr/index.js.map +1 -1
  62. package/dist/ssr/next-config.d.ts +64 -0
  63. package/dist/ssr/next-config.js +128 -0
  64. package/dist/ssr/next-config.js.map +1 -0
  65. package/dist/ssr/next.d.ts +9 -8
  66. package/dist/ssr/next.js +29 -43
  67. package/dist/ssr/next.js.map +1 -1
  68. package/dist/static/index.d.ts +8 -9
  69. package/dist/static/index.js +4 -22
  70. package/dist/static/index.js.map +1 -1
  71. package/dist/static/inject.d.ts +1 -2
  72. package/dist/{index-LC3O3Jj4.d.ts → types-JEY8PVYP.d.ts} +862 -811
  73. package/dist/zero/babel.d.ts +1 -1
  74. package/dist/zero/babel.js +41 -107
  75. package/dist/zero/babel.js.map +1 -1
  76. package/dist/zero/index.d.ts +6 -7
  77. package/dist/zero/index.js +1 -1
  78. package/dist/zero/next.d.ts +3 -5
  79. package/dist/zero/next.js +0 -31
  80. package/dist/zero/next.js.map +1 -1
  81. package/docs/README.md +13 -8
  82. package/docs/adoption.md +56 -31
  83. package/docs/ai-agents.md +107 -80
  84. package/docs/comparison.md +69 -40
  85. package/docs/configuration.md +179 -114
  86. package/docs/debug.md +19 -7
  87. package/docs/design-system.md +41 -22
  88. package/docs/dsl.md +310 -201
  89. package/docs/getting-started.md +64 -41
  90. package/docs/injector.md +63 -26
  91. package/docs/methodology.md +44 -38
  92. package/docs/migration-v3.md +49 -49
  93. package/docs/pipeline.md +32 -8
  94. package/docs/plugins.md +38 -34
  95. package/docs/react-api.md +67 -44
  96. package/docs/runtime-benchmarks.md +388 -0
  97. package/docs/ssr.md +219 -61
  98. package/docs/styles.md +95 -18
  99. package/docs/tasty-static.md +138 -105
  100. package/package.json +104 -33
  101. package/dist/async-storage-DKK-wTD4.js +0 -44
  102. package/dist/async-storage-DKK-wTD4.js.map +0 -1
  103. package/dist/collector-BMT05AmU.js.map +0 -1
  104. package/dist/config-DF93rL3u.js +0 -12661
  105. package/dist/config-DF93rL3u.js.map +0 -1
  106. package/dist/context-CA8YKeMn.js +0 -24
  107. package/dist/context-CA8YKeMn.js.map +0 -1
  108. package/dist/core-B5ybnMP1.js +0 -1529
  109. package/dist/core-B5ybnMP1.js.map +0 -1
  110. package/dist/css-writer-b02uBqNF.js.map +0 -1
  111. package/dist/format-global-rules-DklyaXv-.js +0 -22
  112. package/dist/format-global-rules-DklyaXv-.js.map +0 -1
  113. package/dist/format-rules-Cs4e3M9m.js +0 -130
  114. package/dist/format-rules-Cs4e3M9m.js.map +0 -1
  115. package/dist/hydrate-aaUSNJZJ.js +0 -37
  116. package/dist/hydrate-aaUSNJZJ.js.map +0 -1
  117. package/dist/index.js.map +0 -1
  118. package/dist/keyframes-BedEzEXk.js +0 -424
  119. package/dist/keyframes-BedEzEXk.js.map +0 -1
  120. package/dist/merge-styles-CkAcnd0Z.js.map +0 -1
  121. package/dist/resolve-recipes-CiyLtJG9.js +0 -144
  122. package/dist/resolve-recipes-CiyLtJG9.js.map +0 -1
package/docs/dsl.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Style DSL Reference
2
2
 
3
- This is the Tasty style language reference — the value syntax, state mappings, tokens, units, extending semantics, and special declarations that apply to both runtime `tasty()` and build-time `tastyStatic()`.
3
+ This is the language behind Tasty’s predictable state resolution. Every property can use a state map; later branches declare higher priority, and Tasty compiles them so only one generated selector can match.
4
4
 
5
- For the runtime React API (`tasty()`, hooks, component props), see [React API](react-api.md). For all enhanced style properties, see [Style Properties](styles.md). For global configuration, see [Configuration](configuration.md).
5
+ The same value syntax, state maps, tokens, units, extension semantics, and special declarations apply to React `tasty()`—whether rendered on the client or server—and build-time `tastyStatic()`.
6
+
7
+ For the React API (`tasty()`, style functions, component props), see [React API](react-api.md). For all enhanced style properties, see [Style Properties](styles.md). For global configuration, see [Configuration](configuration.md).
6
8
 
7
9
  ---
8
10
 
@@ -18,17 +20,28 @@ fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
18
20
 
19
21
  #### State Key Types
20
22
 
21
- | Syntax | Example | Generated CSS |
22
- |--------|---------|---------------|
23
- | Boolean modifier | `hovered` | `[data-hovered]` |
24
- | Value modifier | `theme=danger` | `[data-theme="danger"]` |
25
- | Pseudo-class | `:hover` | `:hover` |
26
- | Class selector | `.active` | `.active` |
27
- | Attribute selector | `[aria-expanded="true"]` | `[aria-expanded="true"]` |
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]` |
23
+ | Syntax | Example | Generated CSS |
24
+ | ------------------ | ------------------------ | ------------------------------------------------------------------------ |
25
+ | Boolean modifier | `hovered` | `[data-hovered]` |
26
+ | Value modifier | `theme=danger` | `[data-theme="danger"]` |
27
+ | Pseudo-class | `:hover` | `:hover` |
28
+ | Class selector | `.active` | `.active` |
29
+ | Attribute selector | `[aria-expanded="true"]` | `[aria-expanded="true"]` |
30
+ | Combined (AND) | `hovered & .active` | `[data-hovered].active` |
31
+ | Combined (OR) | `hovered \| focused` | `[data-hovered], [data-focused]` |
32
+ | Negated (NOT) | `!disabled` | `:not([data-disabled])` |
33
+ | Exclusive (XOR) | `hovered ^ focused` | `[data-hovered]:not([data-focused]), :not([data-hovered])[data-focused]` |
34
+
35
+ #### Automatic Native States
36
+
37
+ `disabled` and `checked` are built-in automatic states. On a Tasty component, they track the corresponding native prop or attribute, so these pairs express the same component state:
38
+
39
+ | Concise Tasty state | Explicit attribute selector |
40
+ | ------------------- | --------------------------- |
41
+ | `disabled` | `[disabled]` |
42
+ | `checked` | `[checked]` |
43
+
44
+ Prefer the concise form when authoring Tasty component styles. Explicit attribute selectors remain valid and can be useful when targeting arbitrary attributes or when CSS-shaped syntax makes an introductory example easier to recognize.
32
45
 
33
46
  Operator precedence (highest to lowest): `!` (NOT) > `^` (XOR) > `|` (OR) > `&` (AND). Use parentheses to override: `hovered & (pressed ^ focused)`.
34
47
 
@@ -41,6 +54,8 @@ fill: {
41
54
  }
42
55
  ```
43
56
 
57
+ Modifier and class names start with a letter and may be a single letter (`x` → `[data-x]`, `.x`). Tasty emits an `INVALID_STATE_KEY` warning for a key it cannot fully read and skips the unreadable parts. That covers characters outside the notation (`1st`, or `@media (w < 600px)` with a space before the parenthesis), an operator missing a state (`hovered &`), and two states with no operator between them (`hovered focused`). A partly read key can apply in states you did not mean, so fix the key rather than rely on what it produces.
58
+
44
59
  #### Default State Ordering
45
60
 
46
61
  Key order sets priority — later keys win and turn off earlier ones via negation. The bare default (`''`) is the lowest-priority state, so it **must be the first key**. If it appears after other states, Tasty moves it to the front and emits a dev warning (`MISPLACED_DEFAULT_STATE`); otherwise it would override every state above it.
@@ -53,14 +68,18 @@ color: { '': '#text', hovered: '#accent' }
53
68
  color: { hovered: '#accent', '': '#text' }
54
69
  ```
55
70
 
56
- The bare `''` default still **receives negation** (it is turned off when a higher-priority state matches). For a value that must always apply as a guaranteed floor — even where a query is *unknown* — use the [`_` fallback floor](#_--fallback-floor) instead. The two can coexist: `''` is the negated default, `_` is the always-on floor. If a map contains only `_` and `''` (no other states), the `''` default is redundant — Tasty keeps the `_` value and drops `''` with a `REDUNDANT_DEFAULT_STATE` warning.
71
+ The bare `''` default still **receives negation** (it is turned off when a higher-priority state matches). For a value that must always apply as a guaranteed floor — even where a query is _unknown_ — use the [`_` fallback floor](#_--fallback-floor) instead. The two can coexist: `''` is the negated default, `_` is the always-on floor. If a map contains only `_` and `''` (no other states), the `''` default is redundant — Tasty keeps the `_` value and drops `''` with a `REDUNDANT_DEFAULT_STATE` warning.
57
72
 
58
73
  ### Sub-element
59
74
 
60
75
  Element styled using a capitalized key. Identified by `data-element` attribute:
61
76
 
62
77
  ```jsx
63
- styles: { Title: { preset: 'h3' } }
78
+ styles: {
79
+ Title: {
80
+ preset: 'h3';
81
+ }
82
+ }
64
83
  // Targets: <div data-element="Title">
65
84
  ```
66
85
 
@@ -70,19 +89,19 @@ Control how a sub-element selector attaches to the root selector using the `$` p
70
89
 
71
90
  Examples below assume the sub-element key is **Cell** (i.e. `[data-element="Cell"]` in CSS):
72
91
 
73
- | Pattern | Result | Description |
74
- |---------|--------|-------------|
75
- | *(none)* | `[data-element="Cell"]` | Descendant (default) |
76
- | `>` | `> [data-element="Cell"]` | Direct child |
77
- | `>Body>` | `> [data-element="Body"] > [data-element="Cell"]` | Chained elements |
78
- | `> Cell` | `> [data-element="Cell"]` | Self-name shorthand — when the trailing element name matches the sub-element's own key, it acts as the placeholder (same as `@`); no duplication |
79
- | `h1` | `h1` | Tag selector (no key injection) |
80
- | `h1 >` | `h1 > [data-element="Cell"]` | Key is direct child of tag |
81
- | `h1 *` | `h1 *` | Any descendant of tag |
82
- | `*` | `*` | All descendants |
83
- | `&::before` | `::before` | Root pseudo (no key); `&` is required |
84
- | `@::before` | `[data-element="Cell"]::before` | Pseudo on the sub-element |
85
- | `>@.active` | `> [data-element="Cell"].active` | Class on the sub-element |
92
+ | Pattern | Result | Description |
93
+ | ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
94
+ | _(none)_ | `[data-element="Cell"]` | Descendant (default) |
95
+ | `>` | `> [data-element="Cell"]` | Direct child |
96
+ | `>Body>` | `> [data-element="Body"] > [data-element="Cell"]` | Chained elements |
97
+ | `> Cell` | `> [data-element="Cell"]` | Self-name shorthand — when the trailing element name matches the sub-element's own key, it acts as the placeholder (same as `@`); no duplication |
98
+ | `h1` | `h1` | Tag selector (no key injection) |
99
+ | `h1 >` | `h1 > [data-element="Cell"]` | Key is direct child of tag |
100
+ | `h1 *` | `h1 *` | Any descendant of tag |
101
+ | `*` | `*` | All descendants |
102
+ | `&::before` | `::before` | Root pseudo (no key); `&` is required |
103
+ | `@::before` | `[data-element="Cell"]::before` | Pseudo on the sub-element |
104
+ | `>@.active` | `> [data-element="Cell"].active` | Class on the sub-element |
86
105
 
87
106
  Rules for key injection (`[data-element="..."]`):
88
107
 
@@ -111,7 +130,7 @@ const List = tasty({
111
130
  Named color prefixed with `#` that maps to CSS custom properties. Supports opacity with `.N` suffix:
112
131
 
113
132
  ```jsx
114
- fill: '#purple.5' // → oklch(from var(--purple-color) l c h / .5)
133
+ fill: '#purple.5'; // → oklch(from var(--purple-color) l c h / .5)
115
134
  ```
116
135
 
117
136
  ### Modifier
@@ -146,12 +165,12 @@ fill: '#purple.5';
146
165
  ```
147
166
 
148
167
  The channels are copied over and the alpha slot is written, which asks nothing of
149
- the color beyond *being* a color. That means the suffix works on every one of
168
+ the color beyond _being_ a color. That means the suffix works on every one of
150
169
  these:
151
170
 
152
171
  - a token holding a `color-mix()`, a `light-dark()`, or a `color()` in a space
153
172
  Tasty cannot convert — none of which have channels to decompose
154
- - `#current`, which resolves to `currentcolor`
173
+ - `#current`, which resolves to the color the element inherits
155
174
  - a `--name-color` variable declared in your own CSS, with no Tasty token
156
175
  definition behind it
157
176
 
@@ -163,9 +182,44 @@ Two properties follow from writing the alpha slot rather than compositing:
163
182
  `/ var(--fade)` unchanged, so it works whether `$fade` holds `.5` or `50%` —
164
183
  which is what `--*-opacity` properties are registered to accept.
165
184
 
185
+ ### `#current` is the keyword
186
+
187
+ `#current` emits the `currentcolor` keyword, so it resolves against the element
188
+ that reads it. That is what lets it follow a color an ancestor faded — a ramp
189
+ that expresses its disabled state once, in `color`, fades everything painted from
190
+ `#current` below it with no further declarations.
191
+
192
+ The `color` style also publishes `--current-color` beside every color it sets,
193
+ for anything that needs the inherited color _as a color_ rather than as the
194
+ keyword — hand-authored CSS, or a place the keyword will not do. Read it as
195
+ `$current-color`. It is registered with `initial-value: currentcolor`, so where
196
+ nothing published it, it still resolves against each element's own color:
197
+
198
+ ```jsx
199
+ fill: '$current-color'; // → var(--current-color)
200
+ ```
201
+
202
+ A color that already reads the color it inherits — `#current`, a `#current` fade
203
+ — is not published into it: resolving such a value a second time at a descendant
204
+ would fade it twice.
205
+
206
+ One case needs the wider floor to be considered. A token _defined_ as `#current`
207
+ and then faded gives relative color syntax a `currentcolor` origin, which Safari
208
+ supports only from 18:
209
+
210
+ ```jsx
211
+ { '#ink': '#current', fill: '#ink.5' }
212
+ // --ink-color: currentcolor
213
+ // fill: oklch(from currentcolor l c h / .5) → Chrome, Firefox, Safari 18+
214
+ ```
215
+
216
+ Put the fade in the token instead — `{ '#ink': '#current.5' }` — and it goes
217
+ through `color-mix()`, which works from Safari 16.2 and composes like every other
218
+ `#current` fade.
219
+
166
220
  ### `#current` composes instead
167
221
 
168
- `#current` is the one exception, and the difference is deliberate. A token *names*
222
+ `#current` is the one exception, and the difference is deliberate. A token _names_
169
223
  a color, so fading it sets its alpha. `currentcolor` is the color an element
170
224
  **inherits**, which an ancestor may already have faded — `#current.4` means "40%
171
225
  of what reaches me":
@@ -182,9 +236,9 @@ step. Because a `color-mix()` percentage cannot be a `<number>`, an opacity
182
236
  custom property used as `#current.$fade` must hold a unitless number; a token
183
237
  accepts either form.
184
238
 
185
- The space is always `oklch`, whatever [`colorSpace`](configuration.md#color-space)
186
- is set to: it is unbounded, so a wide-gamut color survives a round trip that a
187
- gamut-limited space would clamp.
239
+ The space is always `oklch`: it is unbounded, so a wide-gamut color survives a
240
+ round trip that a gamut-limited space would clamp. Nothing configures this —
241
+ see [Color space](configuration.md#color-space).
188
242
 
189
243
  ---
190
244
 
@@ -231,22 +285,22 @@ configure({
231
285
  },
232
286
  });
233
287
 
234
- fill: '#solid.5'; // → hsl(220 90% 50% / .5)
235
- fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
288
+ fill: '#solid.5'; // → hsl(220 90% 50% / .5)
289
+ fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
236
290
  ```
237
291
 
238
292
  ---
239
293
 
240
294
  ## Built-in Units
241
295
 
242
- | Unit | Description | Example | CSS Output |
243
- |------|-------------|---------|------------|
244
- | `x` | Gap multiplier | `2x` | `calc(var(--gap) * 2)` |
245
- | `r` | Border radius | `1r` | `var(--radius)` |
246
- | `cr` | Card border radius | `1cr` | `var(--card-radius)` |
247
- | `bw` | Border width | `2bw` | `calc(var(--border-width) * 2)` |
248
- | `ow` | Outline width | `1ow` | `var(--outline-width)` |
249
- | `sf` | Stable fraction | `1sf` | `minmax(0, 1fr)` |
296
+ | Unit | Description | Example | CSS Output |
297
+ | ---- | ------------------ | ------- | ------------------------------- |
298
+ | `x` | Gap multiplier | `2x` | `calc(var(--gap) * 2)` |
299
+ | `r` | Border radius | `1r` | `var(--radius)` |
300
+ | `cr` | Card border radius | `1cr` | `var(--card-radius)` |
301
+ | `bw` | Border width | `2bw` | `calc(var(--border-width) * 2)` |
302
+ | `ow` | Outline width | `1ow` | `var(--outline-width)` |
303
+ | `sf` | Stable fraction | `1sf` | `minmax(0, 1fr)` |
250
304
 
251
305
  You can register additional custom units via [`configure()`](configuration.md#options).
252
306
 
@@ -289,9 +343,29 @@ const ElevatedCard = tasty({
289
343
  });
290
344
  ```
291
345
 
346
+ **Where recipes resolve:**
347
+
348
+ The `recipe` property is resolved on the component root and independently inside each sub-element's styles:
349
+
350
+ ```jsx
351
+ const CardWithPanel = tasty({
352
+ styles: {
353
+ recipe: 'card',
354
+ Panel: {
355
+ recipe: 'card elevated',
356
+ padding: '2x', // Overrides the panel's base recipe padding
357
+ },
358
+ },
359
+ });
360
+ ```
361
+
362
+ At each level, base recipes are collected in name order (later recipes replace earlier values for the same property), then local styles are merged, then post recipes are merged. The merges with local styles and post recipes follow the [state-map extension and replacement rules](#extending-vs-replacing-state-maps).
363
+
364
+ Recipe definitions themselves cannot contain a `recipe` reference to another recipe: there is no recursive expansion or recipe inheritance. Compose names in the root or sub-element `recipe` property instead. See [Configuration — Recipes](configuration.md#recipes) for an unsupported nesting example.
365
+
292
366
  **Post-merge recipes (`/` separator):**
293
367
 
294
- Recipes listed after `/` are applied *after* component styles using `mergeStyles`:
368
+ Recipes listed after `/` are applied _after_ component styles using `mergeStyles`:
295
369
 
296
370
  ```jsx
297
371
  const Input = tasty({
@@ -329,8 +403,8 @@ When a style property uses a state map, the merge behavior depends on whether th
329
403
  const MyButton = tasty(Button, {
330
404
  styles: {
331
405
  fill: {
332
- 'loading': '#yellow', // append new state
333
- 'disabled': '#gray.20', // override existing state in place
406
+ loading: '#yellow', // append new state
407
+ disabled: '#gray.20', // override existing state in place
334
408
  },
335
409
  },
336
410
  });
@@ -340,7 +414,7 @@ const MyButton = tasty(Button, {
340
414
  styles: {
341
415
  fill: {
342
416
  '': '#red',
343
- 'hovered': '#blue',
417
+ hovered: '#blue',
344
418
  },
345
419
  },
346
420
  });
@@ -365,8 +439,12 @@ fill: {
365
439
  Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
366
440
 
367
441
  ```jsx
368
- fill: { pressed: null } // removes pressed from the result
369
- fill: { disabled: false } // tombstone — no CSS for disabled, blocks recipe too
442
+ fill: {
443
+ pressed: null;
444
+ } // removes pressed from the result
445
+ fill: {
446
+ disabled: false;
447
+ } // tombstone — no CSS for disabled, blocks recipe too
370
448
  ```
371
449
 
372
450
  ### Resetting Properties with `null` and `false`
@@ -374,36 +452,36 @@ fill: { disabled: false } // tombstone — no CSS for disabled, blocks recipe to
374
452
  ```jsx
375
453
  const SimpleButton = tasty(Button, {
376
454
  styles: {
377
- fill: null, // discard parent's fill, let recipe fill in
455
+ fill: null, // discard parent's fill, let recipe fill in
378
456
  border: false, // no border at all (tombstone — blocks recipe too)
379
457
  },
380
458
  });
381
459
  ```
382
460
 
383
- | Value | Meaning | Recipe fills in? |
384
- |-------|---------|-----------------|
385
- | `undefined` | Not provided — parent preserved | N/A |
386
- | `null` | Intentional unset — parent discarded | Yes |
387
- | `false` | Tombstone — blocks everything | No |
461
+ | Value | Meaning | Recipe fills in? |
462
+ | ----------- | ------------------------------------ | ---------------- |
463
+ | `undefined` | Not provided — parent preserved | N/A |
464
+ | `null` | Intentional unset — parent discarded | Yes |
465
+ | `false` | Tombstone — blocks everything | No |
388
466
 
389
467
  ---
390
468
 
391
469
  ## Advanced States (`@` prefix)
392
470
 
393
- | Prefix | Purpose | Example |
394
- |--------|---------|---------|
395
- | `@media` | Media queries | `@media(w < 768px)` |
396
- | `@(...)` | Container queries | `@(panel, w >= 300px)` |
397
- | `@supports` | Feature/selector support | `@supports(display: grid)` |
398
- | `@root` | Root element states | `@root(schema=dark)` |
399
- | `@parent` | Parent/ancestor element states | `@parent(hovered)` |
400
- | `@own` | Sub-element's own state | `@own(hovered)` |
401
- | `@starting` | Entry animation | `@starting` |
402
- | `_` | Fallback floor (always-on, never negated) | `_` |
403
- | `:is()` | CSS `:is()` structural pseudo-class | `:is(fieldset > label)` |
404
- | `:has()` | CSS `:has()` relational pseudo-class | `:has(> Icon)` |
405
- | `:not()` | CSS `:not()` negation (prefer `!:is()`) | `:not(:first-child)` |
406
- | `:where()` | CSS `:where()` (zero specificity) | `:where(Section)` |
471
+ | Prefix | Purpose | Example |
472
+ | ----------- | ----------------------------------------- | -------------------------- |
473
+ | `@media` | Media queries | `@media(w < 768px)` |
474
+ | `@(...)` | Container queries | `@(panel, w >= 300px)` |
475
+ | `@supports` | Feature/selector support | `@supports(display: grid)` |
476
+ | `@root` | Root element states | `@root(schema=dark)` |
477
+ | `@parent` | Parent/ancestor element states | `@parent(hovered)` |
478
+ | `@own` | Sub-element's own state | `@own(hovered)` |
479
+ | `@starting` | Entry animation | `@starting` |
480
+ | `_` | Fallback floor (always-on, never negated) | `_` |
481
+ | `:is()` | CSS `:is()` structural pseudo-class | `:is(fieldset > label)` |
482
+ | `:has()` | CSS `:has()` relational pseudo-class | `:has(> Icon)` |
483
+ | `:not()` | CSS `:not()` negation (prefer `!:is()`) | `:not(:first-child)` |
484
+ | `:where()` | CSS `:where()` (zero specificity) | `:where(Section)` |
407
485
 
408
486
  > **Specificity.** All state selectors Tasty generates (modifiers, pseudo-classes,
409
487
  > `:is()`/`:not()` groups, and `@root` / `@parent` context) are wrapped in
@@ -419,9 +497,9 @@ const SimpleButton = tasty(Button, {
419
497
  Media queries support dimension shorthands and custom unit expansion:
420
498
 
421
499
  | Shorthand | Expands to |
422
- |-----------|------------|
423
- | `w` | `width` |
424
- | `h` | `height` |
500
+ | --------- | ---------- |
501
+ | `w` | `width` |
502
+ | `h` | `height` |
425
503
 
426
504
  ```jsx
427
505
  fill: {
@@ -432,14 +510,14 @@ fill: {
432
510
  }
433
511
  ```
434
512
 
435
- | Tasty syntax | CSS output |
436
- |--------------|------------|
437
- | `@media(w < 768px)` | `@media (width < 768px)` |
438
- | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
439
- | `@media:print` | `@media print` |
440
- | `@media:screen` | `@media screen` |
513
+ | Tasty syntax | CSS output |
514
+ | ------------------------------------ | ------------------------------------- |
515
+ | `@media(w < 768px)` | `@media (width < 768px)` |
516
+ | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
517
+ | `@media:print` | `@media print` |
518
+ | `@media:screen` | `@media screen` |
441
519
  | `@media(prefers-color-scheme: dark)` | `@media (prefers-color-scheme: dark)` |
442
- | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
520
+ | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
443
521
 
444
522
  Custom units work inside media queries: `@media(w < 40x)` → `@media (width < calc(var(--gap) * 40))`.
445
523
 
@@ -449,12 +527,12 @@ In practice, define state aliases via `configure({ states })` and use `@mobile`
449
527
 
450
528
  Container queries use the syntax `@(name, condition)` for named containers or `@(condition)` for the nearest ancestor container. Dimension shorthands (`w`, `h`, `is`, `bs`) are expanded the same way as `@media`.
451
529
 
452
- | Shorthand | Expands to |
453
- |-----------|------------|
454
- | `w` | `width` |
455
- | `h` | `height` |
456
- | `is` | `inline-size` |
457
- | `bs` | `block-size` |
530
+ | Shorthand | Expands to |
531
+ | --------- | ------------- |
532
+ | `w` | `width` |
533
+ | `h` | `height` |
534
+ | `is` | `inline-size` |
535
+ | `bs` | `block-size` |
458
536
 
459
537
  ```jsx
460
538
  const Panel = tasty({
@@ -467,26 +545,29 @@ const Panel = tasty({
467
545
  });
468
546
  ```
469
547
 
470
- | Tasty syntax | CSS output |
471
- |--------------|------------|
472
- | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
473
- | `@(w < 600px)` | `@container (width < 600px)` |
474
- | `@(layout, $variant=danger)` | `@container layout style(--variant: "danger")` |
475
- | `@(layout, $compact)` | `@container layout style(--compact)` |
476
- | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
477
- | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
548
+ | Tasty syntax | CSS output |
549
+ | ---------------------------------- | ---------------------------------------------- |
550
+ | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
551
+ | `@(w < 600px)` | `@container (width < 600px)` |
552
+ | `@(layout, $variant=danger)` | `@container layout style(--variant: "danger")` |
553
+ | `@(layout, $compact)` | `@container layout style(--compact)` |
554
+ | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
555
+ | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
478
556
 
479
557
  Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: "value")`.
480
558
 
481
559
  ### `@supports(...)` — Feature Queries
482
560
 
483
- Feature queries test CSS property support. Use `$` as the first argument to test selector support:
561
+ Feature queries test CSS property support and accept CSS support functions such
562
+ as `at-rule()`. Use `$` as the first argument to test selector support:
484
563
 
485
- | Tasty syntax | CSS output |
486
- |--------------|------------|
487
- | `@supports(display: grid)` | `@supports (display: grid)` |
488
- | `@supports($, :has(*))` | `@supports selector(:has(*))` |
489
- | `!@supports(display: grid)` | `@supports (not (display: grid))` |
564
+ | Tasty syntax | CSS output |
565
+ | ---------------------------- | ---------------------------------- |
566
+ | `@supports(display: grid)` | `@supports (display: grid)` |
567
+ | `@supports($, :has(*))` | `@supports selector(:has(*))` |
568
+ | `@supports(at-rule(@scope))` | `@supports (at-rule(@scope))` |
569
+ | `!@supports(display: grid)` | `@supports (not (display: grid))` |
570
+ | `!@supports(at-rule(@scope))` | `@supports (not (at-rule(@scope)))` |
490
571
 
491
572
  ```jsx
492
573
  display: {
@@ -495,6 +576,25 @@ display: {
495
576
  }
496
577
  ```
497
578
 
579
+ `at-rule()` tests whether the browser supports the named at-rule. Its surrounding
580
+ parentheses in Tasty's CSS output are valid CSS. Negation and combinations with
581
+ other states use the same syntax as property queries:
582
+
583
+ ```jsx
584
+ color: {
585
+ _: 'red',
586
+ '@supports(at-rule(@scope)) & @supports(display: grid)': 'blue',
587
+ }
588
+ ```
589
+
590
+ Use `_` to keep a base value when a browser cannot evaluate a query. A support
591
+ query only detects a browser feature; it does not add an API for authoring the
592
+ queried at-rule in a Tasty style object. For conditional stylesheet imports such
593
+ as `@import url("scoped.css") supports(at-rule(@scope));`, use an ordinary CSS
594
+ stylesheet or document-level `useRawCSS()`. Put imports before other rules in
595
+ that stylesheet. Imports cannot be used in constructable stylesheets, including
596
+ Tasty's adopted-sheet mode for Shadow DOM.
597
+
498
598
  ### `_` — Fallback Floor
499
599
 
500
600
  By default Tasty makes states **mutually exclusive**: a higher-priority state
@@ -504,7 +604,7 @@ that assumption: `@supports(...)` and `@(...)` queries can be **unknown** (not
504
604
  just true/false), and `not(unknown)` is also unknown — so a negated default
505
605
  branch silently never applies. The classic case is `scroll-state`: a browser can
506
606
  support `container-type: scroll-state` while a specific `scroll-state(...)` query
507
- is unknown, leaving *no* branch active.
607
+ is unknown, leaving _no_ branch active.
508
608
 
509
609
  The `_` fallback floor solves this. Use `_` as a **standalone key** and its value
510
610
  **always applies** as a guaranteed floor: it never receives negation, and
@@ -519,10 +619,14 @@ inset: {
519
619
  ```
520
620
 
521
621
  ```css
522
- .t0.t0 { inset: 0 ...; }
622
+ .t0.t0 {
623
+ inset: 0...;
624
+ }
523
625
  @container scroll-state(scrolled: block-end) {
524
626
  @supports (container-type: scroll-state) {
525
- .t0.t0 { inset: -80px ...; }
627
+ .t0.t0 {
628
+ inset: -80px...;
629
+ }
526
630
  }
527
631
  }
528
632
  ```
@@ -553,13 +657,13 @@ cause is always the same: **`''` is mutually exclusive (turned off by negation),
553
657
  while `_` is always on (layered underneath).**
554
658
 
555
659
  - **Unknown / invalid branches.** If a higher-priority branch sits behind a query
556
- that can be *unknown* (`@supports`, container, `scroll-state`) or uses a
660
+ that can be _unknown_ (`@supports`, container, `scroll-state`) or uses a
557
661
  selector the browser drops, the negated `''` default disappears along with it
558
662
  (`not(unknown) = unknown`), leaving the property with no rule. The `_` floor
559
663
  survives because it is an unconditional bare rule. This is the case `_` exists
560
664
  for.
561
665
 
562
- - **Empty / "unset" states.** A state can intentionally produce *no* output (an
666
+ - **Empty / "unset" states.** A state can intentionally produce _no_ output (an
563
667
  empty value) to leave a property unset while that state is active. With `''`
564
668
  this works — the default is negated away and nothing replaces it, so the
565
669
  property unsets. With `_` the floor can never be turned off, so its value keeps
@@ -571,14 +675,14 @@ while `_` is always on (layered underneath).**
571
675
  ```
572
676
 
573
677
  - **States with different output shapes.** Because `_` layers additively, when
574
- different states emit *different sets* of declarations, the floor's
678
+ different states emit _different sets_ of declarations, the floor's
575
679
  declarations bleed through wherever the winning state does not override the
576
680
  same property — which can produce inconsistent combinations. A `''` default is
577
681
  swapped out cleanly by its mutually-exclusive condition.
578
682
 
579
683
  Rule of thumb: **use `''` for the normal default** (clean mutual exclusivity,
580
684
  supports unsetting), and reach for `_` only when a value must survive an
581
- *unknown* higher-priority branch.
685
+ _unknown_ higher-priority branch.
582
686
 
583
687
  ### `@root(...)` — Root Element States
584
688
 
@@ -594,13 +698,13 @@ color: {
594
698
  }
595
699
  ```
596
700
 
597
- | Tasty syntax | CSS selector |
598
- |--------------|-------------|
599
- | `@root(schema=dark)` | `:root[data-schema="dark"]` |
600
- | `@root(hovered)` | `:root[data-hovered]` |
601
- | `@root(.premium-user)` | `:root.premium-user` |
602
- | `@root([lang="en"])` | `:root[lang="en"]` |
603
- | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
701
+ | Tasty syntax | CSS selector |
702
+ | ---------------------- | --------------------------------- |
703
+ | `@root(schema=dark)` | `:root[data-schema="dark"]` |
704
+ | `@root(hovered)` | `:root[data-hovered]` |
705
+ | `@root(.premium-user)` | `:root.premium-user` |
706
+ | `@root([lang="en"])` | `:root[lang="en"]` |
707
+ | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
604
708
 
605
709
  Root conditions are prepended to the component selector: `:root[data-schema="dark"] .t0.t0 { ... }`.
606
710
 
@@ -616,7 +720,7 @@ const Nav = tasty({
616
720
  '': '#text',
617
721
  '@own(:hover)': '#primary',
618
722
  '@own(:focus-visible)': '#primary',
619
- 'selected': '#primary', // root-level modifier
723
+ selected: '#primary', // root-level modifier
620
724
  },
621
725
  },
622
726
  },
@@ -624,11 +728,11 @@ const Nav = tasty({
624
728
  });
625
729
  ```
626
730
 
627
- | Tasty syntax (inside sub-element) | CSS output |
628
- |-----------------------------------|------------|
629
- | `@own(:hover)` | `:hover` on the sub-element selector |
630
- | `@own(hovered)` | `[data-hovered]` on the sub-element selector |
631
- | `@own(theme=dark)` | `[data-theme="dark"]` on the sub-element selector |
731
+ | Tasty syntax (inside sub-element) | CSS output |
732
+ | --------------------------------- | ------------------------------------------------- |
733
+ | `@own(:hover)` | `:hover` on the sub-element selector |
734
+ | `@own(hovered)` | `[data-hovered]` on the sub-element selector |
735
+ | `@own(theme=dark)` | `[data-theme="dark"]` on the sub-element selector |
632
736
 
633
737
  `@own()` is only valid inside sub-element styles. Using it on root styles emits a warning and is treated as a regular modifier.
634
738
 
@@ -641,14 +745,14 @@ const FadeIn = tasty({
641
745
  styles: {
642
746
  opacity: { '': '1', '@starting': '0' },
643
747
  transform: { '': 'scale(1)', '@starting': 'scale(0.95)' },
644
- transition: 'opacity 0.3s, translate 0.3s',
748
+ transition: 'opacity, translate',
645
749
  },
646
750
  });
647
751
  ```
648
752
 
649
- | Tasty syntax | CSS output |
650
- |--------------|------------|
651
- | `@starting` | `@starting-style { .t0.t0 { ... } }` |
753
+ | Tasty syntax | CSS output |
754
+ | ------------ | ------------------------------------ |
755
+ | `@starting` | `@starting-style { .t0.t0 { ... } }` |
652
756
 
653
757
  ### `@parent(...)` — Parent Element States
654
758
 
@@ -659,22 +763,22 @@ const Highlight = tasty({
659
763
  styles: {
660
764
  fill: {
661
765
  '': '#white',
662
- '@parent(hovered)': '#gray.05', // Any ancestor has [data-hovered]
663
- '@parent(theme=dark, >)': '#dark-02', // Direct parent has [data-theme="dark"]
766
+ '@parent(hovered)': '#gray.05', // Any ancestor has [data-hovered]
767
+ '@parent(theme=dark, >)': '#dark-02', // Direct parent has [data-theme="dark"]
664
768
  },
665
769
  },
666
770
  });
667
771
  ```
668
772
 
669
- | Syntax | CSS Output |
670
- |--------|------------|
671
- | `@parent(hovered)` | `:is([data-hovered] *)` |
672
- | `!@parent(hovered)` | `:not([data-hovered] *)` |
673
- | `@parent(hovered, >)` | `:is([data-hovered] > *)` (direct parent) |
674
- | `@parent(.active)` | `:is(.active *)` |
675
- | `@parent(hovered & focused)` | `:is([data-hovered][data-focused] *)` (same ancestor) |
773
+ | Syntax | CSS Output |
774
+ | ------------------------------------- | -------------------------------------------------------------------- |
775
+ | `@parent(hovered)` | `:is([data-hovered] *)` |
776
+ | `!@parent(hovered)` | `:not([data-hovered] *)` |
777
+ | `@parent(hovered, >)` | `:is([data-hovered] > *)` (direct parent) |
778
+ | `@parent(.active)` | `:is(.active *)` |
779
+ | `@parent(hovered & focused)` | `:is([data-hovered][data-focused] *)` (same ancestor) |
676
780
  | `@parent(hovered) & @parent(focused)` | `:is([data-hovered] *):is([data-focused] *)` (independent ancestors) |
677
- | `@parent(hovered \| focused)` | `:is([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
781
+ | `@parent(hovered \| focused)` | `:is([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
678
782
 
679
783
  For sub-elements, the parent check applies to the root element's ancestors:
680
784
 
@@ -705,38 +809,38 @@ const Card = tasty({
705
809
  styles: {
706
810
  display: {
707
811
  '': 'block',
708
- ':has(> Icon)': 'grid', // has Icon as direct child
709
- ':has(+ Icon)': 'grid', // immediately followed by an Icon sibling
710
- ':has(~ Icon)': 'grid', // has an Icon sibling somewhere after
711
- ':has(Icon +)': 'grid', // immediately preceded by an Icon sibling (auto-completes to `Icon + *`)
712
- ':has(Icon ~)': 'grid', // has an Icon sibling somewhere before (auto-completes to `Icon ~ *`)
713
- ':is(fieldset > label)': 'inline', // is a label inside a fieldset (HTML tags)
714
- '!:has(> Icon)': 'flex', // negation: no Icon child
812
+ ':has(> Icon)': 'grid', // has Icon as direct child
813
+ ':has(+ Icon)': 'grid', // immediately followed by an Icon sibling
814
+ ':has(~ Icon)': 'grid', // has an Icon sibling somewhere after
815
+ ':has(Icon +)': 'grid', // immediately preceded by an Icon sibling (auto-completes to `Icon + *`)
816
+ ':has(Icon ~)': 'grid', // has an Icon sibling somewhere before (auto-completes to `Icon ~ *`)
817
+ ':is(fieldset > label)': 'inline', // is a label inside a fieldset (HTML tags)
818
+ '!:has(> Icon)': 'flex', // negation: no Icon child
715
819
  },
716
820
  },
717
821
  });
718
822
  ```
719
823
 
720
- | Syntax | CSS Output | Meaning |
721
- |--------|------------|---------|
722
- | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
723
- | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
724
- | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
725
- | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
726
- | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
727
- | `:has(>)` | `:has(> *)` | Has any direct child |
728
- | `:is(> Field + input)` | `:is(> [data-element="Field"] + input)` | Structural match |
729
- | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
730
- | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
731
- | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
824
+ | Syntax | CSS Output | Meaning |
825
+ | ---------------------- | --------------------------------------- | --------------------------------------- |
826
+ | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
827
+ | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
828
+ | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
829
+ | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
830
+ | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
831
+ | `:has(>)` | `:has(> *)` | Has any direct child |
832
+ | `:is(> Field + input)` | `:is(> [data-element="Field"] + input)` | Structural match |
833
+ | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
834
+ | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
835
+ | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
732
836
 
733
837
  Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
734
838
 
735
839
  ```jsx
736
- ':has(> Icon) & hovered' // AND: structural + data attribute
737
- '@parent(hovered) & :has(> Icon)' // AND: parent check + structural
738
- ':has(> Icon) | :has(> Button)' // OR: either sub-element present
739
- ':has(> Icon) ^ :has(> Button)' // XOR: exactly one present
840
+ ':has(> Icon) & hovered'; // AND: structural + data attribute
841
+ '@parent(hovered) & :has(> Icon)'; // AND: parent check + structural
842
+ ':has(> Icon) | :has(> Button)'; // OR: either sub-element present
843
+ ':has(> Icon) ^ :has(> Button)'; // XOR: exactly one present
740
844
  ```
741
845
 
742
846
  > **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.
@@ -772,8 +876,8 @@ const AnimatedGradient = tasty({
772
876
  styles: {
773
877
  '$gradient-angle': '0deg',
774
878
  '#theme': 'okhst(280 80% 50%)',
775
- background: 'linear-gradient($gradient-angle, #theme, transparent)',
776
- transition: '$$gradient-angle 0.3s, ##theme 0.3s',
879
+ image: 'linear-gradient($gradient-angle, #theme, #clear)',
880
+ transition: '$$gradient-angle, ##theme',
777
881
  },
778
882
  });
779
883
  ```
@@ -803,7 +907,7 @@ const Heading = tasty({
803
907
  fontDisplay: 'swap',
804
908
  },
805
909
  },
806
- fontFamily: '"Brand Sans", sans-serif',
910
+ font: '"Brand Sans", sans-serif',
807
911
  },
808
912
  });
809
913
  ```
@@ -823,20 +927,20 @@ Supply an array to register several variants of the same family:
823
927
 
824
928
  ### Supported descriptors
825
929
 
826
- | Descriptor | CSS property | Type |
827
- |---|---|---|
828
- | `src` (required) | `src` | `string` |
829
- | `fontWeight` | `font-weight` | `string \| number` |
830
- | `fontStyle` | `font-style` | `string` |
831
- | `fontStretch` | `font-stretch` | `string` |
832
- | `fontDisplay` | `font-display` | `'auto' \| 'block' \| 'swap' \| 'fallback' \| 'optional'` |
833
- | `unicodeRange` | `unicode-range` | `string` |
834
- | `ascentOverride` | `ascent-override` | `string` |
835
- | `descentOverride` | `descent-override` | `string` |
836
- | `lineGapOverride` | `line-gap-override` | `string` |
837
- | `sizeAdjust` | `size-adjust` | `string` |
838
- | `fontFeatureSettings` | `font-feature-settings` | `string` |
839
- | `fontVariationSettings` | `font-variation-settings` | `string` |
930
+ | Descriptor | CSS property | Type |
931
+ | ----------------------- | ------------------------- | --------------------------------------------------------- |
932
+ | `src` (required) | `src` | `string` |
933
+ | `fontWeight` | `font-weight` | `string \| number` |
934
+ | `fontStyle` | `font-style` | `string` |
935
+ | `fontStretch` | `font-stretch` | `string` |
936
+ | `fontDisplay` | `font-display` | `'auto' \| 'block' \| 'swap' \| 'fallback' \| 'optional'` |
937
+ | `unicodeRange` | `unicode-range` | `string` |
938
+ | `ascentOverride` | `ascent-override` | `string` |
939
+ | `descentOverride` | `descent-override` | `string` |
940
+ | `lineGapOverride` | `line-gap-override` | `string` |
941
+ | `sizeAdjust` | `size-adjust` | `string` |
942
+ | `fontFeatureSettings` | `font-feature-settings` | `string` |
943
+ | `fontVariationSettings` | `font-variation-settings` | `string` |
840
944
 
841
945
  > Font-face rules are permanent — they are injected once and never cleaned up, matching how browsers handle `@font-face`.
842
946
 
@@ -864,18 +968,18 @@ const EmojiList = tasty({
864
968
 
865
969
  ### Supported descriptors
866
970
 
867
- | Descriptor | CSS property | Type |
868
- |---|---|---|
869
- | `system` (required) | `system` | `'cyclic' \| 'numeric' \| 'alphabetic' \| 'symbolic' \| 'additive' \| 'fixed' \| string` |
870
- | `symbols` | `symbols` | `string` |
871
- | `additiveSymbols` | `additive-symbols` | `string` |
872
- | `prefix` | `prefix` | `string` |
873
- | `suffix` | `suffix` | `string` |
874
- | `negative` | `negative` | `string` |
875
- | `range` | `range` | `string` |
876
- | `pad` | `pad` | `string` |
877
- | `fallback` | `fallback` | `string` |
878
- | `speakAs` | `speak-as` | `string` |
971
+ | Descriptor | CSS property | Type |
972
+ | ------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
973
+ | `system` (required) | `system` | `'cyclic' \| 'numeric' \| 'alphabetic' \| 'symbolic' \| 'additive' \| 'fixed' \| string` |
974
+ | `symbols` | `symbols` | `string` |
975
+ | `additiveSymbols` | `additive-symbols` | `string` |
976
+ | `prefix` | `prefix` | `string` |
977
+ | `suffix` | `suffix` | `string` |
978
+ | `negative` | `negative` | `string` |
979
+ | `range` | `range` | `string` |
980
+ | `pad` | `pad` | `string` |
981
+ | `fallback` | `fallback` | `string` |
982
+ | `speakAs` | `speak-as` | `string` |
879
983
 
880
984
  > Counter-style rules are permanent — they are injected once and never cleaned up, matching how browsers handle `@counter-style`.
881
985
 
@@ -897,18 +1001,18 @@ const Box = tasty({
897
1001
  styles: {
898
1002
  '@function': {
899
1003
  // simplest: one bare param + result (auto-calc, no explicit calc())
900
- '$$negative': { args: ['$value'], result: '(-1 * $value)' },
1004
+ $$negative: { args: ['$value'], result: '(-1 * $value)' },
901
1005
 
902
1006
  // typed param + default + return type + local variable
903
- '$$shadow': {
1007
+ $$shadow: {
904
1008
  args: { '$shadow-color': { syntax: '<color>', default: 'inherit' } },
905
1009
  returns: '<color>',
906
- '$offset': '2px', // local variable
1010
+ $offset: '2px', // local variable
907
1011
  result: '$offset $offset ($shadow-color, black)',
908
1012
  },
909
1013
  },
910
- marginTop: '$$negative(10px)', // → margin-top: --negative(10px)
911
- boxShadow: '$$shadow(#accent)', // → box-shadow: --shadow(var(--accent-color))
1014
+ margin: '$$negative(10px) top', // → margin: --negative(10px) 0 0 0
1015
+ shadow: '$$shadow(#accent)', // → box-shadow: --shadow(var(--accent-color))
912
1016
  },
913
1017
  });
914
1018
  ```
@@ -916,22 +1020,27 @@ const Box = tasty({
916
1020
  Generated CSS:
917
1021
 
918
1022
  ```css
919
- @function --negative(--value) { result: calc(-1 * var(--value)); }
1023
+ @function --negative(--value) {
1024
+ result: calc(-1 * var(--value));
1025
+ }
920
1026
  @function --shadow(--shadow-color <color>: inherit) returns <color> {
921
1027
  --offset: 2px;
922
1028
  result: var(--offset) var(--offset) var(--shadow-color, black);
923
1029
  }
924
- .t0 { margin-top: --negative(10px); box-shadow: --shadow(var(--accent-color)); }
1030
+ .t0 {
1031
+ margin: --negative(10px) 0 0 0;
1032
+ box-shadow: --shadow(var(--accent-color));
1033
+ }
925
1034
  ```
926
1035
 
927
1036
  ### Descriptor shape
928
1037
 
929
- | Field | Type | Notes |
930
- |---|---|---|
931
- | `result` (required) | `string` | The `result:` value, parsed through the Tasty DSL. |
932
- | `args` | `string[]` \| `Record<string, FunctionParameter>` | Ordered parameters. Array form lists bare names; object form maps names to type/default. |
933
- | `returns` | `string` | Optional return type, e.g. `'<color>'`. |
934
- | `$name` keys | `string \| number` | Any `$name` key declares a local variable `--name` (value parsed). |
1038
+ | Field | Type | Notes |
1039
+ | ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
1040
+ | `result` (required) | `string` | The `result:` value, parsed through the Tasty DSL. |
1041
+ | `args` | `string[]` \| `Record<string, FunctionParameter>` | Ordered parameters. Array form lists bare names; object form maps names to type/default. |
1042
+ | `returns` | `string` | Optional return type, e.g. `'<color>'`. |
1043
+ | `$name` keys | `string \| number` | Any `$name` key declares a local variable `--name` (value parsed). |
935
1044
 
936
1045
  A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
937
1046
 
@@ -953,4 +1062,4 @@ For a complete reference of all enhanced style properties — syntax, values, mo
953
1062
  - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
954
1063
  - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
955
1064
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
956
- - **[Zero Runtime (tastyStatic)](tasty-static.md)** — Build-time static styling with Babel plugin
1065
+ - **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin