@tenphi/tasty 3.9.11 → 3.11.0

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 (92) hide show
  1. package/README.md +399 -380
  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-5d8WlQbq.js +60 -0
  6. package/dist/chunks/build-config-5d8WlQbq.js.map +1 -0
  7. package/dist/chunks/{collector-D2ARGT2L.js → collector-CjCak4iE.js} +8 -6
  8. package/dist/chunks/collector-CjCak4iE.js.map +1 -0
  9. package/dist/chunks/{config-engine-DEmmPPdp.js → config-engine-uOFYZidB.js} +4 -4
  10. package/dist/chunks/{config-engine-DEmmPPdp.js.map → config-engine-uOFYZidB.js.map} +1 -1
  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-DotDkIXt.js → css-definitions-ubrHP97m.js} +130 -96
  14. package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
  15. package/dist/chunks/{debug-D8pg9RGI.js → debug-CJ31U_WE.js} +6 -6
  16. package/dist/chunks/{debug-D8pg9RGI.js.map → debug-CJ31U_WE.js.map} +1 -1
  17. package/dist/chunks/{dsl-DRhFWQzo.js → dsl-DYSJc1-T.js} +11 -6
  18. package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
  19. package/dist/chunks/{react-runtime-BpxlOTbO.js → react-runtime-CyyCI02N.js} +190 -63
  20. package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
  21. package/dist/chunks/{runtime-engine-CKFXUH-A.js → runtime-engine-BQmOu5-W.js} +69 -53
  22. package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
  23. package/dist/chunks/{runtime-state-BxaLhVdP.js → runtime-state-DEsyUMU-.js} +28 -3
  24. package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
  25. package/dist/chunks/{style-engine-BBbBuSkT.js → style-engine-BX2LshDZ.js} +309 -103
  26. package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
  27. package/dist/chunks/{zero-engine-gJrKBRiw.js → zero-engine-Dw0P4yGM.js} +62 -27
  28. package/dist/chunks/zero-engine-Dw0P4yGM.js.map +1 -0
  29. package/dist/{collector-w4mBj5gd.d.ts → collector-CTU-nl0g.d.ts} +2 -2
  30. package/dist/{config-CZWCzBr6.d.ts → config-CcxqSZI2.d.ts} +35 -16
  31. package/dist/core/index.d.ts +4 -4
  32. package/dist/core/index.js +7 -7
  33. package/dist/hydrate-CRyjjAyl.d.ts +22 -0
  34. package/dist/{index-G9JuwM9p.d.ts → index-xtey-KDu.d.ts} +43 -13
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.js +8 -8
  37. package/dist/{merge-styles-VXwJ0Frr.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -2
  38. package/dist/ssr/astro.js +4 -4
  39. package/dist/ssr/client.d.ts +2 -0
  40. package/dist/ssr/client.js +2 -0
  41. package/dist/ssr/index.d.ts +3 -22
  42. package/dist/ssr/index.js +4 -4
  43. package/dist/ssr/index.js.map +1 -1
  44. package/dist/ssr/next-config.d.ts +1 -1
  45. package/dist/ssr/next-config.js +13 -18
  46. package/dist/ssr/next-config.js.map +1 -1
  47. package/dist/ssr/next.d.ts +1 -1
  48. package/dist/ssr/next.js +3 -3
  49. package/dist/static/index.d.ts +2 -2
  50. package/dist/{types-JEY8PVYP.d.ts → types-DJEM_BOJ.d.ts} +6 -6
  51. package/dist/zero/babel.d.ts +67 -2
  52. package/dist/zero/babel.js +47 -44
  53. package/dist/zero/babel.js.map +1 -1
  54. package/dist/zero/index.d.ts +58 -3
  55. package/dist/zero/index.js +24 -2
  56. package/dist/zero/index.js.map +1 -0
  57. package/dist/zero/next.d.ts +2 -1
  58. package/dist/zero/next.js +3 -7
  59. package/dist/zero/next.js.map +1 -1
  60. package/docs/README.md +4 -1
  61. package/docs/adoption.md +25 -17
  62. package/docs/ai-agents.md +37 -34
  63. package/docs/comparison.md +9 -8
  64. package/docs/configuration.md +174 -59
  65. package/docs/debug.md +3 -2
  66. package/docs/design-system.md +117 -15
  67. package/docs/dsl.md +122 -81
  68. package/docs/getting-started.md +81 -24
  69. package/docs/injector.md +39 -40
  70. package/docs/methodology.md +45 -20
  71. package/docs/migration-v3.md +2 -2
  72. package/docs/pipeline.md +20 -17
  73. package/docs/plugins.md +20 -40
  74. package/docs/react-api.md +28 -8
  75. package/docs/runtime-benchmarks.md +4 -4
  76. package/docs/ssr.md +83 -63
  77. package/docs/styles.md +27 -17
  78. package/docs/tasty-static.md +142 -55
  79. package/package.json +24 -10
  80. package/dist/babel-rZcEwPLM.d.ts +0 -82
  81. package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
  82. package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
  83. package/dist/chunks/build-config-CL4eRu2z.js +0 -42
  84. package/dist/chunks/build-config-CL4eRu2z.js.map +0 -1
  85. package/dist/chunks/collector-D2ARGT2L.js.map +0 -1
  86. package/dist/chunks/css-definitions-DotDkIXt.js.map +0 -1
  87. package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
  88. package/dist/chunks/react-runtime-BpxlOTbO.js.map +0 -1
  89. package/dist/chunks/runtime-engine-CKFXUH-A.js.map +0 -1
  90. package/dist/chunks/runtime-state-BxaLhVdP.js.map +0 -1
  91. package/dist/chunks/style-engine-BBbBuSkT.js.map +0 -1
  92. package/dist/chunks/zero-engine-gJrKBRiw.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
@@ -76,9 +90,7 @@ Element styled using a capitalized key. Identified by `data-element` attribute:
76
90
 
77
91
  ```jsx
78
92
  styles: {
79
- Title: {
80
- preset: 'h3';
81
- }
93
+ Title: { preset: 'h3' },
82
94
  }
83
95
  // Targets: <div data-element="Title">
84
96
  ```
@@ -117,14 +129,16 @@ The `@` placeholder marks exactly where `[data-element="..."]` is injected, allo
117
129
  const List = tasty({
118
130
  styles: {
119
131
  Item: {
120
- $: '>@:last-child',
121
- border: 'none',
132
+ $: '>@::after',
133
+ content: '""',
122
134
  },
123
135
  },
124
136
  });
125
- // → .t0 > [data-element="Item"]:last-child { border: none }
137
+ // → .t0.t0 > [data-element="Item"]::after { content: "" }
126
138
  ```
127
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.
141
+
128
142
  ### Color Token
129
143
 
130
144
  Named color prefixed with `#` that maps to CSS custom properties. Supports opacity with `.N` suffix:
@@ -157,6 +171,8 @@ fill: '#current', // → currentcolor
157
171
  color: '(#primary, #secondary)', // Fallback syntax
158
172
  ```
159
173
 
174
+ `#white`, `#black`, `#border`, and `#outline` are registered by default, and `#clear` is a built-in alias for `transparent` (see [below](#clear-is-transparent)), so they work before you define any tokens.
175
+
160
176
  The suffix sets the alpha on the token's color with CSS relative color syntax:
161
177
 
162
178
  ```jsx
@@ -182,6 +198,17 @@ Two properties follow from writing the alpha slot rather than compositing:
182
198
  `/ var(--fade)` unchanged, so it works whether `$fade` holds `.5` or `50%` —
183
199
  which is what `--*-opacity` properties are registered to accept.
184
200
 
201
+ ### `#clear` is transparent
202
+
203
+ `#clear` is a built-in [replace token](#replace-tokens) for `transparent`.
204
+ Opacity suffixes are ignored: `#clear.5` and `#clear.$fade` both emit
205
+ `transparent`. The same rule applies to user-defined replace tokens resolving
206
+ to `transparent`, including aliases of `#clear`.
207
+
208
+ It does not read `--clear-color` or require an `@property` registration.
209
+ An explicit `configure({ replaceTokens: { '#clear': ... } })` overrides the
210
+ default alias and uses the replacement's usual opacity behavior.
211
+
185
212
  ### `#current` is the keyword
186
213
 
187
214
  `#current` emits the `currentcolor` keyword, so it resolves against the element
@@ -219,7 +246,7 @@ through `color-mix()`, which works from Safari 16.2 and composes like every othe
219
246
 
220
247
  ### `#current` composes instead
221
248
 
222
- `#current` is the one exception, and the difference is deliberate. A token _names_
249
+ `#current` composes alpha, and the difference is deliberate. A token _names_
223
250
  a color, so fading it sets its alpha. `currentcolor` is the color an element
224
251
  **inherits**, which an ancestor may already have faded — `#current.4` means "40%
225
252
  of what reaches me":
@@ -236,9 +263,10 @@ step. Because a `color-mix()` percentage cannot be a `<number>`, an opacity
236
263
  custom property used as `#current.$fade` must hold a unitless number; a token
237
264
  accepts either form.
238
265
 
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).
266
+ Token fades always use `oklch(from …)`: it is unbounded, so a wide-gamut color
267
+ survives a round trip that a gamut-limited space would clamp. `#current` fades
268
+ mix `in oklab`. Nothing configures either — see
269
+ [Color space](configuration.md#color-space).
242
270
 
243
271
  ---
244
272
 
@@ -256,6 +284,9 @@ border: '1bw solid oklch(from #primary l c h / 50%)', // relative color syntax
256
284
  shadow: '0 0 1x color-mix(in oklab, #dark 20%, transparent)',
257
285
  ```
258
286
 
287
+ Tasty also ships `okhsl()` and `okhst()`, which compile to `rgb()` at parse
288
+ time: `fill: 'okhsl(280 80% 50%)'` → `rgb(40% 38.5% 88.2%)`.
289
+
259
290
  `light-dark()` is the one exception to "always a color": CSS lets it pick between
260
291
  values of any type, so it is treated as a color only when its arguments are
261
292
  colors. `padding: 'light-dark(1x, 2x)'` still reaches the padding slot.
@@ -293,16 +324,16 @@ fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
293
324
 
294
325
  ## Built-in Units
295
326
 
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)` |
327
+ | Unit | Description | Default | Example | CSS output |
328
+ | ---- | ------------------ | ---------------- | ------- | ---------------- |
329
+ | `x` | Spacing step | `8px` | `2x` | `16px` |
330
+ | `r` | Border radius | `6px` | `1r` | `6px` |
331
+ | `cr` | Card border radius | `10px` | `1cr` | `10px` |
332
+ | `bw` | Border width | `1px` | `2bw` | `2px` |
333
+ | `ow` | Outline width | `3px` | `1ow` | `3px` |
334
+ | `sf` | Stable fraction | `minmax(0, Nfr)` | `1sf` | `minmax(0, 1fr)` |
304
335
 
305
- You can register additional custom units via [`configure()`](configuration.md#options).
336
+ 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).
306
337
 
307
338
  ---
308
339
 
@@ -359,7 +390,7 @@ const CardWithPanel = tasty({
359
390
  });
360
391
  ```
361
392
 
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).
393
+ 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).
363
394
 
364
395
  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
396
 
@@ -439,12 +470,14 @@ fill: {
439
470
  Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
440
471
 
441
472
  ```jsx
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
473
+ const MyButton = tasty(Button, {
474
+ styles: {
475
+ fill: {
476
+ hovered: null, // removes the parent's hovered state
477
+ disabled: false, // tombstone — no CSS for disabled, blocks recipe too
478
+ },
479
+ },
480
+ });
448
481
  ```
449
482
 
450
483
  ### Resetting Properties with `null` and `false`
@@ -494,7 +527,7 @@ const SimpleButton = tasty(Button, {
494
527
 
495
528
  ### `@media(...)` — Media Queries
496
529
 
497
- Media queries support dimension shorthands and custom unit expansion:
530
+ Media queries support dimension shorthands:
498
531
 
499
532
  | Shorthand | Expands to |
500
533
  | --------- | ---------- |
@@ -516,10 +549,14 @@ fill: {
516
549
  | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
517
550
  | `@media:print` | `@media print` |
518
551
  | `@media:screen` | `@media screen` |
552
+ | `@media:all` | `@media all` |
553
+ | `@media:speech` | `@media speech` |
519
554
  | `@media(prefers-color-scheme: dark)` | `@media (prefers-color-scheme: dark)` |
520
555
  | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
521
556
 
522
- Custom units work inside media queries: `@media(w < 40x)` → `@media (width < calc(var(--gap) * 40))`.
557
+ 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`.
558
+
559
+ 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`.
523
560
 
524
561
  In practice, define state aliases via `configure({ states })` and use `@mobile` instead of writing the full query in every component.
525
562
 
@@ -545,28 +582,30 @@ const Panel = tasty({
545
582
  });
546
583
  ```
547
584
 
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)` |
585
+ | Tasty syntax | CSS output |
586
+ | ---------------------------------- | -------------------------------------------- |
587
+ | `@(layout, w < 600px)` | `@container layout (width < 600px)` |
588
+ | `@(w < 600px)` | `@container (width < 600px)` |
589
+ | `@(layout, $variant=danger)` | `@container layout style(--variant: danger)` |
590
+ | `@(layout, $compact)` | `@container layout style(--compact)` |
591
+ | `@(scroll-state(stuck: top))` | `@container scroll-state(stuck: top)` |
592
+ | `@(nav, scroll-state(stuck: top))` | `@container nav scroll-state(stuck: top)` |
593
+
594
+ 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"`.
556
595
 
557
- Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: "value")`.
596
+ 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.
558
597
 
559
598
  ### `@supports(...)` — Feature Queries
560
599
 
561
600
  Feature queries test CSS property support and accept CSS support functions such
562
601
  as `at-rule()`. Use `$` as the first argument to test selector support:
563
602
 
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))` |
603
+ | Tasty syntax | CSS output |
604
+ | ----------------------------- | ----------------------------------- |
605
+ | `@supports(display: grid)` | `@supports (display: grid)` |
606
+ | `@supports($, :has(*))` | `@supports selector(:has(*))` |
607
+ | `@supports(at-rule(@scope))` | `@supports (at-rule(@scope))` |
608
+ | `!@supports(display: grid)` | `@supports (not (display: grid))` |
570
609
  | `!@supports(at-rule(@scope))` | `@supports (not (at-rule(@scope)))` |
