@tenphi/tasty 3.10.0 → 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 (88) hide show
  1. package/README.md +399 -380
  2. package/dist/{babel-types-C_7kRP38.d.ts → babel-types-BKDTT4--.d.ts} +2 -2
  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-l2Ys-AJr.js → build-config-5d8WlQbq.js} +5 -5
  6. package/dist/chunks/{build-config-l2Ys-AJr.js.map → build-config-5d8WlQbq.js.map} +1 -1
  7. package/dist/chunks/{collector-CaNtsRtn.js → collector-CjCak4iE.js} +4 -4
  8. package/dist/chunks/{collector-CaNtsRtn.js.map → collector-CjCak4iE.js.map} +1 -1
  9. package/dist/chunks/{config-engine-CpExZO_T.js → config-engine-uOFYZidB.js} +4 -4
  10. package/dist/chunks/{config-engine-CpExZO_T.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-DAIJxzFY.js → css-definitions-ubrHP97m.js} +88 -77
  14. package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
  15. package/dist/chunks/{debug-CKy49GEr.js → debug-CJ31U_WE.js} +6 -6
  16. package/dist/chunks/{debug-CKy49GEr.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-oi6i3ELR.js → react-runtime-CyyCI02N.js} +168 -61
  20. package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
  21. package/dist/chunks/{runtime-engine-DoULyhPt.js → runtime-engine-BQmOu5-W.js} +53 -36
  22. package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
  23. package/dist/chunks/{runtime-state-CZ2J5QiZ.js → runtime-state-DEsyUMU-.js} +14 -3
  24. package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
  25. package/dist/chunks/{style-engine-CeKsjtIr.js → style-engine-BX2LshDZ.js} +116 -56
  26. package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
  27. package/dist/chunks/{zero-engine-fxKu8Jbn.js → zero-engine-Dw0P4yGM.js} +22 -7
  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-CrZOfNJ5.d.ts → config-CcxqSZI2.d.ts} +28 -9
  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-CYJtAI9a.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 +6 -17
  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 +1 -1
  52. package/dist/zero/babel.js +30 -19
  53. package/dist/zero/babel.js.map +1 -1
  54. package/dist/zero/index.d.ts +24 -6
  55. package/dist/zero/index.js +4 -4
  56. package/dist/zero/index.js.map +1 -1
  57. package/dist/zero/next.d.ts +1 -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 +173 -58
  65. package/docs/debug.md +3 -2
  66. package/docs/design-system.md +117 -15
  67. package/docs/dsl.md +120 -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 +19 -16
  73. package/docs/plugins.md +20 -40
  74. package/docs/react-api.md +21 -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 +110 -56
  79. package/package.json +23 -9
  80. package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
  81. package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
  82. package/dist/chunks/css-definitions-DAIJxzFY.js.map +0 -1
  83. package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
  84. package/dist/chunks/react-runtime-oi6i3ELR.js.map +0 -1
  85. package/dist/chunks/runtime-engine-DoULyhPt.js.map +0 -1
  86. package/dist/chunks/runtime-state-CZ2J5QiZ.js.map +0 -1
  87. package/dist/chunks/style-engine-CeKsjtIr.js.map +0 -1
  88. package/dist/chunks/zero-engine-fxKu8Jbn.js.map +0 -1
@@ -42,15 +42,25 @@ configure({
42
42
  // Borders
43
43
  '#border': '#e0e0e0',
44
44
 
45
- // Unit values
45
+ // Spacing and shape scale
46
46
  $gap: '8px',
47
47
  $radius: '4px',
48
48
  '$border-width': '1px',
49
49
  '$outline-width': '2px',
50
50
  },
51
+ // Built-in units compile to fixed values by default. Pointing them at the
52
+ // tokens above lets a theme or density mode change the scale at runtime.
53
+ units: {
54
+ x: 'var(--gap)', // 2x → calc(2 * var(--gap))
55
+ r: 'var(--radius)',
56
+ bw: 'var(--border-width)',
57
+ ow: 'var(--outline-width)',
58
+ },
51
59
  });
52
60
  ```
53
61
 
62
+ Leave out the `units` block to keep the built-in fixed scale (`x` = `8px`, `r` = `6px`, `bw` = `1px`, `ow` = `3px`), or set fixed values there instead (`units: { x: '8px', r: '4px' }`). Define the token a unit points at; until you do, `--gap`, `--radius`, `--border-width`, and `--outline-width` start at the built-in unit values. Boolean shorthands follow the same scale: `radius: true` is `1r`, `border: true` is `1bw`, and `outline: true` is `1ow`. See [Built-in Units](dsl.md#built-in-units).
63
+
54
64
  Tokens are injected as CSS custom properties on `:root` when the first style is rendered. They support state maps for theme-aware values:
55
65
 
56
66
  ```tsx
