@tenphi/tasty 0.0.0-snapshot.fafcbfe → 0.0.0-snapshot.fdb16ea

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 (309) hide show
  1. package/README.md +397 -185
  2. package/dist/async-storage-DKK-wTD4.js +44 -0
  3. package/dist/async-storage-DKK-wTD4.js.map +1 -0
  4. package/dist/babel-CMYf4JZ2.d.ts +83 -0
  5. package/dist/collector-Ctn5s74s.d.ts +145 -0
  6. package/dist/collector-DMSR6ANK.js +304 -0
  7. package/dist/collector-DMSR6ANK.js.map +1 -0
  8. package/dist/config-B23gZrfP.js +12317 -0
  9. package/dist/config-B23gZrfP.js.map +1 -0
  10. package/dist/config-CZx8_DCu.d.ts +1296 -0
  11. package/dist/context-CA8YKeMn.js +24 -0
  12. package/dist/context-CA8YKeMn.js.map +1 -0
  13. package/dist/core/index.d.ts +5 -33
  14. package/dist/core/index.js +6 -26
  15. package/dist/core-B1tyn69d.js +1573 -0
  16. package/dist/core-B1tyn69d.js.map +1 -0
  17. package/dist/css-writer-CvAxkR6S.js +390 -0
  18. package/dist/css-writer-CvAxkR6S.js.map +1 -0
  19. package/dist/format-global-rules-DklyaXv-.js +22 -0
  20. package/dist/format-global-rules-DklyaXv-.js.map +1 -0
  21. package/dist/format-rules-BCTWVvcS.js +130 -0
  22. package/dist/format-rules-BCTWVvcS.js.map +1 -0
  23. package/dist/hydrate-BCPvkJj3.js +37 -0
  24. package/dist/hydrate-BCPvkJj3.js.map +1 -0
  25. package/dist/index-B3o0aCYV.d.ts +1538 -0
  26. package/dist/index-CsvB0EXZ.d.ts +1909 -0
  27. package/dist/index.d.ts +5 -40
  28. package/dist/index.js +921 -32
  29. package/dist/index.js.map +1 -0
  30. package/dist/keyframes-Dxdx3KOo.js +493 -0
  31. package/dist/keyframes-Dxdx3KOo.js.map +1 -0
  32. package/dist/{utils/merge-styles.js → merge-styles-C-uwpoNW.js} +4 -6
  33. package/dist/merge-styles-C-uwpoNW.js.map +1 -0
  34. package/dist/{utils/merge-styles.d.ts → merge-styles-DJTSfz_M.d.ts} +3 -3
  35. package/dist/{utils/resolve-recipes.js → resolve-recipes-BuOvSEa2.js} +5 -8
  36. package/dist/resolve-recipes-BuOvSEa2.js.map +1 -0
  37. package/dist/ssr/astro-client.d.ts +1 -0
  38. package/dist/ssr/astro-client.js +19 -0
  39. package/dist/ssr/astro-client.js.map +1 -0
  40. package/dist/ssr/astro-middleware-static.d.ts +16 -0
  41. package/dist/ssr/astro-middleware-static.js +18 -0
  42. package/dist/ssr/astro-middleware-static.js.map +1 -0
  43. package/dist/ssr/astro-middleware.d.ts +17 -0
  44. package/dist/ssr/astro-middleware.js +19 -0
  45. package/dist/ssr/astro-middleware.js.map +1 -0
  46. package/dist/ssr/astro.d.ts +93 -0
  47. package/dist/ssr/astro.js +157 -0
  48. package/dist/ssr/astro.js.map +1 -0
  49. package/dist/ssr/index.d.ts +37 -0
  50. package/dist/ssr/index.js +10 -0
  51. package/dist/ssr/index.js.map +1 -0
  52. package/dist/ssr/next.d.ts +45 -0
  53. package/dist/ssr/next.js +74 -0
  54. package/dist/ssr/next.js.map +1 -0
  55. package/dist/static/index.d.ts +91 -5
  56. package/dist/static/index.js +49 -4
  57. package/dist/static/index.js.map +1 -0
  58. package/dist/static/inject.d.ts +5 -0
  59. package/dist/static/inject.js +17 -0
  60. package/dist/static/inject.js.map +1 -0
  61. package/dist/zero/babel.d.ts +2 -108
  62. package/dist/zero/babel.js +235 -39
  63. package/dist/zero/babel.js.map +1 -1
  64. package/dist/zero/index.d.ts +81 -3
  65. package/dist/zero/index.js +2 -4
  66. package/dist/zero/next.d.ts +56 -30
  67. package/dist/zero/next.js +106 -41
  68. package/dist/zero/next.js.map +1 -1
  69. package/docs/README.md +39 -0
  70. package/docs/adoption.md +323 -0
  71. package/docs/ai-agents.md +224 -0
  72. package/docs/comparison.md +448 -0
  73. package/docs/configuration.md +823 -0
  74. package/docs/debug.md +322 -0
  75. package/docs/design-system.md +455 -0
  76. package/docs/dsl.md +1021 -0
  77. package/docs/getting-started.md +238 -0
  78. package/docs/injector.md +614 -0
  79. package/docs/methodology.md +648 -0
  80. package/docs/migration-v3.md +285 -0
  81. package/docs/pipeline.md +741 -0
  82. package/docs/plugins.md +353 -0
  83. package/docs/react-api.md +681 -0
  84. package/docs/runtime-benchmarks.md +216 -0
  85. package/docs/ssr.md +451 -0
  86. package/docs/styles.md +645 -0
  87. package/docs/tasty-static.md +582 -0
  88. package/package.json +149 -40
  89. package/tasty.config.ts +6 -0
  90. package/dist/_virtual/_rolldown/runtime.js +0 -8
  91. package/dist/chunks/cacheKey.js +0 -70
  92. package/dist/chunks/cacheKey.js.map +0 -1
  93. package/dist/chunks/definitions.d.ts +0 -37
  94. package/dist/chunks/definitions.js +0 -260
  95. package/dist/chunks/definitions.js.map +0 -1
  96. package/dist/chunks/renderChunk.js +0 -61
  97. package/dist/chunks/renderChunk.js.map +0 -1
  98. package/dist/config.d.ts +0 -280
  99. package/dist/config.js +0 -403
  100. package/dist/config.js.map +0 -1
  101. package/dist/debug.d.ts +0 -204
  102. package/dist/debug.js +0 -733
  103. package/dist/debug.js.map +0 -1
  104. package/dist/hooks/useGlobalStyles.d.ts +0 -27
  105. package/dist/hooks/useGlobalStyles.js +0 -56
  106. package/dist/hooks/useGlobalStyles.js.map +0 -1
  107. package/dist/hooks/useKeyframes.d.ts +0 -56
  108. package/dist/hooks/useKeyframes.js +0 -54
  109. package/dist/hooks/useKeyframes.js.map +0 -1
  110. package/dist/hooks/useProperty.d.ts +0 -79
  111. package/dist/hooks/useProperty.js +0 -91
  112. package/dist/hooks/useProperty.js.map +0 -1
  113. package/dist/hooks/useRawCSS.d.ts +0 -53
  114. package/dist/hooks/useRawCSS.js +0 -28
  115. package/dist/hooks/useRawCSS.js.map +0 -1
  116. package/dist/hooks/useStyles.d.ts +0 -40
  117. package/dist/hooks/useStyles.js +0 -169
  118. package/dist/hooks/useStyles.js.map +0 -1
  119. package/dist/injector/index.d.ts +0 -157
  120. package/dist/injector/index.js +0 -154
  121. package/dist/injector/index.js.map +0 -1
  122. package/dist/injector/injector.d.ts +0 -139
  123. package/dist/injector/injector.js +0 -404
  124. package/dist/injector/injector.js.map +0 -1
  125. package/dist/injector/sheet-manager.d.ts +0 -127
  126. package/dist/injector/sheet-manager.js +0 -714
  127. package/dist/injector/sheet-manager.js.map +0 -1
  128. package/dist/injector/types.d.ts +0 -135
  129. package/dist/keyframes/index.js +0 -206
  130. package/dist/keyframes/index.js.map +0 -1
  131. package/dist/parser/classify.js +0 -319
  132. package/dist/parser/classify.js.map +0 -1
  133. package/dist/parser/const.js +0 -33
  134. package/dist/parser/const.js.map +0 -1
  135. package/dist/parser/lru.js +0 -109
  136. package/dist/parser/lru.js.map +0 -1
  137. package/dist/parser/parser.d.ts +0 -25
  138. package/dist/parser/parser.js +0 -116
  139. package/dist/parser/parser.js.map +0 -1
  140. package/dist/parser/tokenizer.js +0 -69
  141. package/dist/parser/tokenizer.js.map +0 -1
  142. package/dist/parser/types.d.ts +0 -51
  143. package/dist/parser/types.js +0 -46
  144. package/dist/parser/types.js.map +0 -1
  145. package/dist/pipeline/conditions.d.ts +0 -134
  146. package/dist/pipeline/conditions.js +0 -400
  147. package/dist/pipeline/conditions.js.map +0 -1
  148. package/dist/pipeline/exclusive.js +0 -231
  149. package/dist/pipeline/exclusive.js.map +0 -1
  150. package/dist/pipeline/index.d.ts +0 -53
  151. package/dist/pipeline/index.js +0 -641
  152. package/dist/pipeline/index.js.map +0 -1
  153. package/dist/pipeline/materialize.js +0 -908
  154. package/dist/pipeline/materialize.js.map +0 -1
  155. package/dist/pipeline/parseStateKey.d.ts +0 -15
  156. package/dist/pipeline/parseStateKey.js +0 -430
  157. package/dist/pipeline/parseStateKey.js.map +0 -1
  158. package/dist/pipeline/simplify.js +0 -557
  159. package/dist/pipeline/simplify.js.map +0 -1
  160. package/dist/plugins/okhsl-plugin.d.ts +0 -35
  161. package/dist/plugins/okhsl-plugin.js +0 -371
  162. package/dist/plugins/okhsl-plugin.js.map +0 -1
  163. package/dist/plugins/types.d.ts +0 -69
  164. package/dist/properties/index.js +0 -158
  165. package/dist/properties/index.js.map +0 -1
  166. package/dist/states/index.d.ts +0 -49
  167. package/dist/states/index.js +0 -416
  168. package/dist/states/index.js.map +0 -1
  169. package/dist/static/tastyStatic.d.ts +0 -46
  170. package/dist/static/tastyStatic.js +0 -31
  171. package/dist/static/tastyStatic.js.map +0 -1
  172. package/dist/static/types.d.ts +0 -49
  173. package/dist/static/types.js +0 -24
  174. package/dist/static/types.js.map +0 -1
  175. package/dist/styles/align.d.ts +0 -15
  176. package/dist/styles/align.js +0 -14
  177. package/dist/styles/align.js.map +0 -1
  178. package/dist/styles/border.d.ts +0 -25
  179. package/dist/styles/border.js +0 -114
  180. package/dist/styles/border.js.map +0 -1
  181. package/dist/styles/color.d.ts +0 -14
  182. package/dist/styles/color.js +0 -23
  183. package/dist/styles/color.js.map +0 -1
  184. package/dist/styles/createStyle.js +0 -77
  185. package/dist/styles/createStyle.js.map +0 -1
  186. package/dist/styles/dimension.js +0 -97
  187. package/dist/styles/dimension.js.map +0 -1
  188. package/dist/styles/display.d.ts +0 -37
  189. package/dist/styles/display.js +0 -67
  190. package/dist/styles/display.js.map +0 -1
  191. package/dist/styles/fade.d.ts +0 -15
  192. package/dist/styles/fade.js +0 -58
  193. package/dist/styles/fade.js.map +0 -1
  194. package/dist/styles/fill.d.ts +0 -44
  195. package/dist/styles/fill.js +0 -51
  196. package/dist/styles/fill.js.map +0 -1
  197. package/dist/styles/flow.d.ts +0 -16
  198. package/dist/styles/flow.js +0 -12
  199. package/dist/styles/flow.js.map +0 -1
  200. package/dist/styles/gap.d.ts +0 -31
  201. package/dist/styles/gap.js +0 -37
  202. package/dist/styles/gap.js.map +0 -1
  203. package/dist/styles/height.d.ts +0 -17
  204. package/dist/styles/height.js +0 -20
  205. package/dist/styles/height.js.map +0 -1
  206. package/dist/styles/index.d.ts +0 -2
  207. package/dist/styles/index.js +0 -9
  208. package/dist/styles/index.js.map +0 -1
  209. package/dist/styles/inset.d.ts +0 -52
  210. package/dist/styles/inset.js +0 -150
  211. package/dist/styles/inset.js.map +0 -1
  212. package/dist/styles/justify.d.ts +0 -15
  213. package/dist/styles/justify.js +0 -14
  214. package/dist/styles/justify.js.map +0 -1
  215. package/dist/styles/list.d.ts +0 -16
  216. package/dist/styles/list.js +0 -98
  217. package/dist/styles/list.js.map +0 -1
  218. package/dist/styles/margin.d.ts +0 -24
  219. package/dist/styles/margin.js +0 -104
  220. package/dist/styles/margin.js.map +0 -1
  221. package/dist/styles/outline.d.ts +0 -29
  222. package/dist/styles/outline.js +0 -65
  223. package/dist/styles/outline.js.map +0 -1
  224. package/dist/styles/padding.d.ts +0 -24
  225. package/dist/styles/padding.js +0 -104
  226. package/dist/styles/padding.js.map +0 -1
  227. package/dist/styles/predefined.d.ts +0 -73
  228. package/dist/styles/predefined.js +0 -241
  229. package/dist/styles/predefined.js.map +0 -1
  230. package/dist/styles/preset.d.ts +0 -47
  231. package/dist/styles/preset.js +0 -126
  232. package/dist/styles/preset.js.map +0 -1
  233. package/dist/styles/radius.d.ts +0 -14
  234. package/dist/styles/radius.js +0 -51
  235. package/dist/styles/radius.js.map +0 -1
  236. package/dist/styles/scrollbar.d.ts +0 -21
  237. package/dist/styles/scrollbar.js +0 -105
  238. package/dist/styles/scrollbar.js.map +0 -1
  239. package/dist/styles/shadow.d.ts +0 -14
  240. package/dist/styles/shadow.js +0 -24
  241. package/dist/styles/shadow.js.map +0 -1
  242. package/dist/styles/styledScrollbar.d.ts +0 -47
  243. package/dist/styles/styledScrollbar.js +0 -38
  244. package/dist/styles/styledScrollbar.js.map +0 -1
  245. package/dist/styles/transition.d.ts +0 -14
  246. package/dist/styles/transition.js +0 -158
  247. package/dist/styles/transition.js.map +0 -1
  248. package/dist/styles/types.d.ts +0 -498
  249. package/dist/styles/width.d.ts +0 -17
  250. package/dist/styles/width.js +0 -20
  251. package/dist/styles/width.js.map +0 -1
  252. package/dist/tasty.d.ts +0 -982
  253. package/dist/tasty.js +0 -191
  254. package/dist/tasty.js.map +0 -1
  255. package/dist/tokens/typography.d.ts +0 -19
  256. package/dist/tokens/typography.js +0 -237
  257. package/dist/tokens/typography.js.map +0 -1
  258. package/dist/types.d.ts +0 -184
  259. package/dist/utils/cache-wrapper.js +0 -26
  260. package/dist/utils/cache-wrapper.js.map +0 -1
  261. package/dist/utils/case-converter.js +0 -8
  262. package/dist/utils/case-converter.js.map +0 -1
  263. package/dist/utils/colors.d.ts +0 -5
  264. package/dist/utils/colors.js +0 -9
  265. package/dist/utils/colors.js.map +0 -1
  266. package/dist/utils/css-types.d.ts +0 -7
  267. package/dist/utils/dotize.d.ts +0 -26
  268. package/dist/utils/dotize.js +0 -122
  269. package/dist/utils/dotize.js.map +0 -1
  270. package/dist/utils/filter-base-props.d.ts +0 -15
  271. package/dist/utils/filter-base-props.js +0 -45
  272. package/dist/utils/filter-base-props.js.map +0 -1
  273. package/dist/utils/get-display-name.d.ts +0 -7
  274. package/dist/utils/get-display-name.js +0 -10
  275. package/dist/utils/get-display-name.js.map +0 -1
  276. package/dist/utils/hsl-to-rgb.js +0 -38
  277. package/dist/utils/hsl-to-rgb.js.map +0 -1
  278. package/dist/utils/is-dev-env.js +0 -19
  279. package/dist/utils/is-dev-env.js.map +0 -1
  280. package/dist/utils/is-valid-element-type.js +0 -15
  281. package/dist/utils/is-valid-element-type.js.map +0 -1
  282. package/dist/utils/merge-styles.js.map +0 -1
  283. package/dist/utils/mod-attrs.d.ts +0 -8
  284. package/dist/utils/mod-attrs.js +0 -21
  285. package/dist/utils/mod-attrs.js.map +0 -1
  286. package/dist/utils/okhsl-to-rgb.js +0 -296
  287. package/dist/utils/okhsl-to-rgb.js.map +0 -1
  288. package/dist/utils/process-tokens.d.ts +0 -31
  289. package/dist/utils/process-tokens.js +0 -171
  290. package/dist/utils/process-tokens.js.map +0 -1
  291. package/dist/utils/resolve-recipes.d.ts +0 -17
  292. package/dist/utils/resolve-recipes.js.map +0 -1
  293. package/dist/utils/string.js +0 -8
  294. package/dist/utils/string.js.map +0 -1
  295. package/dist/utils/styles.d.ts +0 -183
  296. package/dist/utils/styles.js +0 -585
  297. package/dist/utils/styles.js.map +0 -1
  298. package/dist/utils/typography.d.ts +0 -36
  299. package/dist/utils/typography.js +0 -53
  300. package/dist/utils/typography.js.map +0 -1
  301. package/dist/utils/warnings.d.ts +0 -16
  302. package/dist/utils/warnings.js +0 -16
  303. package/dist/utils/warnings.js.map +0 -1
  304. package/dist/zero/css-writer.d.ts +0 -45
  305. package/dist/zero/css-writer.js +0 -74
  306. package/dist/zero/css-writer.js.map +0 -1
  307. package/dist/zero/extractor.d.ts +0 -24
  308. package/dist/zero/extractor.js +0 -150
  309. package/dist/zero/extractor.js.map +0 -1
