@tenphi/tasty 3.10.0 → 3.11.1

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 +403 -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--GdqCPlp.d.ts} +58 -16
  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 +39 -34
  63. package/docs/comparison.md +9 -8
  64. package/docs/configuration.md +174 -58
  65. package/docs/debug.md +3 -2
  66. package/docs/design-system.md +117 -15
  67. package/docs/dsl.md +145 -81
  68. package/docs/getting-started.md +81 -24
  69. package/docs/injector.md +41 -40
  70. package/docs/methodology.md +46 -21
  71. package/docs/migration-v3.md +3 -3
  72. package/docs/pipeline.md +19 -16
  73. package/docs/plugins.md +21 -41
  74. package/docs/react-api.md +61 -9
  75. package/docs/runtime-benchmarks.md +4 -4
  76. package/docs/ssr.md +92 -66
  77. package/docs/styles.md +33 -17
  78. package/docs/tasty-static.md +129 -58
  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
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
+ // → .t1x29r0l.t1x29r0l > [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,10 @@ fill: '#current', // → currentcolor
157
171
  color: '(#primary, #secondary)', // Fallback syntax
158
172
  ```
159
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
+
160
178
  The suffix sets the alpha on the token's color with CSS relative color syntax:
161
179
 
162
180
  ```jsx
@@ -182,6 +200,17 @@ Two properties follow from writing the alpha slot rather than compositing:
182
200
  `/ var(--fade)` unchanged, so it works whether `$fade` holds `.5` or `50%` —
183
201
  which is what `--*-opacity` properties are registered to accept.
184
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
+
185
214
  ### `#current` is the keyword
186
215
 
187
216
  `#current` emits the `currentcolor` keyword, so it resolves against the element
@@ -219,7 +248,7 @@ through `color-mix()`, which works from Safari 16.2 and composes like every othe
219
248
 
220
249
  ### `#current` composes instead
221
250
 
222
- `#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_
223
252
  a color, so fading it sets its alpha. `currentcolor` is the color an element
224
253
  **inherits**, which an ancestor may already have faded — `#current.4` means "40%
225
254
  of what reaches me":
@@ -236,9 +265,10 @@ step. Because a `color-mix()` percentage cannot be a `<number>`, an opacity
236
265
  custom property used as `#current.$fade` must hold a unitless number; a token
237
266
  accepts either form.
238
267
 
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).
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).
242
272
 
243
273
  ---
244
274
 
@@ -256,6 +286,9 @@ border: '1bw solid oklch(from #primary l c h / 50%)', // relative color syntax
256
286
  shadow: '0 0 1x color-mix(in oklab, #dark 20%, transparent)',
257
287
  ```
258
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
+
259
292
  `light-dark()` is the one exception to "always a color": CSS lets it pick between
260
293
  values of any type, so it is treated as a color only when its arguments are
261
294
  colors. `padding: 'light-dark(1x, 2x)'` still reaches the padding slot.
@@ -293,16 +326,16 @@ fill: '#adaptive.5'; // → oklch(from light-dark(…) l c h / .5)
293
326
 
294
327
  ## Built-in Units
295
328
 
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)` |
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)` |
304
337
 
305
- 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).
306
339
 
307
340
  ---
308
341
 
@@ -359,7 +392,7 @@ const CardWithPanel = tasty({
359
392
  });
360
393
  ```
361
394
 
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).
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).
363
396
 
364
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.
365
398
 