571
610
 
572
611
  ```jsx
@@ -620,12 +659,12 @@ inset: {
620
659
 
621
660
  ```css
622
661
  .t0.t0 {
623
- inset: 0...;
662
+ inset: 0 auto auto auto;
624
663
  }
625
- @container scroll-state(scrolled: block-end) {
626
- @supports (container-type: scroll-state) {
664
+ @supports (container-type: scroll-state) {
665
+ @container scroll-state(scrolled: block-end) {
627
666
  .t0.t0 {
628
- inset: -80px...;
667
+ inset: -640px auto auto auto;
629
668
  }
630
669
  }
631
670
  }
@@ -634,7 +673,7 @@ inset: {
634
673
  The floor is emitted as a bare rule (no negated `@supports` / container branches),
635
674
  so it always applies. When the override matches it wins because it is emitted
636
675
  later and all state selectors share the same specificity (see the note on
637
- `:where()` below).
676
+ `:where()` above).
638
677
 
639
678
  Notes:
640
679
 
@@ -706,7 +745,7 @@ color: {
706
745
  | `@root([lang="en"])` | `:root[lang="en"]` |
707
746
  | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
708
747
 
709
- Root conditions are prepended to the component selector: `:root[data-schema="dark"] .t0.t0 { ... }`.
748
+ Root conditions are prepended to the component selector inside `:where()`: `:where(:root[data-schema="dark"]) .t0.t0 { ... }`.
710
749
 
711
750
  ### `@own(...)` — Sub-element's Own State
712
751
 
@@ -752,11 +791,11 @@ const FadeIn = tasty({
752
791
 
753
792
  | Tasty syntax | CSS output |
754
793
  | ------------ | ------------------------------------ |
755
- | `@starting` | `@starting-style { .t0.t0 { ... } }` |
794
+ | `@starting` | `.t0.t0 { @starting-style { ... } }` |
756
795
 
757
796
  ### `@parent(...)` — Parent Element States
758
797
 
759
- Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
798
+ Style based on ancestor element attributes. Uses `:where([selector] *)` / `:where(:not([selector] *))` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
760
799
 
761
800
  ```jsx
762
801
  const Highlight = tasty({
@@ -770,15 +809,15 @@ const Highlight = tasty({
770
809
  });
771
810
  ```
772
811
 
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) |
780
- | `@parent(hovered) & @parent(focused)` | `:is([data-hovered] *):is([data-focused] *)` (independent ancestors) |
781
- | `@parent(hovered \| focused)` | `:is([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
812
+ | Syntax | CSS Output |
813
+ | ------------------------------------- | -------------------------------------------------------------------------- |
814
+ | `@parent(hovered)` | `:where([data-hovered] *)` |
815
+ | `!@parent(hovered)` | `:where(:not([data-hovered] *))` |
816
+ | `@parent(hovered, >)` | `:where([data-hovered] > *)` (direct parent) |
817
+ | `@parent(.active)` | `:where(.active *)` |
818
+ | `@parent(hovered & focused)` | `:where([data-hovered][data-focused] *)` (same ancestor) |
819
+ | `@parent(hovered) & @parent(focused)` | `:where([data-hovered] *):where([data-focused] *)` (independent ancestors) |
820
+ | `@parent(hovered \| focused)` | `:where([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
782
821
 
783
822
  For sub-elements, the parent check applies to the root element's ancestors:
784
823
 
@@ -793,12 +832,12 @@ const Card = tasty({
793
832
  },
794
833
  },
795
834
  });
796
- // → .t0.t0:is([data-hovered] *) [data-element="Label"]
835
+ // → .t0.t0:where([data-hovered] *) [data-element="Label"]
797
836
  ```
798
837
 
799
838
  ### `:is()`, `:has()` — CSS Structural Pseudo-classes
800
839
 
801
- 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 `*`.
840
+ 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()`.
802
841
 
803
842
  `:where()` and `:not()` are also supported but rarely needed — use `:is()` and `!` negation instead.
804
843
 
@@ -821,18 +860,18 @@ const Card = tasty({
821
860
  });
822
861
  ```
823
862
 
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`) |
863
+ | Syntax | CSS Output | Meaning |
864
+ | -------------------- | ------------------------------------- | --------------------------------------- |
865
+ | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
866
+ | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
867
+ | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
868
+ | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
869
+ | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
870
+ | `:has(>)` | `:has(> *)` | Has any direct child |
871
+ | `:is(Field + input)` | `:is([data-element="Field"] + input)` | Is an input right after a Field sibling |
872
+ | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
873
+ | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
874
+ | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
836
875
 
837
876
  Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
838
877
 
@@ -865,6 +904,8 @@ const Pulse = tasty({
865
904
  });
866
905
  ```
867
906
 
907
+ 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.
908
+
868
909
  ---
869
910
 
870
911
  ## Properties (`@property`)
@@ -952,7 +993,7 @@ Define custom list markers via the CSS `@counter-style` at-rule. Keys are counte
952
993
 
953
994
  ```ts
954
995
  const EmojiList = tasty({
955
- tag: 'ol',
996
+ as: 'ol',
956
997
  styles: {
957
998
  '@counter-style': {
958
999
  thumbs: {
@@ -1044,7 +1085,7 @@ Generated CSS:
1044
1085
 
1045
1086
  A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
1046
1087
 
1047
- 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).
1088
+ 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).
1048
1089
 
1049
1090
  > `@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.
1050
1091
 
@@ -64,7 +64,7 @@ export default function App() {
64
64
 
65
65
  `tasty()` returns a normal React component. The keys in each state map define priority: later branches win over earlier ones. Because `disabled` is last, a disabled button keeps its disabled styles even while the pointer is over it.
66
66
 
67
- `disabled` and `checked` are built-in automatic states. Tasty activates them from the corresponding native prop or attribute, so `disabled` is the concise Tasty form of `[disabled]`, and `checked` is the concise form of `[checked]`.
67
+ `disabled`, `checked`, and `hidden` are built-in automatic states. Tasty sets `data-disabled`, `data-checked`, and `data-hidden` from the matching native prop, so `disabled` in a state map is the concise form of `[disabled]` (it compiles to `[data-disabled]`).
68
68
 
69
69
  Tasty compiles that priority into selectors that exclude one another. The hover, active, and disabled background rules cannot all match and ask the CSS cascade to decide the winner.
70
70
 
@@ -101,9 +101,9 @@ import App from './App';
101
101
  createRoot(document.getElementById('root')!).render(<App />);
102
102
  ```
103
103
 
104
- ### Define shared tokens and override default unit values
104
+ ### Define shared tokens and units
105
105
 
106
- Color tokens like `#primary` resolve to CSS custom properties at runtime (e.g. `var(--primary-color)`). Built-in units like `x`, `r`, and `bw` already work without setup and multiply CSS custom properties by default. Use `configure({ tokens })` when you want to define shared token values or override the defaults your app uses:
106
+ Color tokens like `#primary` and value tokens like `$sidebar-width` resolve to CSS custom properties (`var(--primary-color)`, `var(--sidebar-width)`). Define their values with `configure({ tokens })`:
107
107
 
108
108
  ```tsx
109
109
  // src/tasty-config.ts
@@ -114,10 +114,7 @@ configure({
114
114
  '#primary': 'oklch(55% 0.25 265)',
115
115
  '#surface': '#fff',
116
116
  '#text': '#111',
117
- $gap: '8px',
118
- $radius: '4px',
119
- '$border-width': '1px',
120
- '$outline-width': '2px',
117
+ '$sidebar-width': '280px',
121
118
  },
122
119
  });
123
120
  ```
@@ -135,7 +132,20 @@ configure({
135
132
  });
136
133
  ```
137
134
 
138
- Every component using `#primary`, `2x`, or `1r` adjusts automatically. Tokens are injected as `:root` CSS custom properties when the first style is rendered. You can also use standard CSS color values such as `rgb(...)`, `hsl(...)`, and named colors directly; `okhsl(...)` is the recommended choice when you want authored colors that stay aligned with Tasty's design-system-oriented workflow.
135
+ Tokens are injected as `:root` CSS custom properties when the first style is rendered, and every component using `#primary` follows them. You can also use standard CSS color values such as `rgb(...)`, `hsl(...)`, and named colors directly; the built-in `okhsl(...)` and `okhst(...)` functions are the recommended choice when you want authored colors that stay aligned with Tasty's design-system-oriented workflow.
136
+
137
+ Units work differently: `x`, `r`, `cr`, `bw`, and `ow` compile to plain values when styles are generated (`2x` → `16px` with the default `8px` step), so setting a `$gap` token does not change them. Change the scale with `configure({ units })`:
138
+
139
+ ```tsx
140
+ configure({
141
+ units: {
142
+ x: '4px', // spacing step: 2x → 8px
143
+ r: '8px', // radius: 1r → 8px
144
+ },
145
+ });
146
+ ```
147
+
148
+ When the scale has to change at runtime, for example per theme or density, point the unit at a custom property and define that property as a token. With `units: { x: 'var(--gap)' }` and `tokens: { $gap: '8px' }`, `2x` compiles to `calc(2 * var(--gap))`. See [Built-in Units](dsl.md#built-in-units) for the defaults.
139
149
 
140
150
  > **Note:** `configure({ replaceTokens })` is a separate mechanism — it replaces tokens with literal values at parse time (baked into CSS). Use it for value aliases like `$card-padding: '4x'` that should be resolved during style generation, not for defining color or unit values. See [Configuration — Replace Tokens](configuration.md#replace-tokens-parse-time-substitution) for details.
141
151
 
@@ -145,7 +155,7 @@ See [Configuration](configuration.md) for the full `configure()` API — tokens,
145
155
 
146
156
  ## ESLint plugin
147
157
 
148
- The ESLint plugin catches invalid style properties, bad token references, malformed state keys, and other mistakes at lint time — before they reach the browser.
158
+ The ESLint plugin catches invalid style properties, bad token references, malformed state keys, and other mistakes at lint time — before they reach the browser. It checks style objects and JSX style props alike, and runs under both ESLint 9+ and oxlint.
149
159
 
150
160
  ### Install
151
161
 
@@ -167,36 +177,80 @@ export default [
167
177
  ];
168
178
  ```
169
179
 
170
- ### What `recommended` catches
180
+ ### Tell it about your design system
181
+
182
+ Without project configuration the plugin checks syntax and property names, but it cannot know which tokens, presets, or recipes exist. Add a `tasty.config.ts` at the project root to enable those checks for the names your `configure()` call defines:
183
+
184
+ ```ts
185
+ // tasty.config.ts
186
+ export default {
187
+ tokens: ['#primary', '#surface', '#text', '$sidebar-width'],
188
+ states: ['@mobile', '@dark'],
189
+ presets: ['h1', 'h2', 't1', 't2'],
190
+ recipes: ['card'],
191
+ importSources: ['@my-org/ui'], // modules that re-export tasty()
192
+ };
193
+ ```
171
194
 
172
- The `recommended` config covers common correctness and style issues:
195
+ Every list is optional; leaving one out skips that check. `importSources` matters when your design system re-exports `tasty()` from its own module: the plugin only lints calls whose import it can trace.
173
196
 
174
- | Category | Rules | Examples |
175
- | ------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
176
- | Property validation | `known-property`, `valid-boolean-property`, `valid-sub-element` | Flags typos like `pading` or invalid boolean usage |
177
- | Value validation | `valid-value`, `valid-color-token`, `valid-custom-unit` | Catches `#nonexistent` tokens, bad unit syntax |
178
- | State validation | `valid-state-key`, `no-nested-state-map`, `require-default-state` | Validates state key syntax, ensures `''` default exists |
179
- | Structure | `valid-styles-structure`, `no-important`, `no-nested-selector` | Prevents `!important`, invalid nesting |
180
- | Static mode | `static-no-dynamic-values`, `static-valid-selector` | Enforces build-time constraints in `tastyStatic()` |
181
- | Style properties | `valid-preset`, `valid-recipe`, `valid-transition`, `valid-directional-modifier`, `valid-radius-shape` | Validates preset names, recipe references, transition syntax |
182
- | Motion guidance | `no-raw-transition-duration` | Suggests timing tokens or the default timing for transition durations |
183
- | Dynamic styles | `no-runtime-styles-mutation`, `no-style-spread` | Warns on JavaScript values and spreads inside runtime style objects |
197
+ ### What `recommended` catches
198
+
199
+ | Category | Rules | Examples |
200
+ | ------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
201
+ | Property validation | `known-property`, `valid-boolean-property`, `valid-sub-element` | Flags typos like `pading` or invalid boolean usage |
202
+ | Value validation | `valid-value`, `valid-color-token`, `valid-custom-unit` | Catches `#nonexistent` tokens, bad unit syntax |
203
+ | State validation | `valid-state-key`, `no-nested-state-map`, `require-default-state` | Validates state key syntax, ensures `''` default exists |
204
+ | Structure | `valid-styles-structure`, `no-important`, `no-nested-selector` | Prevents `!important`, invalid nesting |
205
+ | Static mode | `static-no-dynamic-values`, `static-valid-selector` | Enforces build-time constraints in `tastyStatic()` |
206
+ | Style properties | `valid-preset`, `valid-recipe`, `valid-transition`, `valid-directional-modifier`, `valid-radius-shape` | Validates preset names, recipe references, transition syntax |
207
+ | Token usage | `no-raw-color-values`, `consistent-token-usage`, `no-raw-transition-duration` | Suggests `#color` tokens over raw colors, units over raw pixel lengths |
208
+ | Preferred syntax | `prefer-shorthand-property`, `prefer-auto-calc`, `prefer-hide`, `prefer-state-negation`, and others | Suggests `fill` over `backgroundColor`, `(...)` over `calc(...)` |
209
+ | Component props | `no-style-prop`, `no-classname-prop`, `no-styles-prop` | Points escape hatches toward `tokens`, `mods`, and the component's props |
210
+ | Dynamic styles | `no-runtime-styles-mutation`, `no-style-spread` | Warns on JavaScript values and spreads inside runtime style objects |
184
211
 
185
212
  ### Strict config
186
213
 
187
- For stricter governance, use `tasty.configs.strict`. It adds rules that flag direct `styles` prop usage and other patterns:
214
+ For stricter governance, use `tasty.configs.strict`. It adds existence checks for `$` custom properties and `@` state aliases, and validates state definitions in `configure()` and `tasty.config.ts`:
188
215
 
189
216
  ```js
190
217
  export default [tasty.configs.strict];
191
218
  ```
192
219
 
220
+ See the [plugin README](https://github.com/tenphi/eslint-plugin-tasty) for every rule, the oxlint setup, and the experimental type-aware mode for JSX props.
221
+
222
+ ---
223
+
224
+ ## TypeScript autocomplete
225
+
226
+ Component props are typed from their definitions without extra setup: `modProps` value lists become unions, `variant` is typed from the variant keys, and sub-elements are typed components. To get autocomplete for your own token, preset, and theme names inside style values, register them with module augmentation:
227
+
228
+ ```ts
229
+ // tasty.d.ts
230
+ import '@tenphi/tasty';
231
+
232
+ declare module '@tenphi/tasty' {
233
+ interface TastyNamedColors {
234
+ primary: true;
235
+ surface: true;
236
+ text: true;
237
+ }
238
+ interface TastyPresetNames {
239
+ h1: true;
240
+ t2: true;
241
+ }
242
+ }
243
+ ```
244
+
245
+ See [Configuration → TypeScript](configuration.md#typescript) for every augmentation interface and how component props are typed.
246
+
193
247
  ---
194
248
 
195
249
  ## Editor support
196
250
 
197
- **[VS Code Extension](https://github.com/tenphi/tasty-vscode-extension)** — Syntax highlighting for Tasty styles in TypeScript/TSX/JavaScript/JSX. Highlights color tokens, custom units, state keys, presets, and style properties inside `tasty()` and `tastyStatic()` calls. Install from the VS Code marketplace or from a `.vsix` file.
251
+ **[VS Code Extension](https://github.com/tenphi/tasty-vscode-extension)** — Syntax highlighting for Tasty styles in TypeScript/TSX/JavaScript/JSX: color tokens, custom units, state keys, presets, and style properties inside `tasty()`, `tastyStatic()`, `styles` objects, and style functions. It also adds snippets for components, state maps, and `configure()`. Install the `.vsix` from the [latest GitHub release](https://github.com/tenphi/tasty-vscode-extension/releases/latest); the extension is not published to the marketplace.
198
252
 
199
- **[Glaze](https://github.com/tenphi/glaze)** — OKHSL-based color theme generator with automatic WCAG contrast solving. Generate light, dark, and high-contrast color palettes from a single hue and export them directly as Tasty color tokens. See the [Ecosystem section](../README.md#ecosystem) in the README.
253
+ **[Glaze](https://github.com/tenphi/glaze)** — Not an editor tool, but the fastest way to fill `configure({ tokens })`: it generates light, dark, and high-contrast palettes from one hue in the OKHST color space, solves WCAG and APCA contrast, and exports them as Tasty tokens with `configure({ tokens: theme.tasty() })`. See [Glaze in the README](../README.md#glaze).
200
254
 
201
255
  ---
202
256
 
@@ -228,6 +282,7 @@ Both `tasty()` and `tastyStatic()` share the same DSL, tokens, units, and state
228
282
  - **[Adoption Guide](adoption.md)** — Roll out Tasty inside an existing design system or platform team
229
283
  - **[Comparison](comparison.md)** — Evaluate Tasty against other styling systems
230
284
  - **[Configuration](configuration.md)** — Full `configure()` API: tokens, recipes, custom units, style handlers, TypeScript extensions
285
+ - **[Plugins & Extension Points](plugins.md)** — Add your own style properties, units, functions, and component props
231
286
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
232
287
  - **[Debug Utilities](debug.md)** — Inspect generated CSS and debug runtime behavior when styles do not look right
233
288
 
@@ -237,4 +292,6 @@ Both `tasty()` and `tastyStatic()` share the same DSL, tokens, units, and state
237
292
 
238
293
  - Styles are missing on first render: make sure the file that calls `configure()` is imported before any `tasty()` component renders.
239
294
  - Token or unit values are not what you expect: check your `configure({ tokens, units })` setup, then inspect the generated CSS variables with [Debug Utilities](debug.md).
295
+ - Changing a `$gap` or `$radius` token does not change `2x` or `1r`: units compile to plain values from `configure({ units })`. Point a unit at the custom property (`units: { x: 'var(--gap)' }`) if it should follow the token.
296
+ - The linter accepts unknown tokens or presets: list them in `tasty.config.ts` so the plugin knows what your design system defines.
240
297
  - You need zero-runtime delivery: use server-only [`tasty()` with server rendering](ssr.md) to keep the full React API, or use [`tastyStatic()` build-time extraction](tasty-static.md) when styles must be compiled before rendering.