@@ -0,0 +1,353 @@
1
+ # Plugins & Extension Points
2
+
3
+ How to extend Tasty: new value syntax, new units, new states, new style properties, new component props, and how to package any of it as a plugin.
4
+
5
+ - [What a plugin is](#what-a-plugin-is)
6
+ - [Choosing an extension point](#choosing-an-extension-point)
7
+ - [Custom style handlers](#custom-style-handlers)
8
+ - [Props middleware](#props-middleware)
9
+ - [Base style props](#base-style-props)
10
+ - [Typing your extension](#typing-your-extension)
11
+ - [Worked example: a `glaze` plugin](#worked-example-a-glaze-plugin)
12
+ - [Rendering modes and caveats](#rendering-modes-and-caveats)
13
+ - [Testing a plugin](#testing-a-plugin)
14
+
15
+ ---
16
+
17
+ ## What a plugin is
18
+
19
+ A plugin is a plain object with a `name` and any subset of the `configure()` options it is allowed to supply. There is no lifecycle and no init hook — a plugin is just pre-packaged configuration.
20
+
21
+ ```ts
22
+ import { configure } from '@tenphi/tasty';
23
+ import type { TastyPluginFactory } from '@tenphi/tasty';
24
+
25
+ const spacingPlugin: TastyPluginFactory = () => ({
26
+ name: 'spacing',
27
+ units: { gu: (n) => `calc(${n} * var(--grid-unit))` },
28
+ tokens: { '$grid-unit': '4px' },
29
+ });
30
+
31
+ configure({ plugins: [spacingPlugin()] });
32
+ ```
33
+
34
+ `TastyPluginFactory<TOptions>` types a factory that takes options; with no type argument it types a zero-argument factory.
35
+
36
+ **Ordering.** Plugins are applied in array order and merged key by key, so a later plugin overrides an earlier one for the same key. Options passed directly to `configure()` are applied last and win over every plugin.
37
+
38
+ **Configuration is locked after the first render.** `configure()` warns and does nothing once any style has been generated, so it must run before your first component renders — at module scope in your entry file, or in a design-system module every component imports. `resetConfig()` reopens it, but it exists for tests.
39
+
40
+ **Repeated `configure()` calls merge** for `tokens` and `globalStyles` (per selector). See [Configuration](configuration.md) for the current per-key semantics.
41
+
42
+ ---
43
+
44
+ ## Choosing an extension point
45
+
46
+ | You want to add | Use | Runs | Build-time extraction |
47
+ | ------------------------------------------- | -------------------------- | ---------------------------- | ----------------------- |
48
+ | New value syntax — `okhsl(…)`, `double(2x)` | `functions` (bare key) | parse time, per value | ✅ |
49
+ | A reusable CSS `@function` | `functions` (`$$name` key) | injected once, globally | ✅ |
50
+ | A new unit — `2gu` | `units` | parse time, per value | ✅ |
51
+ | A new state alias — `@mobile` | `states` | state-key parse | ✅ |
52
+ | A style property → **CSS declarations** | `handlers` | pipeline, per state snapshot | ✅ |
53
+ | A style property on every component | `baseStyleProps` | prop harvest | ✅ (styles only) |
54
+ | A **component prop** → anything | `propHandlers` | render, per component | ❌ (no props to run on) |
55
+ | A named bundle of styles | `recipes` | before chunking | ✅ |
56
+ | Typography scale tokens | `presets` | inject time | ✅ |
57
+ | `:root` variables | `tokens`, `replaceTokens` | inject time | ✅ |
58
+ | Global selector styles | `globalStyles` | inject time | ✅ |
59
+
60
+ Two distinctions do most of the work:
61
+
62
+ - **`handlers` vs `propHandlers`.** A handler turns a _style property_ into CSS declarations and knows nothing about components. A prop handler turns a _component prop_ into other props — including `styles`, which then flow through the normal handlers. Reach for `propHandlers` when the input isn't a style value (an options object, a domain concept) or when it should affect more than styles.
63
+ - **`recipes` vs `propHandlers`.** A recipe is a static named bundle. A prop handler computes its styles from a value.
64
+
65
+ ---
66
+
67
+ ## Custom style handlers
68
+
69
+ A handler receives a map of the style properties it declared and returns CSS declarations. See [Configuration → Custom Style Handlers](configuration.md#custom-style-handlers) for the definition forms, return shape, the shared-handler groups you must be careful of, and chunk membership. The essentials:
70
+
71
+ - Values arrive **state-resolved but unparsed** — the raw authored DSL string (`'2x'`, `'#purple.5'`, `true`, `4`). Call the exported `parseStyle()` / `parseColor()` yourself.
72
+ - Return a `CSSMap` with **kebab-case** keys, an array of them, or nothing. `$` is a selector suffix, not a property.
73
+ - Handlers must be pure and synchronous. They are called once per state combination and the results are cached.
74
+ - Use `defineHandler(deps, fn)` for multi-dependency handlers so the dependency types are inferred.
75
+
76
+ ```ts
77
+ import { configure, defineHandler, parseStyle } from '@tenphi/tasty';
78
+
79
+ configure({
80
+ handlers: {
81
+ // Every declaration set the handler returns can target its own selector.
82
+ stripe: defineHandler(['stripe'], ({ stripe }) => {
83
+ if (!stripe) return;
84
+
85
+ const { values } = parseStyle(String(stripe)).groups[0];
86
+
87
+ return [
88
+ { position: 'relative' },
89
+ {
90
+ $: '&::before',
91
+ content: '""',
92
+ position: 'absolute',
93
+ inset: '0 auto 0 0',
94
+ width: values[0],
95
+ background: 'var(--purple-color)',
96
+ },
97
+ ];
98
+ }),
99
+ },
100
+ });
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Props middleware
106
+
107
+ `propHandlers` are middleware over a component's props: props in, props out. They run at the very top of every tasty component's render, before any prop is destructured, so a handler can read and rewrite `styles`, `mods`, `tokens`, `variant`, `as`, `element`, and `qa`, and can strip its own props so they never reach the DOM.
108
+
109
+ ```ts
110
+ import { configure, mergeStyles } from '@tenphi/tasty';
111
+
112
+ configure({
113
+ propHandlers: {
114
+ glaze: (props) => {
115
+ const { glaze, ...rest } = props;
116
+ if (!glaze) return rest;
117
+
118
+ return { ...rest, styles: mergeStyles(glazeStyles(glaze), rest.styles) };
119
+ },
120
+ },
121
+ });
122
+ ```
123
+
124
+ **The key is the trigger.** By default a handler runs only when a prop matching its key is present, so an absent prop costs one property check rather than a call. Override that with a tuple:
125
+
126
+ | Definition | Runs when |
127
+ | ------------------------- | ------------------------------------- |
128
+ | `fn` | a prop named after the key is present |
129
+ | `['glaze', fn]` | `glaze` is present |
130
+ | `[['glaze', 'tint'], fn]` | either is present |
131
+ | `['*', fn]` | always |
132
+
133
+ **Chaining.** Handlers run in registration order — plugins first, then direct `configure()` — and each receives the previous one's output. Registering the same key twice replaces the handler in place, keeping its position.
134
+
135
+ **Returning nothing means unchanged**, with a development-mode warning, because that is almost always a forgotten `return props`. A non-object return is ignored with a warning.
136
+
137
+ ### Rules
138
+
139
+ - **Be pure. Never mutate the input.** Style values are cached by object identity, so mutating a value object in place yields a stale class name _and_ stale CSS. Return fresh objects.
140
+ - **Memoize the styles you build, per input value.** A reference-stable (ideally frozen) styles object lets the cache key reuse its serialization instead of recomputing it on every render. This is the single highest-leverage thing you can do for performance.
141
+ - **Precedence is fixed** and not adjustable from a handler. Styles are merged as `factory defaults → styles → harvested style props`, and injected styles occupy the `styles` slot: they beat a component's own default styles and lose to a style prop passed at the call site. This matches how `recipe` already behaves.
142
+ - **Prefer returning `styles` over bare style props.** Returning `{ fill: '#red' }` only works if `fill` is harvestable on _that_ component (in its `styleProps`, or promoted via `baseStyleProps`); otherwise it leaks to the DOM.
143
+
144
+ ### What it cannot do
145
+
146
+ - **`ref`** — `forwardRef` separates it from props, so it is out of reach.
147
+ - **Sub-elements** (`Card.Title`) — they have no `styles` prop and no style-prop harvest, so middleware does not run on them.
148
+ - **Build-time extraction** — see [Rendering modes](#rendering-modes-and-caveats).
149
+
150
+ ---
151
+
152
+ ## Base style props
153
+
154
+ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteSpace`, `opacity`, `transition` — are harvested as props on every `tasty()` component. `baseStyleProps` adds to that set globally:
155
+
156
+ ```ts
157
+ configure({ baseStyleProps: ['radius', 'shadow'] });
158
+
159
+ <Card radius="1r" shadow />
160
+ ```
161
+
162
+ Names must be real style properties, must start with a lowercase letter, and must not collide with a prop `tasty()` consumes itself (`as`, `styles`, `variant`, `mods`, `tokens`, …); invalid entries are dropped with a development warning.
163
+
164
+ `configure()` may run _after_ your components are defined — each factory resolves its prop list lazily and refreshes when the registry changes.
165
+
166
+ **Costs and caveats.** Each name adds one property check per render of every component, forever, so keep the list short. The effect is app-global and cannot be scoped to a subtree — use a factory's own `styleProps` for that. Be wary of names that collide with real DOM or component props (`width`, `size`, `color`): promoting one means every component swallows it as a style, including when rendering `as={SomeThirdPartyComponent}`.
167
+
168
+ ---
169
+
170
+ ## Typing your extension
171
+
172
+ Four augmentation points, each matching a runtime registry:
173
+
174
+ ```ts
175
+ // tasty.d.ts
176
+ import type { StylePropValue } from '@tenphi/tasty';
177
+
178
+ declare module '@tenphi/tasty' {
179
+ // configure({ handlers }) — a new style property
180
+ interface StylesInterface {
181
+ stripe?: StylePropValue<string>;
182
+ }
183
+
184
+ // configure({ propHandlers }) — a new component prop
185
+ interface TastyCustomProps {
186
+ glaze: 'soft' | 'strong' | { tone: string; intensity?: number };
187
+ }
188
+
189
+ // configure({ baseStyleProps }) — promoted style names, typed like the style
190
+ interface TastyBaseStylePropNames {
191
+ radius: true;
192
+ shadow: true;
193
+ }
194
+
195
+ // configure({ tokens }) / recipes / presets — autocomplete for names
196
+ interface TastyNamedColors {
197
+ 'glaze-bg': true;
198
+ }
199
+ }
200
+ ```
201
+
202
+ `TastyCustomProps` keys become optional props on every component. `TastyBaseStylePropNames` entries are typed exactly like the style they name, so `radius="1r"` accepts the same values as `styles={{ radius: '1r' }}`.
203
+
204
+ **Also update `tasty.config.ts`.** The ESLint plugin and VS Code extension validate style and token names against it, and neither is derived from the runtime registries:
205
+
206
+ ```ts
207
+ // tasty.config.ts
208
+ export default {
209
+ styles: ['stripe'],
210
+ tokens: ['#glaze-bg'],
211
+ };
212
+ ```
213
+
214
+ `propHandlers` keys are JSX props rather than style keys, so they need no entry — but any _style_ name or token a handler expands into does.
215
+
216
+ ---
217
+
218
+ ## Worked example: a `glaze` plugin
219
+
220
+ A `glaze` prop on every component that takes a configuration object and expands into color token declarations. This is the case `propHandlers` exists for: the value is an options object, not a style value, so it cannot be a style property.
221
+
222
+ ```ts
223
+ // glaze-plugin.ts
224
+ import { mergeStyles } from '@tenphi/tasty';
225
+ import type { Styles, TastyPluginFactory } from '@tenphi/tasty';
226
+
227
+ export interface GlazeConfig {
228
+ tone: string;
229
+ /** 0–100. Default 10. */
230
+ intensity?: number;
231
+ }
232
+
233
+ type GlazeValue = string | GlazeConfig;
234
+
235
+ // Memoized per value so the styles object is reference-stable across renders.
236
+ const cache = new Map<string, Styles>();
237
+
238
+ function glazeStyles(value: GlazeValue): Styles {
239
+ const { tone, intensity = 10 } =
240
+ typeof value === 'string' ? { tone: value } : value;
241
+ const key = `${tone}:${intensity}`;
242
+
243
+ let styles = cache.get(key);
244
+
245
+ if (!styles) {
246
+ styles = Object.freeze({
247
+ '#glaze-bg': `#${tone}.${intensity}`,
248
+ fill: '#glaze-bg',
249
+ transition: 'fill 0.2s',
250
+ }) as Styles;
251
+ cache.set(key, styles);
252
+ }
253
+
254
+ return styles;
255
+ }
256
+
257
+ export const glazePlugin: TastyPluginFactory = () => ({
258
+ name: 'glaze',
259
+
260
+ propHandlers: {
261
+ glaze: (props) => {
262
+ const { glaze, ...rest } = props;
263
+ if (!glaze) return rest;
264
+
265
+ return {
266
+ ...rest,
267
+ styles: mergeStyles(
268
+ glazeStyles(glaze as GlazeValue),
269
+ rest.styles as Styles,
270
+ ),
271
+ };
272
+ },
273
+ },
274
+
275
+ // Typed so `#glaze-bg` animates smoothly instead of snapping.
276
+ properties: {
277
+ '#glaze-bg': {
278
+ syntax: '<color>',
279
+ inherits: false,
280
+ initialValue: 'transparent',
281
+ },
282
+ },
283
+ });
284
+ ```
285
+
286
+ ```ts
287
+ // app entry, before the first render
288
+ import { configure } from '@tenphi/tasty';
289
+
290
+ import { glazePlugin } from './glaze-plugin';
291
+
292
+ configure({ plugins: [glazePlugin()] });
293
+ ```
294
+
295
+ ```tsx
296
+ <Element glaze="purple" />
297
+ <Element glaze={{ tone: 'success', intensity: 30 }} />
298
+ ```
299
+
300
+ ```ts
301
+ // tasty.d.ts
302
+ declare module '@tenphi/tasty' {
303
+ interface TastyCustomProps {
304
+ glaze: import('./glaze-plugin').GlazeConfig | string;
305
+ }
306
+ }
307
+ ```
308
+
309
+ The object value is unambiguous here precisely because `glaze` is a **prop**. A style _value_ of the same shape would be indistinguishable from a state map (`{ tone: … }` looks exactly like `{ hovered: … }`), which is why this belongs in `propHandlers` rather than `handlers`.
310
+
311
+ ---
312
+
313
+ ## Rendering modes and caveats
314
+
315
+ | Mode | `functions` / `units` / `states` / `handlers` / `recipes` / `tokens` | `propHandlers` |
316
+ | -------------------------- | -------------------------------------------------------------------- | -------------- |
317
+ | Client | ✅ | ✅ |
318
+ | SSR / RSC | ✅ | ✅ |
319
+ | Build-time (`tastyStatic`) | ✅ at build time | not applicable |
320
+
321
+ **Build-time extraction.** The Babel plugin transforms `tastyStatic()` calls, which take styles objects — there are no props in that pipeline, so there is nothing for props middleware to run on. Components rendered through `tasty()` are untouched by the plugin; `propHandlers` work whether those components render on the server or in the browser. Everything in the `tastyStatic()` path runs at build time, which is why handlers, functions, and units must be pure functions of their input.
322
+
323
+ **Server and client must configure identically.** Class names are derived from resolved styles, so a handler or promoted prop registered on one side and not the other produces a hydration mismatch. This is the same requirement as `namePrefix` — see [SSR → Hydration mismatch warnings](ssr.md#hydration-mismatch-warnings).
324
+
325
+ ---
326
+
327
+ ## Testing a plugin
328
+
329
+ ```ts
330
+ import { configure, resetConfig, renderStyles } from '@tenphi/tasty';
331
+
332
+ describe('glaze plugin', () => {
333
+ beforeEach(() => resetConfig());
334
+ afterEach(() => resetConfig());
335
+
336
+ it('expands into a color token declaration', () => {
337
+ configure({ plugins: [glazePlugin()] });
338
+
339
+ const rules = renderStyles({ '#glaze-bg': '#purple.10' }, '.test');
340
+
341
+ expect(rules[0].declarations).toContain('--glaze-bg-color');
342
+ });
343
+ });
344
+ ```
345
+
346
+ Two things to know:
347
+
348
+ - **Create components inside each test.** `resetConfig()` does not clear the per-factory class-name cache or prop-list memo, so a module-scope component created in one test carries state into the next.
349
+ - **Dev-mode warnings may not fire.** `isDevEnv()` reports `false` for `NODE_ENV=test`. Warnings that check it lazily can be enabled with `vi.stubEnv('NODE_ENV', 'development')`; some older ones capture it at module load and cannot be triggered from a test at all.
350
+
351
+ ---
352
+
353
+ See [Configuration](configuration.md) for every option in detail, [Style DSL](dsl.md) for the value syntax your extensions produce, and [React API](react-api.md) for `tasty()`, `styleProps`, `modProps`, and `tokenProps`.