@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
@@ -23,9 +23,8 @@ configure({
23
23
 
24
24
  // Custom units (merged with built-in units)
25
25
  units: {
26
- vh: 'vh',
27
- vw: 'vw',
28
- custom: (n) => `${n * 10}px`, // Function-based unit
26
+ gu: '4px', // `2gu` → `8px`
27
+ cols: (n) => `calc(${n} * var(--col-width))`, // function-based unit
29
28
  },
30
29
 
31
30
  // Custom functions — a single map for both flavors, discriminated by value type:
@@ -47,35 +46,35 @@ These docs use `data-schema="dark"` in examples. If your app already standardize
47
46
 
48
47
  ## Options
49
48
 
50
- | Option | Type | Default | Description |
51
- | -------------------- | ------------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
- | `nonce` | `string` | - | CSP nonce for style elements |
53
- | `maxRulesPerSheet` | `number` | `8192` | Maximum rules per injected stylesheet |
54
- | `forceTextInjection` | `boolean` | auto (`true` in test envs) | Force text-node CSS injection instead of constructable stylesheets |
55
- | `devMode` | `boolean` | auto | Enable development-mode features: performance metrics and debug info |
56
- | `states` | `Record<string, string>` | - | Global state aliases for advanced state mapping |
57
- | `parserCacheSize` | `number` | `1000` | Parser LRU cache size |
58
- | `units` | `Record<string, string \| UnitHandler>` | Built-in | Custom units (merged with built-in). See [built-in units](dsl.md#built-in-units) |
59
- | `functions` | `Record<string, FunctionDefinition \| Function>` | - | Custom functions (merged). Bare keys → parse functions; `$$name` keys → declarative CSS `@function` definitions |
60
- | `handlers` | `Record<string, StyleHandlerDefinition>` | Built-in | Custom style handlers (replace built-in). See [Custom Style Handlers](#custom-style-handlers) |
61
- | `propHandlers` | `Record<string, PropHandlerDefinition>` | - | Props middleware for every component — props in, props out. See [Props Middleware](#props-middleware) |
62
- | `baseStyleProps` | `readonly string[]` | - | Style names exposed as props on **every** component. See [Base Style Props](#base-style-props) |
63
- | `tokens` | `Record<string, value \| stateMap>` | - | Design tokens injected as `:root` CSS custom properties |
64
- | `replaceTokens` | `Record<string, string \| number \| boolean>` | - | Parse-time token substitution (inline replacement). `boolean` is allowed for `#` color tokens |
65
- | `keyframes` | `Record<string, KeyframesSteps>` | - | Global keyframes for animations |
66
- | `properties` | `Record<string, PropertyDefinition>` | - | Global CSS @property definitions |
67
- | `fontFaces` | `Record<string, FontFaceInput>` | - | Global @font-face definitions |
68
- | `counterStyles` | `Record<string, CounterStyleDescriptors>` | - | Global @counter-style definitions |
69
- | `polyfills` | `{ functions?: boolean }` | `{}` | Opt-in polyfills for not-yet-baseline features. `functions: true` inlines `@function` calls into plain CSS at parse time |
70
- | `autoPropertyTypes` | `boolean` | `true` | Auto-infer and register `@property` types from values |
71
- | `recipes` | `Record<string, RecipeStyles>` | - | Predefined style recipes (named style bundles) |
72
- | `presets` | `Record<string, TypographyPreset>` | - | Typography presets — shorthand for `generateTypographyTokens()` |
73
- | `globalStyles` | `Record<string, Styles>` | - | Global Tasty styles keyed by CSS selector |
74
- | `plugins` | `TastyPlugin[]` | - | Plugins that bundle any of the above (processed in order; later override earlier, and direct config wins over all). See [Plugins](plugins.md) |
75
- | `gc` | `GCConfig` | - | Garbage-collection tuning for unused styles (`{ touchInterval, capacity }`) |
76
- | `batchInjection` | `boolean \| 'always'` | `false` | Defer stylesheet writes and apply them in one batch. See [Batched injection](#batched-injection) |
77
- | `colorSpace` | `'rgb' \| 'hsl' \| 'oklch'` | - | **Deprecated** — no longer has any effect. See [Color space](#color-space) |
78
- | `namePrefix` | `string` | `'t'` (`tasty`) / `'ts'` (build-time) | Prefix prepended to every generated identifier (class, keyframe, counter-style names). Must match `^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$`. See [Name prefix](#name-prefix). |
49
+ | Option | Type | Default | Description |
50
+ | -------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `nonce` | `string` | - | CSP nonce for style elements |
52
+ | `maxRulesPerSheet` | `number` | `8192` | Maximum rules per injected stylesheet |
53
+ | `forceTextInjection` | `boolean` | `false` (`true` in test environments only while `configure()` has never been called) | Force text-node CSS injection instead of constructable stylesheets. Set it explicitly in test setup |
54
+ | `devMode` | `boolean` | auto | Enable development-mode features: performance metrics and debug info. Does not turn on dev warnings; those follow `NODE_ENV` (or `localStorage.TASTY_DEBUG = 'true'` in a browser) |
55
+ | `states` | `Record<string, string>` | - | Global state aliases for advanced state mapping |
56
+ | `parserCacheSize` | `number` | `1000` | Parser LRU cache size |
57
+ | `units` | `Record<string, string \| UnitHandler>` | Built-in | Custom units (merged with built-in). See [built-in units](dsl.md#built-in-units) |
58
+ | `functions` | `Record<string, FunctionDefinition \| Function>` | - | Custom functions (merged). Bare keys → parse functions; `$$name` keys → declarative CSS `@function` definitions |
59
+ | `handlers` | `Record<string, StyleHandlerDefinition>` | Built-in | Custom style handlers (replace built-in). See [Custom Style Handlers](#custom-style-handlers) |
60
+ | `propHandlers` | `Record<string, PropHandlerDefinition>` | - | Props middleware for every component — props in, props out. See [Props Middleware](#props-middleware) |
61
+ | `baseStyleProps` | `readonly string[]` | - | Style names exposed as props on **every** component. See [Base Style Props](#base-style-props) |
62
+ | `tokens` | `Record<string, value \| stateMap>` | - | Design tokens injected as `:root` CSS custom properties |
63
+ | `replaceTokens` | `Record<string, string \| number \| boolean>` | `{ '#clear': 'transparent' }` | Parse-time token substitution (inline replacement). `boolean` is allowed for `#` color tokens |
64
+ | `keyframes` | `Record<string, KeyframesSteps>` | - | Global keyframes for animations |
65
+ | `properties` | `Record<string, PropertyDefinition>` | - | Global CSS @property definitions |
66
+ | `fontFaces` | `Record<string, FontFaceInput>` | - | Global @font-face definitions |
67
+ | `counterStyles` | `Record<string, CounterStyleDescriptors>` | - | Global @counter-style definitions |
68
+ | `polyfills` | `{ functions?: boolean }` | `{}` | Opt-in polyfills for not-yet-baseline features. `functions: true` inlines `@function` calls into plain CSS at parse time |
69
+ | `autoPropertyTypes` | `boolean` | `true` | Auto-infer and register `@property` types from values |
70
+ | `recipes` | `Record<string, RecipeStyles>` | - | Predefined style recipes (named style bundles) |
71
+ | `presets` | `Record<string, TypographyPreset>` | - | Typography presets — shorthand for `generateTypographyTokens()` |
72
+ | `globalStyles` | `Record<string, Styles>` | - | Global Tasty styles keyed by CSS selector |
73
+ | `plugins` | `TastyPlugin[]` | - | Plugins that bundle `functions`, `units`, `states`, `handlers`, `propHandlers`, `baseStyleProps`, `tokens`, `replaceTokens`, `recipes`, `presets`, `globalStyles`, `properties`, `keyframes`, `fontFaces`, and `counterStyles` (processed in order; later override earlier, and direct config wins over all). See [Plugins](plugins.md) |
74
+ | `gc` | `GCConfig` | - | Turns on automatic collection of unused styles: `{ touchInterval?, capacity?, grace? }` (defaults `1000`, `1000`, `10000` ms). Without it nothing is collected automatically; call `gc()` yourself |
75
+ | `batchInjection` | `boolean \| 'always'` | `false` | Defer stylesheet writes and apply them in one batch. See [Batched injection](#batched-injection) |
76
+ | `colorSpace` | `'rgb' \| 'hsl' \| 'oklch'` | - | **Deprecated** — no longer has any effect. See [Color space](#color-space) |
77
+ | `namePrefix` | `string` | `'t'` (`tasty`) / `'ts'` (build-time) | Prefix prepended to every generated identifier (class, keyframe, counter-style names). Must match `^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$`. See [Name prefix](#name-prefix). |
79
78
 
80
79
  ---
81
80
 
@@ -334,8 +333,7 @@ Tokens support state maps for responsive or theme-aware values:
334
333
  ```jsx
335
334
  configure({
336
335
  tokens: {
337
- $gap: '4px',
338
- $radius: '6px',
336
+ '$sidebar-width': '280px',
339
337
  '#primary': {
340
338
  '': '#purple',
341
339
  '@dark': '#light-purple',
@@ -350,9 +348,10 @@ configure({
350
348
 
351
349
  - `$name` keys become `--name` CSS custom properties
352
350
  - `#name` keys become `--name-color` custom properties
351
+ - Tokens do not change units: `2x` stays `16px` whatever `$gap` is. To drive a unit from a token, point the unit at it: `units: { x: 'var(--gap)' }` makes `2x` compile to `calc(2 * var(--gap))`
353
352
  - Names keep their case, since CSS custom properties are case-sensitive: `$myVar` is `--myVar` and is referenced as `$myVar`. A leading capital is not supported and folds — `$Foo` and `#Purple` become `--foo` and `--purple-color` — so start names lowercase. Kebab-case (`$my-var`) remains the convention.
354
353
 
355
- Tokens are automatically emitted in all delivery modes: client rendering, server rendering, and build-time extraction with the Babel plugin.
354
+ Tokens are emitted by the client injector, by the SSR collector (`TastyRegistry`, `tastyIntegration()`, `runWithCollector()`), by the `withTastyNext()` shared stylesheet, and by the Babel plugin. The inline React Server Component path without a collector does not emit them — see [Server-Side Rendering](ssr.md).
356
355
 
357
356
  ---
358
357
 
@@ -375,6 +374,17 @@ configure({
375
374
 
376
375
  When a component uses `padding: '$card-padding'`, the parser replaces it with `'4x'` before generating CSS. When a component uses `fill: '#accent'`, it is replaced with `'#purple'`, which in turn resolves to `var(--purple-color)`.
377
376
 
377
+ Tasty includes `'#clear': 'transparent'` as a default replace token, available
378
+ without calling `configure()`. It survives `resetConfig()`, and explicit
379
+ `replaceTokens` entries can override it. A replace token resolving to the literal
380
+ `transparent` ignores numeric and custom-property opacity suffixes:
381
+ `#clear`, `#clear.5`, and `#clear.$opacity` all emit `transparent`.
382
+
383
+ `#clear` no longer reads `--clear-color`, and Tasty no longer registers that
384
+ property by default. If you previously customized `#clear` through `tokens`,
385
+ local token definitions, or CSS, use a different color token or explicitly
386
+ override `replaceTokens['#clear']` instead.
387
+
378
388
  See [Replace Tokens](dsl.md#replace-tokens) in the Style DSL reference.
379
389
 
380
390
  ---
@@ -471,7 +481,7 @@ See [Functions (`@function`)](dsl.md#functions-function) for inline usage inside
471
481
 
472
482
  ### Custom color functions
473
483
 
474
- A parse function whose output is an already-supported color (`rgb`, `hsl`, `#…`, `oklch`, …) is treated as a **color function**: it works everywhere a color is accepted — style values, `#token.alpha` opacity injection, and `parseColor` — with no extra registration. This is the same mechanism the built-in `okhsl`/`okhst` plugins use; they are ordinary plugins registered by default.
484
+ A parse function whose output is an already-supported color (`rgb`, `hsl`, `#…`, `oklch`, …) is treated as a **color function**: it works everywhere a color is accepted — style values, `parseColor`, and the `#token.alpha` opacity suffix, which calls the function with a `/ alpha` part appended (read `groups[0].parts[1]`, as below) — with no extra registration. This is the same mechanism the built-in `okhsl`/`okhst` plugins use; they are ordinary plugins registered by default.
475
485
 
476
486
  ```ts
477
487
  import { configure, createColorFunc } from '@tenphi/tasty';
@@ -482,8 +492,12 @@ const myColorPlugin = () => ({
482
492
  functions: {
483
493
  // Hand-written parse function:
484
494
  mycolor: (groups) => {
485
- const [r, g, b] = groups[0].all;
486
- return `rgb(${r} ${g} ${b})`;
495
+ // `mycolor(255 0 0 / .5)` and `#brand.5` both arrive as two parts
496
+ const [main, alpha] = groups[0].parts;
497
+ const [r, g, b] = main.all;
498
+ return alpha?.all.length
499
+ ? `rgb(${r} ${g} ${b} / ${alpha.output})`
500
+ : `rgb(${r} ${g} ${b})`;
487
501
  },
488
502
  },
489
503
  });
@@ -631,7 +645,7 @@ configure({
631
645
  });
632
646
  ```
633
647
 
634
- Each key is a CSS selector, and each value is a Tasty `Styles` object supporting the full style syntax including style properties, tokens, state maps, and selector-based sub-styling (e.g. `$: '> .app'` for elements outside React scope). Global styles are injected alongside `:root` tokens when the first style is rendered.
648
+ Each key is a CSS selector (a list such as `'.uni-checkbox, uni-checkbox'` styles every item), and each value is a Tasty `Styles` object supporting the full style syntax including style properties, tokens, state maps, `@keyframes`, and selector-based sub-styling (e.g. `$: '> .app'` for elements outside React scope). Global styles are injected alongside `:root` tokens when the first style is rendered.
635
649
 
636
650
  Global styles are automatically emitted in all delivery modes: client rendering, server rendering, and build-time extraction with the Babel plugin. Plugins can also provide `globalStyles`; they are merged per selector with config global styles (config wins on conflict).
637
651
 
@@ -690,10 +704,16 @@ Override or extend the built-in style property handlers. A handler definition ca
690
704
  | Single dep | `['styleName', handler]` | Triggered by the specified style property |
691
705
  | Multi dep | `[['dep1', 'dep2', ...], handler]` | Triggered by any of the listed properties; receives all of them |
692
706
 
693
- The multi-dep form is useful when output depends on several style properties together (e.g., `gap` needs to know `display` and `flow` to decide the CSS strategy). Use `defineHandler` for it and the dependency types are inferred from the dependency list, so a typo in the destructure is a type error instead of a silent `undefined`.
707
+ The multi-dep form is useful when output depends on several style properties together (e.g., `gap` needs to know `display` and `flow` to decide the CSS strategy). A handler whose dependencies include a built-in style takes over that style's whole group — see [Replacing a built-in handler](#replacing-a-built-in-handler). Use `defineHandler` for it and the dependency types are inferred from the dependency list, so a typo in the destructure is a type error instead of a silent `undefined`.
694
708
 
695
709
  ```jsx
696
- import { configure, defineHandler, styleHandlers } from '@tenphi/tasty';
710
+ import {
711
+ configure,
712
+ defineHandler,
713
+ parseColor,
714
+ parseStyle,
715
+ styleHandlers,
716
+ } from '@tenphi/tasty';
697
717
 
698
718
  configure({
699
719
  handlers: {
@@ -717,6 +737,10 @@ configure({
717
737
  'backgroundPosition',
718
738
  'backgroundSize',
719
739
  'backgroundRepeat',
740
+ 'backgroundAttachment',
741
+ 'backgroundOrigin',
742
+ 'backgroundClip',
743
+ 'background',
720
744
  ],
721
745
  (props) => {
722
746
  if (
@@ -729,12 +753,18 @@ configure({
729
753
  },
730
754
  ),
731
755
 
732
- // Multi dep — handler reads multiple style properties
733
- gap: defineHandler(['display', 'flow', 'gap'], ({ display, flow, gap }) => {
734
- if (!gap) return;
735
- const isGrid = String(display ?? '').includes('grid');
736
- return { gap: isGrid ? gap : `/* custom logic for ${flow} */` };
737
- }),
756
+ // Multi dep — triggered by either name, receives both
757
+ stripe: defineHandler(
758
+ ['stripe', 'stripeColor'],
759
+ ({ stripe, stripeColor }) => {
760
+ if (!stripe) return;
761
+ const width = parseStyle(String(stripe)).output;
762
+ const color = stripeColor
763
+ ? parseColor(String(stripeColor)).color
764
+ : 'currentcolor';
765
+ return { 'border-left': `${width} solid ${color}` };
766
+ },
767
+ ),
738
768
  },
739
769
  });
740
770
  ```
@@ -756,12 +786,17 @@ Built-in handlers are shared across several style names. Registering a handler f
756
786
 
757
787
  The shared groups worth knowing:
758
788
 
759
- | Registering a handler for | Also takes over |
760
- | ------------------------------ | --------------------------------------------------------------------------------------------------------- |
761
- | `fill` | `backgroundColor`, `image`, `backgroundImage`, `backgroundPosition`, `backgroundSize`, `backgroundRepeat` |
762
- | `display` | `hide`, `overflow`, `whiteSpace`, `textOverflow`, `flow`, `gap` |
763
- | `preset` | `font`, `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`, `letterSpacing`, `textTransform` |
764
- | `padding` / `margin` / `inset` | their `*Top`/`*Right`/`*Bottom`/`*Left`/`*Block`/`*Inline` longhands |
789
+ | Registering a handler for | Also takes over |
790
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
791
+ | `fill` | `backgroundColor`, `image`, `backgroundImage`, `backgroundPosition`, `backgroundSize`, `backgroundRepeat`, `backgroundAttachment`, `backgroundOrigin`, `backgroundClip`, `background` |
792
+ | `display` | `hide`, `whiteSpace`, `textOverflow`, `flow`, `gap` |
793
+ | `flow` | `gap` |
794
+ | `preset` | `font`, `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`, `letterSpacing`, `textTransform` |
795
+ | `padding` / `margin` | their `Top`/`Right`/`Bottom`/`Left` longhands (`paddingTop`, `marginLeft`, …) |
796
+ | `inset` | `top`, `right`, `bottom`, `left` |
797
+ | `width` / `height` | `minWidth`/`maxWidth`, `minHeight`/`maxHeight` |
798
+
799
+ A multi-dep handler takes over every group its dependencies touch: `defineHandler(['display', 'flow', 'gap'], …)` takes over `display`, `flow`, and `gap` together, so it must emit `display` and `flex-flow` itself.
765
800
 
766
801
  Either declare the whole group and delegate the rest to `styleHandlers.*`, or pick a name that isn't shared.
767
802
 
@@ -822,18 +857,98 @@ Each name costs one property check per render of every component, and the effect
822
857
 
823
858
  ---
824
859
 
825
- ## Extending Style Types (TypeScript)
860
+ ## TypeScript
826
861
 
827
- Use module augmentation to extend the `StylesInterface`:
862
+ Component props are typed from the `tasty()` definition with no setup. Module augmentation adds the names your design system defines and the extensions your plugins add.
863
+
864
+ ### Component props
865
+
866
+ | In `tasty({ … })` | Adds the props |
867
+ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
868
+ | `styleProps: ['gap', 'padding'] as const` | `gap`, `padding`, each typed like the style: a value or a state map |
869
+ | `modProps: { isLoading: Boolean, size: ['sm', 'md', 'lg'] as const }` | `isLoading?: boolean`, `size?: 'sm' \| 'md' \| 'lg'` (`String` gives `string`, `Number` gives `number`) |
870
+ | `modProps: ['isLoading'] as const` | `isLoading?: ModValue` |
871
+ | `tokenProps: ['progress', 'accentColor'] as const` | `progress?: TokenValue`, `accentColor?: TokenValue` |
872
+ | `variants: { default: {…}, danger: {…} }` | `variant?: 'default' \| 'danger'` |
873
+ | `elements: { Title: 'h3' }` | `Card.Title`, with the `h3` props plus `qa`, `qaVal`, `mods`, `tokens`, `isDisabled`, `isHidden`, `isChecked`, and a `ref` typed as `HTMLHeadingElement`; no `styles` |
874
+ | `as: SomeComponent` | `SomeComponent`'s own props, required ones still required |
875
+
876
+ Write the array forms with `as const`. Without it, `modProps: ['isLoading']` widens to `string[]` and every prop on the component is typed `ModValue`, so an `onClick` handler no longer type-checks. A `modProps` value list without `as const` widens to `string`.
877
+
878
+ `tasty(Base, { … })` keeps `Base`'s prop types and adds none for the options above, which that form ignores — see [Extend an existing component](react-api.md#extend-an-existing-component).
879
+
880
+ ### Registering your names and extensions
881
+
882
+ ```ts
883
+ // tasty.d.ts — keep the import: it makes this file a module, so the block
884
+ // below extends '@tenphi/tasty' instead of replacing it.
885
+ import '@tenphi/tasty';
828
886
 
829
- ```tsx
830
- // tasty.d.ts
831
887
  declare module '@tenphi/tasty' {
888
+ interface TastyNamedColors {
889
+ surface: true;
890
+ text: true;
891
+ primary: true;
892
+ }
893
+ interface TastyPresetNames {
894
+ h1: true;
895
+ t2: true;
896
+ }
897
+ interface TastyThemeNames {
898
+ danger: true;
899
+ success: true;
900
+ }
832
901
  interface StylesInterface {
833
- elevation?: string;
834
- gradient?: string;
902
+ stripe?: string;
903
+ }
904
+ interface TastyCustomProps {
905
+ glaze: 'soft' | 'strong' | { tone: string; intensity?: number };
906
+ }
907
+ interface TastyBaseStylePropNames {
908
+ radius: true;
909
+ stripe: true;
835
910
  }
836
911
  }
837
912
  ```
838
913
 
914
+ | Interface | Register | What it types |
915
+ | ------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
916
+ | `TastyNamedColors` | Color token names from `tokens` and `replaceTokens`, without the `#` | Suggestions for `#name` and `#name.50` in `fill`, `color`, and `svgFill` |
917
+ | `TastyPresetNames` | `presets` names | The `preset` style and prop |
918
+ | `TastyThemeNames` | Theme names | The `theme` prop (rendered as `data-theme`) |
919
+ | `StylesInterface` | Style properties added with `handlers` | Their value type in `styles`, `styleProps`, and base style props |
920
+ | `TastyCustomProps` | Props added with `propHandlers` | An optional prop on every `tasty()` component (not on sub-elements) |
921
+ | `TastyBaseStylePropNames` | Names passed to `baseStyleProps` | A prop on every component, typed like the style it names |
922
+
923
+ - The first three add autocomplete, not checking: any other string still type-checks (`preset: 'h9'` compiles). The ESLint plugin is what reports unknown names.
924
+ - In `StylesInterface`, declare the plain value type (`stripe?: string`). Tasty adds state-map support itself, as it does for built-in styles.
925
+ - A custom style name in `styles` type-checks without augmentation, but `styleProps` and `TastyBaseStylePropNames` accept only names in `StylesInterface`. A base style prop name missing from it adds no prop.
926
+ - `TastyCustomProps` types the component prop, not the handler. A prop handler receives `Record<string, unknown>`, so cast inside it (`rest.styles as Styles`).
927
+ - State keys, `$` value tokens, units, and recipe names have no TypeScript types. The ESLint plugin checks them against `tasty.config.ts`.
928
+
929
+ ### `tasty.config.ts`
930
+
931
+ TypeScript augmentation does not reach the linter or the editor. [`@tenphi/eslint-plugin-tasty`](https://github.com/tenphi/eslint-plugin-tasty) and the Tasty VS Code extension read a `tasty.config.ts` (or `.js`, `.json`) at the project root instead. It only lists names: it is not passed to `configure()`, and it is unrelated to the Babel plugin's `configFile` (such as `tasty-zero.config.ts`).
932
+
933
+ ```ts
934
+ // tasty.config.ts
935
+ import type { TastyExtensionConfig } from '@tenphi/tasty';
936
+ import type { TastyValidationConfig } from '@tenphi/eslint-plugin-tasty';
937
+
938
+ export default {
939
+ tokens: ['#surface', '#text', '#primary', '$sidebar-width'],
940
+ units: ['cols'],
941
+ states: ['@mobile', '@dark'],
942
+ presets: ['h1', 't2'],
943
+ recipes: ['card', 'elevated'],
944
+ styles: ['stripe'],
945
+ importSources: ['@my-org/ui'],
946
+ presetDescriptions: { h1: 'Heading 1 (32px, bold)' },
947
+ } satisfies TastyExtensionConfig & TastyValidationConfig;
948
+ ```
949
+
950
+ `TastyExtensionConfig` from `@tenphi/tasty` types the fields the VS Code extension reads: `extends`, `tokens`, `units`, `functions`, `states`, `presets`, `styles`, `presetDescriptions`, and `stateDescriptions`. It has no `recipes`, `importSources`, `ownedSources`, or `styleFunctions`, so annotating the file with it alone rejects those ESLint fields; intersect it with the plugin's `TastyValidationConfig`, as above. An omitted list is not checked; `tokens: false`, `units: false`, or `functions: false` turns a check off even when an `extends` parent sets it.
951
+
952
+ Keep it in step with `configure()`: token names with their `#` or `$`, custom units, parse-function names, state aliases, presets, recipes, and custom style names.
953
+
839
954
  See [Style DSL](dsl.md) for state maps, tokens, units, and extending semantics, and [React API](react-api.md) for `tasty()`, style functions, and component props.
package/docs/debug.md CHANGED
@@ -78,7 +78,7 @@ Retrieves CSS text for a given target. Logs the result with rule count and size.
78
78
  |---|---|
79
79
  | `'all'` | All tasty CSS (component + global + raw) |
80
80
  | `'active'` | CSS for classes currently in the DOM |
81
- | `'unused'` | CSS for injected classes no element carries (cached but not rendered) |
81
+ | `'unused'` | CSS for classes `gc({ force: true })` would delete now — not rendered, not pinned, past the `gc.grace` window |
82
82
  | `'global'` | Only global CSS (from `injectGlobal`) |
83
83
  | `'page'` | All CSS on the page (including non-tasty) |
84
84
  | `'t3a5f'` | CSS for a specific tasty class (class names are `t` + base36 hash) |
@@ -154,7 +154,8 @@ One-shot overview of the entire Tasty CSS state. Logs a compact report.
154
154
  ```typescript
155
155
  interface Summary {
156
156
  activeClasses: string[];
157
- unusedClasses: string[];
157
+ unusedClasses: string[]; // classes gc({ force: true }) would delete now
158
+ hotClasses: string[]; // not rendered, but pinned or inside gc.grace — not yet collectable
158
159
  totalStyledClasses: string[];
159
160
 
160
161
  activeCSSSize: number;
@@ -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