@@ -66,7 +76,7 @@ configure({
66
76
 
67
77
  The `#on-*` convention names the text color that sits on top of a fill — `#on-primary` is the text color on a `#primary` background. This makes state maps self-documenting: `fill: '#primary'` and `color: '#on-primary'` clearly belong together.
68
78
 
69
- For OKHSL-based palette generation with automatic WCAG contrast solving, see [Glaze](https://github.com/tenphi/glaze).
79
+ To generate the whole palette instead of hand-picking values, use [Glaze](https://github.com/tenphi/glaze): it builds light, dark, and high-contrast variants from one hue in the OKHST color space, solves WCAG and APCA contrast, and exports Tasty tokens you can pass straight to `configure({ tokens: theme.tasty() })`.
70
80
 
71
81
  ### Replace tokens for value aliases
72
82
 
@@ -176,11 +186,13 @@ const Card = tasty({
176
186
  styles: {
177
187
  padding: { '': '4x', '@mobile': '2x' },
178
188
  flow: { '': 'row', '@mobile': 'column' },
179
- fill: { '': '#surface', '@dark': '#surface-dark' },
189
+ radius: { '': '1r', '@mobile': '0' },
180
190
  },
181
191
  });
182
192
  ```
183
193
 
194
+ Theme switching belongs in tokens (`'#surface': { '': '#fff', '@dark': '#1c1c1e' }`) rather than in component state maps, so components stay theme-agnostic.
195
+
184
196
  Without aliases, the same component would need the full media query expression inlined in every state map — across every property, in every component. Aliases eliminate that duplication.
185
197
 
186
198
  ---
@@ -279,7 +291,7 @@ const Grid = tasty({
279
291
  display: 'grid',
280
292
  gap: '1x',
281
293
  },
282
- styleProps: [...FLOW_STYLES, 'gridColumns', 'gridRows', 'gridAreas'],
294
+ styleProps: FLOW_STYLES, // includes gridColumns, gridRows, gridAreas
283
295
  });
284
296
  ```
285
297
 
@@ -298,18 +310,54 @@ Product engineers use these to compose layouts without writing CSS:
298
310
  </Grid>
299
311
  ```
300
312
 
313
+ ### Interactive components with variants
314
+
315
+ Variants are named looks that replace each other. Combine them with `modProps` for states that stack, such as size or loading:
316
+
317
+ ```tsx
318
+ const Button = tasty({
319
+ as: 'button',
320
+ modProps: { size: ['sm', 'md', 'lg'] as const },
321
+ styles: {
322
+ recipe: 'interactive',
323
+ padding: { '': '1.5x 3x', 'size=sm': '1x 2x', 'size=lg': '2x 4x' },
324
+ radius: '1r',
325
+ },
326
+ variants: {
327
+ default: {
328
+ fill: {
329
+ '': '#primary',
330
+ ':hover': '#primary-hover',
331
+ disabled: '#surface',
332
+ },
333
+ color: '#on-primary',
334
+ },
335
+ danger: {
336
+ fill: { '': '#danger', ':hover': '#danger-hover', disabled: '#surface' },
337
+ color: '#on-danger',
338
+ },
339
+ },
340
+ });
341
+
342
+ <Button variant="danger" size="lg">
343
+ Delete
344
+ </Button>;
345
+ ```
346
+
347
+ `variant` is typed from the keys, the `default` variant applies when no `variant` prop is passed, and only the variants that actually render emit CSS. See [React API — Variants](react-api.md#variants).
348
+
301
349
  ### Which styleProps to expose
302
350
 
303
351
  Match the prop set to the component's role:
304
352
 
305
- | Component category | Recommended styleProps |
306
- | ------------------------------------------ | -------------------------------------------------------------- |
307
- | Layout containers (`Space`, `Box`, `Grid`) | `FLOW_STYLES` — flow, gap, align, justify, padding, fill |
308
- | Positioned elements (`Button`, `Badge`) | `POSITION_STYLES` — placeSelf, gridArea, order |
309
- | Text elements | `['preset', 'color']` or a custom subset |
310
- | Compound components | Typically none — styling happens via sub-elements and wrapping |
353
+ | Component category | Recommended styleProps |
354
+ | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
355
+ | Layout containers (`Space`, `Box`, `Grid`) | `FLOW_STYLES` — flow, gap, align/justify/place, grid templates (add `padding` or `fill` explicitly) |
356
+ | Positioned elements (`Button`, `Badge`) | `POSITION_STYLES` — placeSelf, gridArea, order |
357
+ | Text elements | `['color', ...TEXT_STYLES]` (`preset` is always available) |
358
+ | Compound components | Typically none — styling happens via sub-elements and wrapping |
311
359
 
312
- Exposing too many props weakens the design system's constraints. See [Methodology — styleProps as the public API](methodology.md#styleprops-as-the-public-api) for the rationale.
360
+ `BASE_STYLES` (`display`, `font`, `preset`, `hide`, `whiteSpace`, `opacity`, `transition`) are accepted as props on every component without being listed. Exposing too many props weakens the design system's constraints. See [Methodology — styleProps as the public API](methodology.md#styleprops-as-the-public-api) for the rationale.
313
361
 
314
362
  ---
315
363
 
@@ -369,14 +417,14 @@ For sub-element syntax details (selector affix `$`, `@own()`, `elements` config)
369
417
 
370
418
  ## Defining the override contract
371
419
 
372
- A design system works best when the rules for customization are explicit. Tasty provides three levels of control:
420
+ A design system works best when the rules for customization are explicit. Tasty gives product engineers four sanctioned ways to customize a component, and a few to discourage:
373
421
 
374
422
  ### What product engineers can do
375
423
 
376
424
  1. **Use styleProps** — adjust layout, spacing, positioning through the props the component exposes:
377
425
 
378
426
  ```tsx
379
- <Space flow="row" gap="3x" padding="2x">
427
+ <Space flow="row" gap="3x" placeItems="center">
380
428
  ```
381
429
 
382
430
  2. **Use modProps** — control component states through typed props instead of `mods`:
@@ -398,7 +446,12 @@ A design system works best when the rules for customization are explicit. Tasty
398
446
  ```tsx
399
447
  const DangerButton = tasty(Button, {
400
448
  styles: {
401
- fill: { '': '#danger', ':hover': '#danger-hover' },
449
+ fill: {
450
+ '': '#danger',
451
+ ':hover': '#danger-hover',
452
+ ':active': '#danger-pressed',
453
+ disabled: '@inherit', // a map with '' replaces the parent's; keep its disabled branch
454
+ },
402
455
  color: '#on-danger',
403
456
  },
404
457
  });
@@ -414,6 +467,53 @@ See [Methodology — styles prop vs style prop](methodology.md#styles-prop-vs-st
414
467
 
415
468
  ---
416
469
 
470
+ ## Guardrails: linting and types
471
+
472
+ A vocabulary only holds if mistakes surface early. Register the same names you pass to `configure()` with the ESLint plugin and with TypeScript:
473
+
474
+ ```ts
475
+ // tasty.config.ts — read by @tenphi/eslint-plugin-tasty
476
+ export default {
477
+ tokens: ['#surface', '#text', '#primary', '#danger', '$gap', '$radius'],
478
+ states: ['@mobile', '@tablet', '@desktop', '@dark', '@reduced-motion'],
479
+ presets: ['h1', 'h2', 'h3', 't2', 't3'],
480
+ recipes: ['card', 'elevated', 'input-reset', 'interactive'],
481
+ importSources: ['@my-org/ds'], // where product code imports tasty() from
482
+ };
483
+ ```
484
+
485
+ ```js
486
+ // eslint.config.js
487
+ import tasty from '@tenphi/eslint-plugin-tasty';
488
+
489
+ export default [tasty.configs.recommended]; // or tasty.configs.strict
490
+ ```
491
+
492
+ ```ts
493
+ // tasty.d.ts
494
+ import '@tenphi/tasty';
495
+
496
+ declare module '@tenphi/tasty' {
497
+ interface TastyNamedColors {
498
+ surface: true;
499
+ text: true;
500
+ primary: true;
501
+ danger: true;
502
+ }
503
+ interface TastyPresetNames {
504
+ h1: true;
505
+ h2: true;
506
+ h3: true;
507
+ t2: true;
508
+ t3: true;
509
+ }
510
+ }
511
+ ```
512
+
513
+ Unknown colors, presets, and recipes then fail lint (`valid-color-token`, `valid-preset`, `valid-recipe`), and `strict` adds checks for unknown `$` tokens and state aliases (`valid-custom-property`, `no-unknown-state-alias`). The same names autocomplete in editors. Deriving both lists from the same token and recipe modules that `config.ts` imports keeps all three in sync.
514
+
515
+ ---
516
+
417
517
  ## Project structure
418
518
 
419
519
  A recommended structure for a design system built on Tasty:
@@ -421,6 +521,8 @@ A recommended structure for a design system built on Tasty:
421
521
  ```
422
522
  ds/
423
523
  config.ts # configure() — tokens, units, states, recipes
524
+ tasty.config.ts # Names the ESLint plugin validates against
525
+ tasty.d.ts # TypeScript augmentation for autocomplete
424
526
  primitives/
425
527
  Space.tsx # Layout: flex container with FLOW_STYLES
426
528
  Box.tsx # Generic container
@@ -436,7 +538,7 @@ ds/
436
538
  tokens/
437
539
  colors.ts # Color token definitions
438
540
  typography.ts # Typography presets for configure({ presets })
439
- spacing.ts # Spacing token definitions
541
+ units.ts # Spacing and shape scale for configure({ units })
440
542
  index.ts # Public API: re-exports components + configure
441
543
  ```
442
544
 
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
 
@@ -954,7 +993,7 @@ Define custom list markers via the CSS `@counter-style` at-rule. Keys are counte
954
993
 
955
994
  ```ts
956
995
  const EmojiList = tasty({
957
- tag: 'ol',
996
+ as: 'ol',
958
997
  styles: {
959
998
  '@counter-style': {
960
999
  thumbs: {
@@ -1046,7 +1085,7 @@ Generated CSS:
1046
1085
 
1047
1086
  A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
1048
1087
 
1049
- 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).
1050
1089
 
1051
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.
1052
1091