@@ -439,12 +472,14 @@ fill: {
439
472
  Use `null` inside a state map to remove a state, or `false` to block it entirely (tombstone):
440
473
 
441
474
  ```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
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
+ });
448
483
  ```
449
484
 
450
485
  ### Resetting Properties with `null` and `false`
@@ -494,7 +529,7 @@ const SimpleButton = tasty(Button, {
494
529
 
495
530
  ### `@media(...)` — Media Queries
496
531
 
497
- Media queries support dimension shorthands and custom unit expansion:
532
+ Media queries support dimension shorthands:
498
533
 
499
534
  | Shorthand | Expands to |
500
535
  | --------- | ---------- |
@@ -516,10 +551,17 @@ fill: {
516
551
  | `@media(600px <= w < 1200px)` | `@media (600px <= width < 1200px)` |
517
552
  | `@media:print` | `@media print` |
518
553
  | `@media:screen` | `@media screen` |
554
+ | `@media:all` | `@media all` |
555
+ | `@media:speech` | `@media speech` |
519
556
  | `@media(prefers-color-scheme: dark)` | `@media (prefers-color-scheme: dark)` |
520
557
  | `@media(prefers-reduced-motion)` | `@media (prefers-reduced-motion)` |
558
+ | `@media(aspect-ratio > 16/9)` | `@media (aspect-ratio > 16/9)` |
521
559
 
522
- Custom units work inside media queries: `@media(w < 40x)` → `@media (width < calc(var(--gap) * 40))`.
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`.
561
+
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.
523
565
 
524
566
  In practice, define state aliases via `configure({ states })` and use `@mobile` instead of writing the full query in every component.
525
567
 
@@ -545,28 +587,30 @@ const Panel = tasty({
545
587
  });
546
588
  ```
547
589
 
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)` |
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)` |
556
598
 
557
- Container style queries use `$prop` (boolean) or `$prop=value` syntax, which maps to CSS `style(--prop)` or `style(--prop: "value")`.
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"`.
600
+
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.
558
602
 
559
603
  ### `@supports(...)` — Feature Queries
560
604
 
561
605
  Feature queries test CSS property support and accept CSS support functions such
562
606
  as `at-rule()`. Use `$` as the first argument to test selector support:
563
607
 
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))` |
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))` |
570
614
  | `!@supports(at-rule(@scope))` | `@supports (not (at-rule(@scope)))` |
571
615
 
572
616
  ```jsx
@@ -620,12 +664,12 @@ inset: {
620
664
 
621
665
  ```css
622
666
  .t0.t0 {
623
- inset: 0...;
667
+ inset: 0 auto auto auto;
624
668
  }
625
- @container scroll-state(scrolled: block-end) {
626
- @supports (container-type: scroll-state) {
669
+ @supports (container-type: scroll-state) {
670
+ @container scroll-state(scrolled: block-end) {
627
671
  .t0.t0 {
628
- inset: -80px...;
672
+ inset: -640px auto auto auto;
629
673
  }
630
674
  }
631
675
  }
@@ -634,7 +678,7 @@ inset: {
634
678
  The floor is emitted as a bare rule (no negated `@supports` / container branches),
635
679
  so it always applies. When the override matches it wins because it is emitted
636
680
  later and all state selectors share the same specificity (see the note on
637
- `:where()` below).
681
+ `:where()` above).
638
682
 
639
683
  Notes:
640
684
 
@@ -706,7 +750,7 @@ color: {
706
750
  | `@root([lang="en"])` | `:root[lang="en"]` |
707
751
  | `!@root(schema=dark)` | `:root:not([data-schema="dark"])` |
708
752
 
709
- 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 { ... }`.
710
754
 
711
755
  ### `@own(...)` — Sub-element's Own State
712
756
 
@@ -752,11 +796,11 @@ const FadeIn = tasty({
752
796
 
753
797
  | Tasty syntax | CSS output |
754
798
  | ------------ | ------------------------------------ |
755
- | `@starting` | `@starting-style { .t0.t0 { ... } }` |
799
+ | `@starting` | `.t0.t0 { @starting-style { ... } }` |
756
800
 
757
801
  ### `@parent(...)` — Parent Element States
758
802
 
759
- Style based on ancestor element attributes. Uses `:is([selector] *)` / `:not([selector] *)` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
803
+ Style based on ancestor element attributes. Uses `:where([selector] *)` / `:where(:not([selector] *))` for symmetric, composable parent checks. Boolean logic (`&`, `|`, `!`, `^`) is supported inside `@parent()`.
760
804
 
761
805
  ```jsx
762
806
  const Highlight = tasty({
@@ -770,15 +814,15 @@ const Highlight = tasty({
770
814
  });
771
815
  ```
772
816
 
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) |
817
+ | Syntax | CSS Output |
818
+ | ------------------------------------- | -------------------------------------------------------------------------- |
819
+ | `@parent(hovered)` | `:where([data-hovered] *)` |
820
+ | `!@parent(hovered)` | `:where(:not([data-hovered] *))` |
821
+ | `@parent(hovered, >)` | `:where([data-hovered] > *)` (direct parent) |
822
+ | `@parent(.active)` | `:where(.active *)` |
823
+ | `@parent(hovered & focused)` | `:where([data-hovered][data-focused] *)` (same ancestor) |
824
+ | `@parent(hovered) & @parent(focused)` | `:where([data-hovered] *):where([data-focused] *)` (independent ancestors) |
825
+ | `@parent(hovered \| focused)` | `:where([data-hovered] *, [data-focused] *)` (OR inside single wrapper) |
782
826
 
783
827
  For sub-elements, the parent check applies to the root element's ancestors:
784
828
 
@@ -793,12 +837,12 @@ const Card = tasty({
793
837
  },
794
838
  },
795
839
  });
796
- // → .t0.t0:is([data-hovered] *) [data-element="Label"]
840
+ // → .t0.t0:where([data-hovered] *) [data-element="Label"]
797
841
  ```
798
842
 
799
843
  ### `:is()`, `:has()` — CSS Structural Pseudo-classes
800
844
 
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 `*`.
845
+ 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
846
 
803
847
  `:where()` and `:not()` are also supported but rarely needed — use `:is()` and `!` negation instead.
804
848
 
@@ -821,18 +865,18 @@ const Card = tasty({
821
865
  });
822
866
  ```
823
867
 
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`) |
868
+ | Syntax | CSS Output | Meaning |
869
+ | -------------------- | ------------------------------------- | --------------------------------------- |
870
+ | `:has(> Icon)` | `:has(> [data-element="Icon"])` | Has Icon as direct child |
871
+ | `:has(+ Icon)` | `:has(+ [data-element="Icon"])` | Immediately followed by an Icon sibling |
872
+ | `:has(~ Icon)` | `:has(~ [data-element="Icon"])` | Has an Icon sibling somewhere after |
873
+ | `:has(Icon +)` | `:has([data-element="Icon"] + *)` | Immediately preceded by an Icon sibling |
874
+ | `:has(Icon ~)` | `:has([data-element="Icon"] ~ *)` | Has an Icon sibling somewhere before |
875
+ | `:has(>)` | `:has(> *)` | Has any direct child |
876
+ | `:is(Field + input)` | `:is([data-element="Field"] + input)` | Is an input right after a Field sibling |
877
+ | `:has(button)` | `:has(button)` | HTML tag (lowercase, unchanged) |
878
+ | `!:has(> Icon)` | `:not(:has(> [data-element="Icon"]))` | Negation (use `!`) |
879
+ | `!:is(Panel)` | `:not([data-element="Panel"])` | Negation (use `!:is`) |
836
880
 
837
881
  Combine with other states using boolean logic (`&`, `|`, `!`, `^`):
838
882
 
@@ -894,6 +938,26 @@ Use explicit `@property` when you need non-default settings like `inherits: fals
894
938
  },
895
939
  ```
896
940
 
941
+ ### Default properties
942
+
943
+ 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.
944
+
945
+ | Token | Initial value | Read by |
946
+ | ---------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- |
947
+ | `#current` | `currentcolor` | anything reading `$current-color`, which `color` sets |
948
+ | `#white`, `#black` | `rgb(255 255 255)`, `rgb(0 0 0)` | your styles |
949
+ | `#border` | `currentcolor` | `border` with no color |
950
+ | `#outline` | `currentcolor` | `outline` with no color |
951
+ | `$gap`, `$radius`, `$border-width`, `$outline-width` | `8px`, `6px`, `1px`, `3px` | units you point at them, as in `units: { x: 'var(--gap)' }` |
952
+ | `$transition` | `80ms` | `transition` with no duration |
953
+ | `$sharp-radius` | `0px` | the sharp corners of `radius: 'leaf'` and `radius: 'backleaf'` |
954
+ | `$bold-font-weight` | `700` | `preset: 'name / strong'` |
955
+ | `$font-sans-fallback`, `$font-mono-fallback` | a `system-ui` and a `ui-monospace` stack | `font` and presets, after `$font-sans` / `$font-mono` |
956
+
957
+ 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.
958
+
959
+ 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 })`.
960
+
897
961
  ---
898
962
 
899
963
  ## Font Face (`@font-face`)
@@ -954,7 +1018,7 @@ Define custom list markers via the CSS `@counter-style` at-rule. Keys are counte
954
1018
 
955
1019
  ```ts
956
1020
  const EmojiList = tasty({
957
- tag: 'ol',
1021
+ as: 'ol',
958
1022
  styles: {
959
1023
  '@counter-style': {
960
1024
  thumbs: {
@@ -1046,7 +1110,7 @@ Generated CSS:
1046
1110
 
1047
1111
  A `FunctionParameter` is one of: `true` (bare param), a string CSS type shorthand (`'<length>'`), or `{ syntax?: string; default?: string | number }`.
1048
1112
 
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).
1113
+ 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
1114
 
1051
1115
  > `@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
1116
 
@@ -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.