@tenphi/tasty 0.0.0-snapshot.06c8a41 → 0.0.0-snapshot.07ea4a0

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 (132) hide show
  1. package/README.md +408 -385
  2. package/dist/babel-types-BKDTT4--.d.ts +18 -0
  3. package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
  4. package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
  5. package/dist/chunks/build-config-CW4k04_V.js +60 -0
  6. package/dist/chunks/build-config-CW4k04_V.js.map +1 -0
  7. package/dist/{collector-B3OsM252.js → chunks/collector-DKWQXEwN.js} +10 -127
  8. package/dist/chunks/collector-DKWQXEwN.js.map +1 -0
  9. package/dist/chunks/config-engine-DTglOA78.js +272 -0
  10. package/dist/chunks/config-engine-DTglOA78.js.map +1 -0
  11. package/dist/chunks/config-file-DlZXJMj8.js +25 -0
  12. package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
  13. package/dist/chunks/css-definitions-_C7C-daI.js +910 -0
  14. package/dist/chunks/css-definitions-_C7C-daI.js.map +1 -0
  15. package/dist/{css-resources-Cyl_axbI.js → chunks/css-resources-P7IBUUZs.js} +2 -9
  16. package/dist/chunks/css-resources-P7IBUUZs.js.map +1 -0
  17. package/dist/chunks/debug-BapJwy9w.js +502 -0
  18. package/dist/chunks/debug-BapJwy9w.js.map +1 -0
  19. package/dist/chunks/dsl-DYSJc1-T.js +1383 -0
  20. package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
  21. package/dist/chunks/hydration-TRcz5vnk.js +41 -0
  22. package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
  23. package/dist/chunks/react-runtime-DayjnPuC.js +689 -0
  24. package/dist/chunks/react-runtime-DayjnPuC.js.map +1 -0
  25. package/dist/chunks/runtime-engine-IwGbSMb4.js +2432 -0
  26. package/dist/chunks/runtime-engine-IwGbSMb4.js.map +1 -0
  27. package/dist/chunks/runtime-state-CVrL92zX.js +152 -0
  28. package/dist/chunks/runtime-state-CVrL92zX.js.map +1 -0
  29. package/dist/{merge-styles-DuoZEsm9.js → chunks/shared-utils-y-HjvPN4.js} +60 -55
  30. package/dist/chunks/shared-utils-y-HjvPN4.js.map +1 -0
  31. package/dist/chunks/style-engine-CVmNIjow.js +5521 -0
  32. package/dist/chunks/style-engine-CVmNIjow.js.map +1 -0
  33. package/dist/{css-writer-Bgowobbx.js → chunks/zero-engine-igHhzYM6.js} +65 -145
  34. package/dist/chunks/zero-engine-igHhzYM6.js.map +1 -0
  35. package/dist/{collector-BTVXj2hS.d.ts → collector-CTU-nl0g.d.ts} +6 -5
  36. package/dist/{config-LfIDmVHx.d.ts → config-CcxqSZI2.d.ts} +45 -40
  37. package/dist/core/index.d.ts +5 -5
  38. package/dist/core/index.js +9 -6
  39. package/dist/hydrate-CRyjjAyl.d.ts +22 -0
  40. package/dist/{index-Bm9ltvdH.d.ts → index--GdqCPlp.d.ts} +271 -242
  41. package/dist/index.d.ts +5 -5
  42. package/dist/index.js +11 -922
  43. package/dist/{merge-styles-BcLl1u_d.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -3
  44. package/dist/ssr/astro-client.d.ts +1 -1
  45. package/dist/ssr/astro-client.js +1 -14
  46. package/dist/ssr/astro-client.js.map +1 -1
  47. package/dist/ssr/astro-middleware-extract-static.d.ts +1 -2
  48. package/dist/ssr/astro-middleware-extract-static.js +1 -1
  49. package/dist/ssr/astro-middleware-extract.d.ts +1 -2
  50. package/dist/ssr/astro-middleware-extract.js +1 -1
  51. package/dist/ssr/astro-middleware-static.d.ts +1 -2
  52. package/dist/ssr/astro-middleware-static.js +1 -12
  53. package/dist/ssr/astro-middleware-static.js.map +1 -1
  54. package/dist/ssr/astro-middleware.d.ts +1 -2
  55. package/dist/ssr/astro-middleware.js +1 -13
  56. package/dist/ssr/astro-middleware.js.map +1 -1
  57. package/dist/ssr/astro.d.ts +8 -16
  58. package/dist/ssr/astro.js +220 -1
  59. package/dist/ssr/astro.js.map +1 -0
  60. package/dist/ssr/client.d.ts +2 -0
  61. package/dist/ssr/client.js +2 -0
  62. package/dist/ssr/index.d.ts +5 -25
  63. package/dist/ssr/index.js +5 -5
  64. package/dist/ssr/index.js.map +1 -1
  65. package/dist/ssr/next-config.d.ts +3 -5
  66. package/dist/ssr/next-config.js +43 -35
  67. package/dist/ssr/next-config.js.map +1 -1
  68. package/dist/ssr/next.d.ts +3 -9
  69. package/dist/ssr/next.js +7 -38
  70. package/dist/ssr/next.js.map +1 -1
  71. package/dist/static/index.d.ts +8 -9
  72. package/dist/static/index.js +4 -22
  73. package/dist/static/index.js.map +1 -1
  74. package/dist/static/inject.d.ts +1 -2
  75. package/dist/{index-BmogEzTo.d.ts → types-DJEM_BOJ.d.ts} +852 -860
  76. package/dist/zero/babel.d.ts +67 -2
  77. package/dist/zero/babel.js +77 -140
  78. package/dist/zero/babel.js.map +1 -1
  79. package/dist/zero/index.d.ts +62 -8
  80. package/dist/zero/index.js +24 -2
  81. package/dist/zero/index.js.map +1 -0
  82. package/dist/zero/next.d.ts +4 -5
  83. package/dist/zero/next.js +3 -38
  84. package/dist/zero/next.js.map +1 -1
  85. package/docs/README.md +4 -1
  86. package/docs/adoption.md +25 -17
  87. package/docs/ai-agents.md +66 -33
  88. package/docs/comparison.md +9 -8
  89. package/docs/configuration.md +192 -60
  90. package/docs/debug.md +5 -7
  91. package/docs/design-system.md +117 -15
  92. package/docs/dsl.md +204 -82
  93. package/docs/getting-started.md +81 -22
  94. package/docs/injector.md +41 -40
  95. package/docs/methodology.md +48 -23
  96. package/docs/migration-v3.md +3 -3
  97. package/docs/pipeline.md +54 -27
  98. package/docs/plugins.md +22 -42
  99. package/docs/react-api.md +72 -9
  100. package/docs/runtime-benchmarks.md +146 -91
  101. package/docs/ssr.md +92 -66
  102. package/docs/styles.md +117 -31
  103. package/docs/tasty-static.md +162 -58
  104. package/package.json +96 -38
  105. package/dist/astro-CeYENy2x.js +0 -301
  106. package/dist/astro-CeYENy2x.js.map +0 -1
  107. package/dist/async-storage-DKK-wTD4.js +0 -44
  108. package/dist/async-storage-DKK-wTD4.js.map +0 -1
  109. package/dist/babel-DWCsafYw.d.ts +0 -83
  110. package/dist/collector-B3OsM252.js.map +0 -1
  111. package/dist/config-B3gPdCqd.js +0 -12421
  112. package/dist/config-B3gPdCqd.js.map +0 -1
  113. package/dist/context-CA8YKeMn.js +0 -24
  114. package/dist/context-CA8YKeMn.js.map +0 -1
  115. package/dist/core-DGm0CFHP.js +0 -1732
  116. package/dist/core-DGm0CFHP.js.map +0 -1
  117. package/dist/css-resources-Cyl_axbI.js.map +0 -1
  118. package/dist/css-writer-Bgowobbx.js.map +0 -1
  119. package/dist/format-global-rules-DklyaXv-.js +0 -22
  120. package/dist/format-global-rules-DklyaXv-.js.map +0 -1
  121. package/dist/format-rules-XRw9u7d4.js +0 -104
  122. package/dist/format-rules-XRw9u7d4.js.map +0 -1
  123. package/dist/hydrate-CNOmZprz.js +0 -37
  124. package/dist/hydrate-CNOmZprz.js.map +0 -1
  125. package/dist/index.js.map +0 -1
  126. package/dist/keyframes-DE-OE76F.js +0 -535
  127. package/dist/keyframes-DE-OE76F.js.map +0 -1
  128. package/dist/merge-styles-DuoZEsm9.js.map +0 -1
  129. package/dist/resolve-recipes-H9NqOQuP.js +0 -144
  130. package/dist/resolve-recipes-H9NqOQuP.js.map +0 -1
  131. package/dist/ssr-collector-ref-COs_ioWl.js +0 -29
  132. package/dist/ssr-collector-ref-COs_ioWl.js.map +0 -1
package/docs/dsl.md CHANGED
@@ -6,6 +6,8 @@ The same value syntax, state maps, tokens, units, extension semantics, and speci
6
6
 
7
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).
8
8
 
9
+ To autocomplete your own token and preset names, see [TypeScript autocomplete](getting-started.md#typescript-autocomplete); to check state keys, tokens, units, and values at lint time, see [ESLint plugin](getting-started.md#eslint-plugin).
10
+
9
11
  ---
10
12
 
11
13
  ## Dictionary
@@ -18,6 +20,15 @@ Object where keys represent states and values are the styles to apply:
18
20
  fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
19
21
  ```
20
22
 
23
+ Define an alias for one component by adding an `'@name': '<state expression>'` key to its styles. It works like a global alias from [`configure({ states })`](configuration.md#options), scoped to that component:
24
+
25
+ ```jsx
26
+ styles: {
27
+ '@narrow': '@media(w < 600px)',
28
+ fill: { '': '#surface', '@narrow': '#surface-mobile' },
29
+ }
30
+ ```
31
+
21
32
  #### State Key Types
22
33
 
23
34
  | Syntax | Example | Generated CSS |
@@ -34,17 +45,20 @@ fill: { '': '#white', hovered: '#gray.05', 'theme=danger': '#red' }
34
45
 
35
46
  #### Automatic Native States
36
47
 
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:
48
+ `disabled`, `checked`, and `hidden` are built-in automatic states. A Tasty component sets `data-disabled`, `data-checked`, and `data-hidden` from the native `disabled`, `checked`, and `hidden` props (or `isDisabled`, `isChecked`, `isHidden`), so each pair below expresses the same component state:
38
49
 
39
- | Concise Tasty state | Explicit attribute selector |
40
- | ------------------- | --------------------------- |
41
- | `disabled` | `[disabled]` |
42
- | `checked` | `[checked]` |
50
+ | Concise Tasty state | Compiles to | Explicit attribute selector |
51
+ | ------------------- | ----------------- | --------------------------- |
52
+ | `disabled` | `[data-disabled]` | `[disabled]` |
53
+ | `checked` | `[data-checked]` | `[checked]` |
54
+ | `hidden` | `[data-hidden]` | `[hidden]` |
43
55
 
44
56
  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.
45
57
 
46
58
  Operator precedence (highest to lowest): `!` (NOT) > `^` (XOR) > `|` (OR) > `&` (AND). Use parentheses to override: `hovered & (pressed ^ focused)`.
47
59
 
60
+ A top-level comma is the same as `|`: `'hovered, focused'` is `'hovered | focused'`.
61
+
48
62
  `^` (XOR) means "exactly one of" — `A ^ B` expands to `(A & !B) | (!A & B)`. This is useful for mutually exclusive states where exactly one should be active:
49
63
 
50
64
  ```jsx
@@ -54,6 +68,8 @@ fill: {
54
68
  }
55
69
  ```
56
70
 
71
+ 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.
72
+
57
73
  #### Default State Ordering
58
74
 
59
75
  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.
@@ -74,9 +90,7 @@ Element styled using a capitalized key. Identified by `data-element` attribute:
74
90
 
75
91
  ```jsx
76
92
  styles: {
77
- Title: {
78
- preset: 'h3';
79
- }
93
+ Title: { preset: 'h3' },
80
94
  }
81
95
  // Targets: <div data-element="Title">
82
96
  ```
@@ -115,14 +129,16 @@ The `@` placeholder marks exactly where `[data-element="..."]` is injected, allo
115
129
  const List = tasty({
116
130
  styles: {
117
131
  Item: {
118
- $: '>@:last-child',
119
- border: 'none',
132
+ $: '>@::after',
133
+ content: '""',
120
134
  },
121
135
  },
122
136
  });
123
- // → .t0 > [data-element="Item"]:last-child { border: none }
137
+ // → .t1x29r0l.t1x29r0l > [data-element="Item"]::after { content: ""; }
124
138
  ```
125
139
 
140
+ Keep conditions such as `:last-child` out of `$` and write them as `@own(:last-child)` in a state map; `tasty/no-state-in-selector` warns otherwise. This works when `$` ends in a pseudo-element too: the condition attaches to the element the pseudo-element belongs to (see [`@own(...)`](#own--sub-elements-own-state)).
141
+
126
142
  ### Color Token
127
143
 
128
144
  Named color prefixed with `#` that maps to CSS custom properties. Supports opacity with `.N` suffix:
@@ -155,6 +171,10 @@ fill: '#current', // → currentcolor
155
171
  color: '(#primary, #secondary)', // Fallback syntax
156
172
  ```
157
173
 
174
+ `#white`, `#black`, `#border`, and `#outline` are registered by default (see [Default properties](#default-properties)), and `#clear` is a built-in alias for `transparent` (see [below](#clear-is-transparent)), so they work before you define any tokens.
175
+
176
+ A name that is also a hex color is read as a token with the literal as its fallback, in every color style: `color: '#ddd'` emits `var(--ddd-color, #ddd)`, and `#ddd.5` emits `oklch(from var(--ddd-color, #ddd) l c h / .5)`. A `#ddd` token, if you define one, wins over the literal.
177
+
158
178
  The suffix sets the alpha on the token's color with CSS relative color syntax:
159
179
 
160
180
  ```jsx
@@ -180,6 +200,17 @@ Two properties follow from writing the alpha slot rather than compositing:
180
200
  `/ var(--fade)` unchanged, so it works whether `$fade` holds `.5` or `50%` —
181
201
  which is what `--*-opacity` properties are registered to accept.
182
202
 
203
+ ### `#clear` is transparent
204
+
205
+ `#clear` is a built-in [replace token](#replace-tokens) for `transparent`.
206
+ Opacity suffixes are ignored: `#clear.5` and `#clear.$fade` both emit
207
+ `transparent`. The same rule applies to user-defined replace tokens resolving
208
+ to `transparent`, including aliases of `#clear`.
209
+
210
+ It does not read `--clear-color` or require an `@property` registration.
211
+ An explicit `configure({ replaceTokens: { '#clear': ... } })` overrides the
212
+ default alias and uses the replacement's usual opacity behavior.
213
+
183
214
  ### `#current` is the keyword
184
215
 
185
216
  `#current` emits the `currentcolor` keyword, so it resolves against the element
@@ -217,7 +248,7 @@ through `color-mix()`, which works from Safari 16.2 and composes like every othe
217
248
 
218
249
  ### `#current` composes instead
219
250
 
220
- `#current` is the one exception, and the difference is deliberate. A token _names_
251
+ `#current` composes alpha, and the difference is deliberate. A token _names_
221
252
  a color, so fading it sets its alpha. `currentcolor` is the color an element
222
253
  **inherits**, which an ancestor may already have faded — `#current.4` means "40%
223
254
  of what reaches me":
@@ -234,9 +265,10 @@ step. Because a `color-mix()` percentage cannot be a `<number>`, an opacity
234
265
  custom property used as `#current.$fade` must hold a unitless number; a token
235
266
  accepts either form.
236
267
 
237
- The space is always `oklch`: it is unbounded, so a wide-gamut color survives a
238
- round trip that a gamut-limited space would clamp. Nothing configures this —
239
- see [Color space](configuration.md#color-space).
268
+ Token fades always use `oklch(from …)`: it is unbounded, so a wide-gamut color
269
+ survives a round trip that a gamut-limited space would clamp. `#current` fades
270
+ mix `in oklab`. Nothing configures either — see
271
+ [Color space](configuration.md#color-space).
240
272
 
241
273
  ---
242
274
 
@@ -254,6 +286,9 @@ border: '1bw solid oklch(from #primary l c h / 50%)', // relative color syntax
254
286
  shadow: '0 0 1x color-mix(in oklab, #dark 20%, transparent)',
255
287
  ```
256
288
 
289
+ Tasty also ships `okhsl()` and `okhst()`, which compile to `rgb()` at parse
290
+ time: `fill: 'okhsl(280 80% 50%)'` → `rgb(40% 38.5% 88.2%)`.
291
+
257
292
  `light-dark()` is the one exception to "always a color": CSS lets it pick between
258
293
  values of any type, so it is treated as a color only when its arguments are
259
294
  colors. `padding: 'light-dark(1x, 2x)'` still reaches the padding slot.
@@ -291,16 +326,16 @@ fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
291
326
 
292
327
  ## Built-in Units
293
328
 
294
- | Unit | Description | Example | CSS Output |
295
- | ---- | ------------------ | ------- | ------------------------------- |
296
- | `x` | Gap multiplier | `2x` | `calc(var(--gap) * 2)` |
297
- | `r` | Border radius | `1r` | `var(--radius)` |
298
- | `cr` | Card border radius | `1cr` | `var(--card-radius)` |
299
- | `bw` | Border width | `2bw` | `calc(var(--border-width) * 2)` |
300
- | `ow` | Outline width | `1ow` | `var(--outline-width)` |
301
- | `sf` | Stable fraction | `1sf` | `minmax(0, 1fr)` |
329
+ | Unit | Description | Default | Example | CSS output |
330
+ | ---- | ------------------ | ---------------- | ------- | ---------------- |
331
+ | `x` | Spacing step | `8px` | `2x` | `16px` |
332
+ | `r` | Border radius | `6px` | `1r` | `6px` |
333
+ | `cr` | Card border radius | `10px` | `1cr` | `10px` |
334
+ | `bw` | Border width | `1px` | `2bw` | `2px` |
335
+ | `ow` | Outline width | `3px` | `1ow` | `3px` |
336
+ | `sf` | Stable fraction | `minmax(0, Nfr)` | `1sf` | `minmax(0, 1fr)` |
302
337
 
303
- You can register additional custom units via [`configure()`](configuration.md#options).
338
+ Units compile to plain values. Change the scale with `configure({ units: { x: '4px' } })`, or point a unit at a custom property when it must change at runtime: with `units: { x: 'var(--gap)' }`, `2x` compiles to `calc(2 * var(--gap))`. Setting a `$gap` or `$radius` token does not change the units. Register additional units the same way via [`configure()`](configuration.md#options).
304
339
 
305
340
  ---
306
341
 
@@ -341,6 +376,26 @@ const ElevatedCard = tasty({
341
376
  });
342
377
  ```
343
378
 
379
+ **Where recipes resolve:**
380
+
381
+ The `recipe` property is resolved on the component root and independently inside each sub-element's styles:
382
+
383
+ ```jsx
384
+ const CardWithPanel = tasty({
385
+ styles: {
386
+ recipe: 'card',
387
+ Panel: {
388
+ recipe: 'card elevated',
389
+ padding: '2x', // Overrides the panel's base recipe padding
390
+ },
391
+ },
392
+ });
393
+ ```
394
+
395
+ At each level, base recipes are collected in the order listed (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).
396
+
397
+ 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.
398
+
344
399
  **Post-merge recipes (`/` separator):**
345
400
 
346
401
  Recipes listed after `/` are applied _after_ component styles using `mergeStyles`:
@@ -417,12 +472,14 @@ fill: {
417
472
  Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
418
473
 
419
474
  ```jsx
420
- fill: {
421
- pressed: null;
422
- } // removes pressed from the result
423
- fill: {
424
- disabled: false;
425
- } // tombstone — no CSS for disabled, blocks recipe too
475
+ const MyButton = tasty(Button, {
476
+ styles: {
477
+ fill: {
478
+ hovered: null, // removes the parent's hovered state
479
+ disabled: false, // tombstone — no CSS for disabled, blocks recipe too
480
+ },
481
+ },
482
+ });
426
483
  ```
427
484
 
428
485
  ### Resetting Properties with `null` and `false`
@@ -472,7 +529,7 @@ const SimpleButton = tasty(Button, {
472
529
 
473
530
  ### `@media(...)` — Media Queries
474
531
 
475
- Media queries support dimension shorthands and custom unit expansion:
532
+ Media queries support dimension shorthands:
476
533
 
477
534
  | Shorthand | Expands to |
478
535
  | --------- | ---------- |
@@ -494,10 +551,17 @@ fill: {
494
551
  | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
495
552
  | `@media:print` | `@media print` |
496
553
  | `@media:screen` | `@media screen` |
554
+ | `@media:all` | `@media all` |
555
+ | `@media:speech` | `@media speech` |
497
556
  | `@media(prefers-color-scheme: dark)` | `@media (prefers-color-scheme: dark)` |
498
557
  | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
558
+ | `@media(aspect-ratio > 16/9)` | `@media (aspect-ratio > 16/9)` |
559
+
560
+ Tasty units resolve to their literal length inside query conditions, built-in and configured units alike: `@media(w < 40x)` emits `@media (width < 320px)`. A query condition cannot read a custom property, so a unit configured as one (`units: { x: 'var(--gap)' }`) cannot be used there; write that length in `px`, `em`, or `rem`.
499
561
 
500
- Custom units work inside media queries: `@media(w < 40x)` → `@media (width < calc(var(--gap) * 40))`.
562
+ Units are only converted in features that take a length: `width`, `height`, `inline-size`, and `block-size`, with or without `min-`/`max-` (`@media(min-width: 40x)` converts too). Every other feature keeps its value as written, so `@media(min-resolution: 2x)` and `@media(resolution >= 2x)` reach the browser unchanged: there `x` is CSS's own resolution unit, an alias of `dppx`.
563
+
564
+ When it combines or orders conditions, Tasty compares bounds only within one unit. `@media(w >= 600px) & @media(w < 40em)` keeps both bounds, because a query `em` follows the user's font size, so `600px` and `40em` cannot be ordered at build time.
501
565
 
502
566
  In practice, define state aliases via `configure({ states })` and use `@mobile` instead of writing the full query in every component.
503
567
 
@@ -523,26 +587,31 @@ const Panel = tasty({
523
587
  });
524
588
  ```
525
589
 
526
- | Tasty syntax | CSS output |
527
- | ---------------------------------- | ---------------------------------------------- |
528
- | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
529
- | `@(w < 600px)` | `@container (width < 600px)` |
530
- | `@(layout, $variant=danger)` | `@container layout style(--variant: "danger")` |
531
- | `@(layout, $compact)` | `@container layout style(--compact)` |
532
- | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
533
- | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
590
+ | Tasty syntax | CSS output |
591
+ | ---------------------------------- | -------------------------------------------- |
592
+ | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
593
+ | `@(w < 600px)` | `@container (width < 600px)` |
594
+ | `@(layout, $variant=danger)` | `@container layout style(--variant: danger)` |
595
+ | `@(layout, $compact)` | `@container layout style(--compact)` |
596
+ | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
597
+ | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
598
+
599
+ Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: value)`. The value is emitted unquoted, so it matches a custom property set to the identifier `danger`, not the string `"danger"`.
534
600
 
535
- Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: "value")`.
601
+ Other size features pass through as written: `@(aspect-ratio > 1)` → `@container (aspect-ratio > 1)`, `@(orientation: portrait)` → `@container (orientation: portrait)`. They need a container with `containerType: 'size'`, since an `inline-size` container has no block size to compare against.
536
602
 
537
603
  ### `@supports(...)` — Feature Queries
538
604
 
539
- Feature queries test CSS property support. Use `$` as the first argument to test selector support:
605
+ Feature queries test CSS property support and accept CSS support functions such
606
+ as `at-rule()`. Use `$` as the first argument to test selector support:
540
607
 
541
- | Tasty syntax | CSS output |
542
- | --------------------------- | --------------------------------- |
543
- | `@supports(display: grid)` | `@supports (display: grid)` |
544
- | `@supports($, :has(*))` | `@supports selector(:has(*))` |
545
- | `!@supports(display: grid)` | `@supports (not (display: grid))` |
608
+ | Tasty syntax | CSS output |
609
+ | ----------------------------- | ----------------------------------- |
610
+ | `@supports(display: grid)` | `@supports (display: grid)` |
611
+ | `@supports($, :has(*))` | `@supports selector(:has(*))` |
612
+ | `@supports(at-rule(@scope))` | `@supports (at-rule(@scope))` |
613
+ | `!@supports(display: grid)` | `@supports (not (display: grid))` |
614
+ | `!@supports(at-rule(@scope))` | `@supports (not (at-rule(@scope)))` |
546
615
 
547
616
  ```jsx
548
617
  display: {
@@ -551,6 +620,25 @@ display: {
551
620
  }
552
621
  ```
553
622
 
623
+ `at-rule()` tests whether the browser supports the named at-rule. Its surrounding
624
+ parentheses in Tasty's CSS output are valid CSS. Negation and combinations with
625
+ other states use the same syntax as property queries:
626
+
627
+ ```jsx
628
+ color: {
629
+ _: 'red',
630
+ '@supports(at-rule(@scope)) & @supports(display: grid)': 'blue',
631
+ }
632
+ ```
633
+
634
+ Use `_` to keep a base value when a browser cannot evaluate a query. A support
635
+ query only detects a browser feature; it does not add an API for authoring the
636
+ queried at-rule in a Tasty style object. For conditional stylesheet imports such
637
+ as `@import url("scoped.css") supports(at-rule(@scope));`, use an ordinary CSS
638
+ stylesheet or document-level `useRawCSS()`. Put imports before other rules in
639
+ that stylesheet. Imports cannot be used in constructable stylesheets, including
640
+ Tasty's adopted-sheet mode for Shadow DOM.
641
+
554
642
  ### `_` — Fallback Floor
555
643
 
556
644
  By default Tasty makes states **mutually exclusive**: a higher-priority state
@@ -576,12 +664,12 @@ inset: {
576
664
 
577
665
  ```css
578
666
  .t0.t0 {
579
- inset: 0...;
667
+ inset: 0 auto auto auto;
580
668
  }
581
- @container scroll-state(scrolled: block-end) {
582
- @supports (container-type: scroll-state) {
669
+ @supports (container-type: scroll-state) {
670
+ @container scroll-state(scrolled: block-end) {
583
671
  .t0.t0 {
584
- inset: -80px...;
672
+ inset: -640px auto auto auto;
585
673
  }
586
674
  }
587
675
  }
@@ -590,7 +678,7 @@ inset: {
590
678
  The floor is emitted as a bare rule (no negated `@supports` / container branches),
591
679
  so it always applies. When the override matches it wins because it is emitted
592
680
  later and all state selectors share the same specificity (see the note on
593
- `:where()` below).
681
+ `:where()` above).
594
682
 
595
683
  Notes:
596
684
 
@@ -662,7 +750,7 @@ color: {
662
750
  | `@root([lang="en"])` | `:root[lang="en"]` |
663
751
  | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
664
752
 
665
- Root conditions are prepended to the component selector: `:root[data-schema="dark"] .t0.t0 { ... }`.
753
+ Root conditions are prepended to the component selector inside `:where()`: `:where(:root[data-schema="dark"]) .t0.t0 { ... }`.
666
754
 
667
755
  ### `@own(...)` — Sub-element's Own State
668
756
 
@@ -692,6 +780,18 @@ const Nav = tasty({
692
780
 
693
781
  `@own()` is only valid inside sub-element styles. Using it on root styles emits a warning and is treated as a regular modifier.
694
782
 
783
+ When `$` ends in a pseudo-element, the condition attaches to the originating element, before the pseudo-element. CSS accepts no `:where()` after a pseudo-element:
784
+
785
+ ```jsx
786
+ Icon: {
787
+ $: 'a::before',
788
+ content: { '': 'none', '@own([rel="next"])': '"→"' },
789
+ },
790
+ // → .t0.t0 a:where([rel="next"])::before { content: "→"; }
791
+ ```
792
+
793
+ This covers every pseudo-element form: `::before`, `::marker`, `::backdrop`, vendor-prefixed ones such as `::-webkit-details-marker`, functional ones such as `::part(label)`, and the legacy `:before`, `:after`, `:first-line`, `:first-letter`. In a `$` list, each item gets the condition before its own pseudo-element. A pseudo-class written after the pseudo-element in `$`, as in `'@::part(label):hover'`, qualifies the pseudo-element and stays after it.
794
+
695
795
  ### `@starting` — Entry Animation
696
796
 
697
797
  Wraps the rule in `@starting-style`, enabling CSS entry animations for elements as they appear in the DOM:
@@ -701,18 +801,18 @@ const FadeIn = tasty({
701
801
  styles: {
702
802
  opacity: { '': '1', '@starting': '0' },
703
803
  transform: { '': 'scale(1)', '@starting': 'scale(0.95)' },
704
- transition: 'opacity 0.3s, translate 0.3s',
804
+ transition: 'opacity, translate',
705
805
  },
706
806
  });
707
807
  ```
708
808
 
709
809
  | Tasty syntax | CSS output |
710
810
  | ------------ | ------------------------------------ |
711
- | `@starting` | `@starting-style { .t0.t0 { ... } }` |
811
+ | `@starting` | `.t0.t0 { @starting-style { ... } }` |
712
812
 
713
813
  ### `@parent(...)` — Parent Element States
714
814
 
715
- Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
815
+ Style based on ancestor element attributes. Uses `:where([selector] *)` / `:where(:not([selector] *))` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
716
816
 
717
817
  ```jsx
718
818
  const Highlight = tasty({
@@ -726,15 +826,15 @@ const Highlight = tasty({
726
826
  });
727
827
  ```
728
828
 
729
- | Syntax | CSS Output |
730
- | ------------------------------------- | -------------------------------------------------------------------- |
731
- | `@parent(hovered)` | `:is([data-hovered] *)` |
732
- | `!@parent(hovered)` | `:not([data-hovered] *)` |
733
- | `@parent(hovered, >)` | `:is([data-hovered] > *)` (direct parent) |
734
- | `@parent(.active)` | `:is(.active *)` |
735
- | `@parent(hovered & focused)` | `:is([data-hovered][data-focused] *)` (same ancestor) |
736
- | `@parent(hovered) & @parent(focused)` | `:is([data-hovered] *):is([data-focused] *)` (independent ancestors) |
737
- | `@parent(hovered \| focused)` | `:is([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
829
+ | Syntax | CSS Output |
830
+ | ------------------------------------- | -------------------------------------------------------------------------- |
831
+ | `@parent(hovered)` | `:where([data-hovered] *)` |
832
+ | `!@parent(hovered)` | `:where(:not([data-hovered] *))` |
833
+ | `@parent(hovered, >)` | `:where([data-hovered] > *)` (direct parent) |
834
+ | `@parent(.active)` | `:where(.active *)` |
835
+ | `@parent(hovered & focused)` | `:where([data-hovered][data-focused] *)` (same ancestor) |
836
+ | `@parent(hovered) & @parent(focused)` | `:where([data-hovered] *):where([data-focused] *)` (independent ancestors) |
837
+ | `@parent(hovered \| focused)` | `:where([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
738
838
 
739
839
  For sub-elements, the parent check applies to the root element's ancestors:
740
840
 
@@ -749,12 +849,12 @@ const Card = tasty({
749
849
  },
750
850
  },
751
851
  });
752
- // → .t0.t0:is([data-hovered] *) [data-element="Label"]
852
+ // → .t0.t0:where([data-hovered] *) [data-element="Label"]
753
853
  ```
754
854
 
755
855
  ### `:is()`, `:has()` — CSS Structural Pseudo-classes
756
856
 
757
- Use CSS structural pseudo-classes directly in state keys. Capitalized words become `[data-element="..."]` selectors; lowercase words are HTML tags. A trailing combinator (`>`, `+`, `~`) is auto-completed with `*`.
857
+ Use CSS structural pseudo-classes directly in state keys. Capitalized words become `[data-element="..."]` selectors; lowercase words are HTML tags. A trailing combinator (`>`, `+`, `~`) is auto-completed with `*`. Leading combinators (`> Icon`, `+ Icon`) are relative selectors and are valid only inside `:has()`.
758
858
 
759
859
  `:where()` and `:not()` are also supported but rarely needed — use `:is()` and `!` negation instead.
760
860
 
@@ -777,18 +877,18 @@ const Card = tasty({
777
877
  });
778
878
  ```
779
879
 
780
- | Syntax | CSS Output | Meaning |
781
- | ---------------------- | --------------------------------------- | --------------------------------------- |
782
- | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
783
- | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
784
- | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
785
- | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
786
- | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
787
- | `:has(>)` | `:has(> *)` | Has any direct child |
788
- | `:is(> Field + input)` | `:is(> [data-element="Field"] + input)` | Structural match |
789
- | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
790
- | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
791
- | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
880
+ | Syntax | CSS Output | Meaning |
881
+ | -------------------- | ------------------------------------- | --------------------------------------- |
882
+ | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
883
+ | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
884
+ | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
885
+ | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
886
+ | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
887
+ | `:has(>)` | `:has(> *)` | Has any direct child |
888
+ | `:is(Field + input)` | `:is([data-element="Field"] + input)` | Is an input right after a Field sibling |
889
+ | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
890
+ | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
891
+ | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
792
892
 
793
893
  Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
794
894
 
@@ -821,6 +921,8 @@ const Pulse = tasty({
821
921
  });
822
922
  ```
823
923
 
924
+ Keyframes names are case-sensitive and keep their authored case in `animation` and `animationName` (`fadeIn`, not `fadein`), so they match `@keyframes` defined here, in `configure({ keyframes })`, or in plain CSS. Everything else in the value — durations, timing functions, keywords — is parsed as usual.
925
+
824
926
  ---
825
927
 
826
928
  ## Properties (`@property`)
@@ -833,7 +935,7 @@ const AnimatedGradient = tasty({
833
935
  '$gradient-angle': '0deg',
834
936
  '#theme': 'okhst(280 80% 50%)',
835
937
  image: 'linear-gradient($gradient-angle, #theme, #clear)',
836
- transition: '$$gradient-angle 0.3s, ##theme 0.3s',
938
+ transition: '$$gradient-angle, ##theme',
837
939
  },
838
940
  });
839
941
  ```
@@ -848,6 +950,26 @@ Use explicit `@property` when you need non-default settings like `inherits: fals
848
950
  },
849
951
  ```
850
952
 
953
+ ### Default properties
954
+
955
+ Tasty registers these properties itself. Style handlers read their defaults from them rather than repeating the values inline, so styles work before you define any tokens, and defining a token replaces the initial value.
956
+
957
+ | Token | Initial value | Read by |
958
+ | ---------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- |
959
+ | `#current` | `currentcolor` | anything reading `$current-color`, which `color` sets |
960
+ | `#white`, `#black` | `rgb(255 255 255)`, `rgb(0 0 0)` | your styles |
961
+ | `#border` | `currentcolor` | `border` with no color |
962
+ | `#outline` | `currentcolor` | `outline` with no color |
963
+ | `$gap`, `$radius`, `$border-width`, `$outline-width` | `8px`, `6px`, `1px`, `3px` | units you point at them, as in `units: { x: 'var(--gap)' }` |
964
+ | `$transition` | `80ms` | `transition` with no duration |
965
+ | `$sharp-radius` | `0px` | the sharp corners of `radius: 'leaf'` and `radius: 'backleaf'` |
966
+ | `$bold-font-weight` | `700` | `preset: 'name / strong'` |
967
+ | `$font-sans-fallback`, `$font-mono-fallback` | a `system-ui` and a `ui-monospace` stack | `font` and presets, after `$font-sans` / `$font-mono` |
968
+
969
+ Every rendering path emits them: the client injector, the SSR collector (`TastyRegistry`, `tastyIntegration()`, `runWithCollector()`), the `withTastyNext()` shared stylesheet, and the Babel plugin. CSS written with the programmatic API needs [`extractConfigProperties()`](tasty-static.md#programmatic-api), once per stylesheet. Server rendering without a collector, such as Astro without `tastyIntegration()`, emits none of them.
970
+
971
+ The initial values need `@property` support: Chrome and Edge 85+, Firefox 128+, Safari 16.4+. In older engines a handler that reads one of these tokens resolves to nothing unless your tokens define it: a `border` with no color draws no border, and a `transition` with no duration does not animate. To support those engines, define the tokens your styles rely on in `configure({ tokens })`.
972
+
851
973
  ---
852
974
 
853
975
  ## Font Face (`@font-face`)
@@ -908,7 +1030,7 @@ Define custom list markers via the CSS `@counter-style` at-rule. Keys are counte
908
1030
 
909
1031
  ```ts
910
1032
  const EmojiList = tasty({
911
- tag: 'ol',
1033
+ as: 'ol',
912
1034
  styles: {
913
1035
  '@counter-style': {
914
1036
  thumbs: {
@@ -1000,7 +1122,7 @@ Generated CSS:
1000
1122
 
1001
1123
  A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
1002
1124
 
1003
- Functions can also be registered with the [`useFunction`](react-api.md#usefunction) hook or globally via [`configure({ functions })`](configuration.md#functions) (the same map also accepts bare-key parse functions).
1125
+ Functions can also be registered with the [`useFunction`](react-api.md#usefunction) style function or globally via [`configure({ functions })`](configuration.md#functions) (the same map also accepts bare-key parse functions).
1004
1126
 
1005
1127
  > `@function` is permanent — injected once and never cleaned up, like `@counter-style`. It is an experimental CSS feature ([Chrome 139+](https://developer.mozilla.org/en-US/docs/Web/CSS/@function)); unsupported browsers safely ignore the native rule, so use it as progressive enhancement — or enable the [`configure({ polyfills: { functions: true } })`](configuration.md#polyfills) inline polyfill to expand `$$name(...)` calls into plain CSS for full cross-browser support.
1006
1128