@tenphi/tasty 0.0.0-snapshot.d2dcdeb → 0.0.0-snapshot.d4ccea8

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 (357) hide show
  1. package/README.md +181 -131
  2. package/dist/{ssr/async-storage.js → async-storage-DKK-wTD4.js} +2 -2
  3. package/dist/async-storage-DKK-wTD4.js.map +1 -0
  4. package/dist/babel-eMs_zZjM.d.ts +83 -0
  5. package/dist/{ssr/collector.js → collector-BjkQEtbh.js} +111 -38
  6. package/dist/collector-BjkQEtbh.js.map +1 -0
  7. package/dist/{ssr/collector.d.ts → collector-DzeNxHOq.d.ts} +58 -7
  8. package/dist/config-DMslHK_e.d.ts +1298 -0
  9. package/dist/config-DX126v_w.js +12318 -0
  10. package/dist/config-DX126v_w.js.map +1 -0
  11. package/dist/{ssr/context.js → context-CA8YKeMn.js} +11 -3
  12. package/dist/context-CA8YKeMn.js.map +1 -0
  13. package/dist/core/index.d.ts +5 -34
  14. package/dist/core/index.js +6 -27
  15. package/dist/core-CMn-Tj2X.js +1573 -0
  16. package/dist/core-CMn-Tj2X.js.map +1 -0
  17. package/dist/{zero/extractor.js → css-writer-C8ShXK5R.js} +148 -24
  18. package/dist/css-writer-C8ShXK5R.js.map +1 -0
  19. package/dist/{ssr/format-global-rules.js → format-global-rules-DklyaXv-.js} +2 -2
  20. package/dist/format-global-rules-DklyaXv-.js.map +1 -0
  21. package/dist/{ssr/format-rules.js → format-rules-BkstDvlN.js} +59 -2
  22. package/dist/format-rules-BkstDvlN.js.map +1 -0
  23. package/dist/{ssr/hydrate.js → hydrate-DXXWZgm0.js} +4 -12
  24. package/dist/hydrate-DXXWZgm0.js.map +1 -0
  25. package/dist/index-BcsaHo8x.d.ts +1568 -0
  26. package/dist/index-CC0wykKn.d.ts +1909 -0
  27. package/dist/index.d.ts +5 -51
  28. package/dist/index.js +922 -36
  29. package/dist/index.js.map +1 -0
  30. package/dist/keyframes-Z0YLVOWq.js +493 -0
  31. package/dist/keyframes-Z0YLVOWq.js.map +1 -0
  32. package/dist/{utils/merge-styles.d.ts → merge-styles-BTFAkYSD.d.ts} +3 -3
  33. package/dist/{utils/merge-styles.js → merge-styles-Dpueyn82.js} +3 -4
  34. package/dist/merge-styles-Dpueyn82.js.map +1 -0
  35. package/dist/{utils/resolve-recipes.js → resolve-recipes-B2Vjr7ET.js} +4 -6
  36. package/dist/resolve-recipes-B2Vjr7ET.js.map +1 -0
  37. package/dist/ssr/astro-client.js +1 -1
  38. package/dist/ssr/astro-client.js.map +1 -1
  39. package/dist/ssr/astro-middleware-static.d.ts +16 -0
  40. package/dist/ssr/astro-middleware-static.js +18 -0
  41. package/dist/ssr/astro-middleware-static.js.map +1 -0
  42. package/dist/ssr/astro-middleware.d.ts +8 -6
  43. package/dist/ssr/astro-middleware.js +10 -10
  44. package/dist/ssr/astro-middleware.js.map +1 -1
  45. package/dist/ssr/astro.d.ts +2 -17
  46. package/dist/ssr/astro.js +29 -21
  47. package/dist/ssr/astro.js.map +1 -1
  48. package/dist/ssr/index.d.ts +37 -4
  49. package/dist/ssr/index.js +5 -5
  50. package/dist/ssr/index.js.map +1 -1
  51. package/dist/ssr/next.d.ts +2 -3
  52. package/dist/ssr/next.js +7 -8
  53. package/dist/ssr/next.js.map +1 -1
  54. package/dist/static/index.d.ts +91 -5
  55. package/dist/static/index.js +49 -3
  56. package/dist/static/index.js.map +1 -0
  57. package/dist/static/inject.js.map +1 -1
  58. package/dist/zero/babel.d.ts +2 -81
  59. package/dist/zero/babel.js +45 -29
  60. package/dist/zero/babel.js.map +1 -1
  61. package/dist/zero/index.d.ts +81 -3
  62. package/dist/zero/index.js +2 -3
  63. package/dist/zero/next.d.ts +1 -1
  64. package/dist/zero/next.js +1 -1
  65. package/dist/zero/next.js.map +1 -1
  66. package/docs/README.md +18 -10
  67. package/docs/adoption.md +56 -31
  68. package/docs/ai-agents.md +224 -0
  69. package/docs/comparison.md +69 -40
  70. package/docs/configuration.md +501 -72
  71. package/docs/debug.md +22 -18
  72. package/docs/design-system.md +43 -24
  73. package/docs/dsl.md +512 -179
  74. package/docs/getting-started.md +60 -39
  75. package/docs/injector.md +110 -32
  76. package/docs/methodology.md +68 -36
  77. package/docs/migration-v3.md +285 -0
  78. package/docs/pipeline.md +741 -0
  79. package/docs/plugins.md +353 -0
  80. package/docs/react-api.md +164 -40
  81. package/docs/runtime-benchmarks.md +216 -0
  82. package/docs/ssr.md +92 -81
  83. package/docs/styles.md +65 -16
  84. package/docs/tasty-static.md +149 -99
  85. package/package.json +49 -21
  86. package/tasty.config.ts +6 -0
  87. package/dist/_virtual/_rolldown/runtime.js +0 -7
  88. package/dist/chunks/cacheKey.d.ts +0 -1
  89. package/dist/chunks/cacheKey.js +0 -77
  90. package/dist/chunks/cacheKey.js.map +0 -1
  91. package/dist/chunks/definitions.d.ts +0 -37
  92. package/dist/chunks/definitions.js +0 -258
  93. package/dist/chunks/definitions.js.map +0 -1
  94. package/dist/chunks/index.d.ts +0 -1
  95. package/dist/chunks/renderChunk.d.ts +0 -1
  96. package/dist/chunks/renderChunk.js +0 -59
  97. package/dist/chunks/renderChunk.js.map +0 -1
  98. package/dist/compute-styles.d.ts +0 -31
  99. package/dist/compute-styles.js +0 -335
  100. package/dist/compute-styles.js.map +0 -1
  101. package/dist/config.d.ts +0 -407
  102. package/dist/config.js +0 -587
  103. package/dist/config.js.map +0 -1
  104. package/dist/counter-style/index.js +0 -51
  105. package/dist/counter-style/index.js.map +0 -1
  106. package/dist/debug.d.ts +0 -89
  107. package/dist/debug.js +0 -453
  108. package/dist/debug.js.map +0 -1
  109. package/dist/font-face/index.js +0 -63
  110. package/dist/font-face/index.js.map +0 -1
  111. package/dist/hooks/index.d.ts +0 -7
  112. package/dist/hooks/useCounterStyle.d.ts +0 -36
  113. package/dist/hooks/useCounterStyle.js +0 -65
  114. package/dist/hooks/useCounterStyle.js.map +0 -1
  115. package/dist/hooks/useFontFace.d.ts +0 -45
  116. package/dist/hooks/useFontFace.js +0 -66
  117. package/dist/hooks/useFontFace.js.map +0 -1
  118. package/dist/hooks/useGlobalStyles.d.ts +0 -46
  119. package/dist/hooks/useGlobalStyles.js +0 -88
  120. package/dist/hooks/useGlobalStyles.js.map +0 -1
  121. package/dist/hooks/useKeyframes.d.ts +0 -58
  122. package/dist/hooks/useKeyframes.js +0 -55
  123. package/dist/hooks/useKeyframes.js.map +0 -1
  124. package/dist/hooks/useProperty.d.ts +0 -81
  125. package/dist/hooks/useProperty.js +0 -96
  126. package/dist/hooks/useProperty.js.map +0 -1
  127. package/dist/hooks/useRawCSS.d.ts +0 -22
  128. package/dist/hooks/useRawCSS.js +0 -103
  129. package/dist/hooks/useRawCSS.js.map +0 -1
  130. package/dist/hooks/useStyles.d.ts +0 -40
  131. package/dist/hooks/useStyles.js +0 -31
  132. package/dist/hooks/useStyles.js.map +0 -1
  133. package/dist/injector/index.d.ts +0 -182
  134. package/dist/injector/index.js +0 -185
  135. package/dist/injector/index.js.map +0 -1
  136. package/dist/injector/injector.d.ts +0 -198
  137. package/dist/injector/injector.js +0 -651
  138. package/dist/injector/injector.js.map +0 -1
  139. package/dist/injector/sheet-manager.d.ts +0 -132
  140. package/dist/injector/sheet-manager.js +0 -699
  141. package/dist/injector/sheet-manager.js.map +0 -1
  142. package/dist/injector/types.d.ts +0 -235
  143. package/dist/keyframes/index.js +0 -206
  144. package/dist/keyframes/index.js.map +0 -1
  145. package/dist/parser/classify.js +0 -319
  146. package/dist/parser/classify.js.map +0 -1
  147. package/dist/parser/const.js +0 -60
  148. package/dist/parser/const.js.map +0 -1
  149. package/dist/parser/lru.js +0 -109
  150. package/dist/parser/lru.js.map +0 -1
  151. package/dist/parser/parser.d.ts +0 -25
  152. package/dist/parser/parser.js +0 -115
  153. package/dist/parser/parser.js.map +0 -1
  154. package/dist/parser/tokenizer.js +0 -69
  155. package/dist/parser/tokenizer.js.map +0 -1
  156. package/dist/parser/types.d.ts +0 -51
  157. package/dist/parser/types.js +0 -46
  158. package/dist/parser/types.js.map +0 -1
  159. package/dist/pipeline/conditions.d.ts +0 -134
  160. package/dist/pipeline/conditions.js +0 -406
  161. package/dist/pipeline/conditions.js.map +0 -1
  162. package/dist/pipeline/exclusive.js +0 -230
  163. package/dist/pipeline/exclusive.js.map +0 -1
  164. package/dist/pipeline/index.d.ts +0 -55
  165. package/dist/pipeline/index.js +0 -708
  166. package/dist/pipeline/index.js.map +0 -1
  167. package/dist/pipeline/materialize.js +0 -1103
  168. package/dist/pipeline/materialize.js.map +0 -1
  169. package/dist/pipeline/parseStateKey.d.ts +0 -15
  170. package/dist/pipeline/parseStateKey.js +0 -446
  171. package/dist/pipeline/parseStateKey.js.map +0 -1
  172. package/dist/pipeline/simplify.js +0 -515
  173. package/dist/pipeline/simplify.js.map +0 -1
  174. package/dist/pipeline/warnings.js +0 -18
  175. package/dist/pipeline/warnings.js.map +0 -1
  176. package/dist/plugins/index.d.ts +0 -2
  177. package/dist/plugins/okhsl-plugin.d.ts +0 -35
  178. package/dist/plugins/okhsl-plugin.js +0 -97
  179. package/dist/plugins/okhsl-plugin.js.map +0 -1
  180. package/dist/plugins/types.d.ts +0 -87
  181. package/dist/properties/index.js +0 -222
  182. package/dist/properties/index.js.map +0 -1
  183. package/dist/properties/property-type-resolver.d.ts +0 -24
  184. package/dist/properties/property-type-resolver.js +0 -90
  185. package/dist/properties/property-type-resolver.js.map +0 -1
  186. package/dist/rsc-cache.js +0 -79
  187. package/dist/rsc-cache.js.map +0 -1
  188. package/dist/ssr/async-storage.d.ts +0 -17
  189. package/dist/ssr/async-storage.js.map +0 -1
  190. package/dist/ssr/collect-auto-properties.js +0 -58
  191. package/dist/ssr/collect-auto-properties.js.map +0 -1
  192. package/dist/ssr/collector.js.map +0 -1
  193. package/dist/ssr/context.js.map +0 -1
  194. package/dist/ssr/format-global-rules.js.map +0 -1
  195. package/dist/ssr/format-keyframes.js +0 -69
  196. package/dist/ssr/format-keyframes.js.map +0 -1
  197. package/dist/ssr/format-property.js +0 -49
  198. package/dist/ssr/format-property.js.map +0 -1
  199. package/dist/ssr/format-rules.js.map +0 -1
  200. package/dist/ssr/hydrate.d.ts +0 -29
  201. package/dist/ssr/hydrate.js.map +0 -1
  202. package/dist/ssr/ssr-collector-ref.js +0 -29
  203. package/dist/ssr/ssr-collector-ref.js.map +0 -1
  204. package/dist/states/index.d.ts +0 -49
  205. package/dist/states/index.js +0 -170
  206. package/dist/states/index.js.map +0 -1
  207. package/dist/static/tastyStatic.d.ts +0 -46
  208. package/dist/static/tastyStatic.js +0 -30
  209. package/dist/static/tastyStatic.js.map +0 -1
  210. package/dist/static/types.d.ts +0 -49
  211. package/dist/static/types.js +0 -24
  212. package/dist/static/types.js.map +0 -1
  213. package/dist/styles/border.d.ts +0 -25
  214. package/dist/styles/border.js +0 -120
  215. package/dist/styles/border.js.map +0 -1
  216. package/dist/styles/color.d.ts +0 -14
  217. package/dist/styles/color.js +0 -26
  218. package/dist/styles/color.js.map +0 -1
  219. package/dist/styles/const.js +0 -17
  220. package/dist/styles/const.js.map +0 -1
  221. package/dist/styles/createStyle.js +0 -79
  222. package/dist/styles/createStyle.js.map +0 -1
  223. package/dist/styles/dimension.js +0 -109
  224. package/dist/styles/dimension.js.map +0 -1
  225. package/dist/styles/directional.js +0 -133
  226. package/dist/styles/directional.js.map +0 -1
  227. package/dist/styles/display.d.ts +0 -30
  228. package/dist/styles/display.js +0 -73
  229. package/dist/styles/display.js.map +0 -1
  230. package/dist/styles/fade.d.ts +0 -15
  231. package/dist/styles/fade.js +0 -62
  232. package/dist/styles/fade.js.map +0 -1
  233. package/dist/styles/fill.d.ts +0 -42
  234. package/dist/styles/fill.js +0 -51
  235. package/dist/styles/fill.js.map +0 -1
  236. package/dist/styles/flow.d.ts +0 -16
  237. package/dist/styles/flow.js +0 -12
  238. package/dist/styles/flow.js.map +0 -1
  239. package/dist/styles/gap.d.ts +0 -31
  240. package/dist/styles/gap.js +0 -38
  241. package/dist/styles/gap.js.map +0 -1
  242. package/dist/styles/height.d.ts +0 -17
  243. package/dist/styles/height.js +0 -19
  244. package/dist/styles/height.js.map +0 -1
  245. package/dist/styles/index.d.ts +0 -1
  246. package/dist/styles/index.js +0 -8
  247. package/dist/styles/index.js.map +0 -1
  248. package/dist/styles/inset.d.ts +0 -24
  249. package/dist/styles/inset.js +0 -34
  250. package/dist/styles/inset.js.map +0 -1
  251. package/dist/styles/list.d.ts +0 -16
  252. package/dist/styles/list.js +0 -100
  253. package/dist/styles/list.js.map +0 -1
  254. package/dist/styles/margin.d.ts +0 -24
  255. package/dist/styles/margin.js +0 -32
  256. package/dist/styles/margin.js.map +0 -1
  257. package/dist/styles/outline.d.ts +0 -29
  258. package/dist/styles/outline.js +0 -55
  259. package/dist/styles/outline.js.map +0 -1
  260. package/dist/styles/padding.d.ts +0 -24
  261. package/dist/styles/padding.js +0 -32
  262. package/dist/styles/padding.js.map +0 -1
  263. package/dist/styles/placement.d.ts +0 -37
  264. package/dist/styles/placement.js +0 -74
  265. package/dist/styles/placement.js.map +0 -1
  266. package/dist/styles/predefined.d.ts +0 -71
  267. package/dist/styles/predefined.js +0 -237
  268. package/dist/styles/predefined.js.map +0 -1
  269. package/dist/styles/preset.d.ts +0 -52
  270. package/dist/styles/preset.js +0 -127
  271. package/dist/styles/preset.js.map +0 -1
  272. package/dist/styles/radius.d.ts +0 -12
  273. package/dist/styles/radius.js +0 -83
  274. package/dist/styles/radius.js.map +0 -1
  275. package/dist/styles/scrollMargin.d.ts +0 -24
  276. package/dist/styles/scrollMargin.js +0 -32
  277. package/dist/styles/scrollMargin.js.map +0 -1
  278. package/dist/styles/scrollbar.d.ts +0 -25
  279. package/dist/styles/scrollbar.js +0 -51
  280. package/dist/styles/scrollbar.js.map +0 -1
  281. package/dist/styles/shadow.d.ts +0 -14
  282. package/dist/styles/shadow.js +0 -25
  283. package/dist/styles/shadow.js.map +0 -1
  284. package/dist/styles/shared.js +0 -17
  285. package/dist/styles/shared.js.map +0 -1
  286. package/dist/styles/transition.d.ts +0 -14
  287. package/dist/styles/transition.js +0 -159
  288. package/dist/styles/transition.js.map +0 -1
  289. package/dist/styles/types.d.ts +0 -564
  290. package/dist/styles/width.d.ts +0 -17
  291. package/dist/styles/width.js +0 -19
  292. package/dist/styles/width.js.map +0 -1
  293. package/dist/tasty.d.ts +0 -134
  294. package/dist/tasty.js +0 -248
  295. package/dist/tasty.js.map +0 -1
  296. package/dist/types.d.ts +0 -184
  297. package/dist/utils/cache-wrapper.js +0 -21
  298. package/dist/utils/cache-wrapper.js.map +0 -1
  299. package/dist/utils/case-converter.js +0 -8
  300. package/dist/utils/case-converter.js.map +0 -1
  301. package/dist/utils/color-math.d.ts +0 -46
  302. package/dist/utils/color-math.js +0 -749
  303. package/dist/utils/color-math.js.map +0 -1
  304. package/dist/utils/color-space.d.ts +0 -5
  305. package/dist/utils/color-space.js +0 -228
  306. package/dist/utils/color-space.js.map +0 -1
  307. package/dist/utils/colors.d.ts +0 -5
  308. package/dist/utils/colors.js +0 -10
  309. package/dist/utils/colors.js.map +0 -1
  310. package/dist/utils/css-types.d.ts +0 -7
  311. package/dist/utils/deps-equal.js +0 -15
  312. package/dist/utils/deps-equal.js.map +0 -1
  313. package/dist/utils/dotize.d.ts +0 -26
  314. package/dist/utils/dotize.js +0 -122
  315. package/dist/utils/dotize.js.map +0 -1
  316. package/dist/utils/filter-base-props.d.ts +0 -15
  317. package/dist/utils/filter-base-props.js +0 -45
  318. package/dist/utils/filter-base-props.js.map +0 -1
  319. package/dist/utils/get-display-name.d.ts +0 -7
  320. package/dist/utils/get-display-name.js +0 -10
  321. package/dist/utils/get-display-name.js.map +0 -1
  322. package/dist/utils/has-keys.js +0 -13
  323. package/dist/utils/has-keys.js.map +0 -1
  324. package/dist/utils/hash.js +0 -14
  325. package/dist/utils/hash.js.map +0 -1
  326. package/dist/utils/is-dev-env.js +0 -19
  327. package/dist/utils/is-dev-env.js.map +0 -1
  328. package/dist/utils/is-valid-element-type.js +0 -15
  329. package/dist/utils/is-valid-element-type.js.map +0 -1
  330. package/dist/utils/merge-styles.js.map +0 -1
  331. package/dist/utils/mod-attrs.d.ts +0 -6
  332. package/dist/utils/mod-attrs.js +0 -20
  333. package/dist/utils/mod-attrs.js.map +0 -1
  334. package/dist/utils/process-tokens.d.ts +0 -17
  335. package/dist/utils/process-tokens.js +0 -83
  336. package/dist/utils/process-tokens.js.map +0 -1
  337. package/dist/utils/resolve-recipes.d.ts +0 -17
  338. package/dist/utils/resolve-recipes.js.map +0 -1
  339. package/dist/utils/selector-transform.js +0 -32
  340. package/dist/utils/selector-transform.js.map +0 -1
  341. package/dist/utils/string.js +0 -8
  342. package/dist/utils/string.js.map +0 -1
  343. package/dist/utils/styles.d.ts +0 -99
  344. package/dist/utils/styles.js +0 -220
  345. package/dist/utils/styles.js.map +0 -1
  346. package/dist/utils/typography.d.ts +0 -58
  347. package/dist/utils/typography.js +0 -51
  348. package/dist/utils/typography.js.map +0 -1
  349. package/dist/utils/warnings.d.ts +0 -16
  350. package/dist/utils/warnings.js +0 -16
  351. package/dist/utils/warnings.js.map +0 -1
  352. package/dist/zero/css-writer.d.ts +0 -45
  353. package/dist/zero/css-writer.js +0 -73
  354. package/dist/zero/css-writer.js.map +0 -1
  355. package/dist/zero/extractor.d.ts +0 -24
  356. package/dist/zero/extractor.js.map +0 -1
  357. package/docs/PIPELINE.md +0 -519
package/docs/react-api.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # React API
2
2
 
3
- The React-specific `tasty()` component factory, component props, and style functions. All Tasty style functions — `tasty()` components, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, and `useCounterStyle()` — are hook-free and compatible with React Server Components. No `'use client'` directive needed. For the shared style language (state maps, tokens, units, extending semantics), see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
3
+ Use `tasty()` to turn state-aware style definitions into normal React components, extend existing Tasty components, and expose typed styling APIs to consumers.
4
+
5
+ All Tasty style functions — `tasty()` components, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — are hook-free and compatible with React Server Components. No `'use client'` directive is needed. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
4
6
 
5
7
  > **Note:** This file was previously named `runtime.md`. All functionality documented here works in both server and client contexts — "runtime" referred to style computation during React rendering, not to client-side JavaScript.
6
8
 
@@ -42,6 +44,32 @@ const PrimaryButton = tasty(Button, {
42
44
 
43
45
  Style maps merge intelligently — see [Style DSL — Extending vs. Replacing State Maps](dsl.md#extending-vs-replacing-state-maps) for extend mode, replace mode, `@inherit`, `null`, and `false` tombstones.
44
46
 
47
+ `tasty(Component, ...)` always wraps `Component` and forwards every prop to it, so `Component` must be Tasty-aware (i.e. it accepts `styles`, `mods`, `qa`, etc. and renders them through its own Tasty pipeline). To apply styles to a third-party component or a string DOM tag via `className`, use the options-only form with `as`:
48
+
49
+ ```jsx
50
+ import NextLink from 'next/link';
51
+
52
+ const Link = tasty({
53
+ as: NextLink,
54
+ styles: {
55
+ color: { '': '#accent-text', ':hover': '#text' },
56
+ textDecoration: 'underline',
57
+ },
58
+ styleProps: ['padding'],
59
+ });
60
+
61
+ const Span = tasty({
62
+ as: 'span',
63
+ styles: { preset: 'strong' },
64
+ });
65
+
66
+ <Link href="/blog" padding="1x">
67
+ Blog
68
+ </Link>;
69
+ ```
70
+
71
+ The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty-specific props (`qa`, `qaVal`, `mods`, `tokens`, `styleProps`, `modProps`, `tokenProps`) are consumed by Tasty and never leak to the DOM.
72
+
45
73
  ---
46
74
 
47
75
  ## Style Props
@@ -60,7 +88,7 @@ const FlexibleBox = tasty({
60
88
 
61
89
  <FlexibleBox gap="2x" align="center" fill="#surface">
62
90
  Content
63
- </FlexibleBox>
91
+ </FlexibleBox>;
64
92
  ```
65
93
 
66
94
  Style props accept state maps, so responsive values work through the same API:
@@ -74,6 +102,26 @@ Style props accept state maps, so responsive values work through the same API:
74
102
 
75
103
  For predefined style prop lists (`FLOW_STYLES`, `POSITION_STYLES`, `DIMENSION_STYLES`, etc.) and guidance on which props to expose per component category, see [Methodology — styleProps as the public API](methodology.md#styleprops-as-the-public-api).
76
104
 
105
+ ### Always-available style props
106
+
107
+ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteSpace`, `opacity`, and `transition` — are always harvested as style props on every `tasty()` component, even when `styleProps` is omitted. This means you can pass `<Card display="grid" />` or `<Text preset="h1" />` without declaring them. When you do declare `styleProps`, these base props are unioned with your list (not replaced).
108
+
109
+ ---
110
+
111
+ ## `Element`
112
+
113
+ `Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts all Tasty props (`styles`, `styleProps`, `mods`, `tokens`, `as`, `qa`, `theme`, the `is*` props, etc.) and is useful as a generic styled box or as a building block for layout primitives.
114
+
115
+ ```jsx
116
+ import { Element } from '@tenphi/tasty';
117
+
118
+ <Element as="section" padding="4x" fill="#surface">
119
+ Content
120
+ </Element>;
121
+ ```
122
+
123
+ > Note: `Element` shadows the global DOM `Element` type when imported from `@tenphi/tasty`. In files that need the DOM type, alias the import: `import { Element as TastyElement } from '@tenphi/tasty'`.
124
+
77
125
  ---
78
126
 
79
127
  ## Mod Props
@@ -112,9 +160,9 @@ Map modifier names to type descriptors for precise TypeScript types:
112
160
  ```tsx
113
161
  const Button = tasty({
114
162
  modProps: {
115
- isLoading: Boolean, // isLoading?: boolean
116
- isSelected: Boolean, // isSelected?: boolean
117
- size: ['small', 'medium', 'large'] as const, // size?: 'small' | 'medium' | 'large'
163
+ isLoading: Boolean, // isLoading?: boolean
164
+ isSelected: Boolean, // isSelected?: boolean
165
+ size: ['small', 'medium', 'large'] as const, // size?: 'small' | 'medium' | 'large'
118
166
  },
119
167
  styles: {
120
168
  padding: { '': '2x 4x', 'size=small': '1x 2x', 'size=large': '3x 6x' },
@@ -122,18 +170,20 @@ const Button = tasty({
122
170
  },
123
171
  });
124
172
 
125
- <Button isLoading size="large">Submit</Button>
173
+ <Button isLoading size="large">
174
+ Submit
175
+ </Button>;
126
176
  // Renders: <button data-is-loading="" data-size="large">Submit</button>
127
177
  ```
128
178
 
129
179
  Available type descriptors:
130
180
 
131
- | Descriptor | TypeScript type | Example |
132
- |---|---|---|
133
- | `Boolean` | `boolean` | `isLoading: Boolean` |
134
- | `String` | `string` | `label: String` |
135
- | `Number` | `number` | `count: Number` |
136
- | `['a', 'b'] as const` | `'a' \| 'b'` | `size: ['sm', 'md', 'lg'] as const` |
181
+ | Descriptor | TypeScript type | Example |
182
+ | --------------------- | --------------- | ----------------------------------- |
183
+ | `Boolean` | `boolean` | `isLoading: Boolean` |
184
+ | `String` | `string` | `label: String` |
185
+ | `Number` | `number` | `count: Number` |
186
+ | `['a', 'b'] as const` | `'a' \| 'b'` | `size: ['sm', 'md', 'lg'] as const` |
137
187
 
138
188
  ### Merge with `mods`
139
189
 
@@ -146,11 +196,11 @@ Mod props and the `mods` object can be used together. Mod props take precedence:
146
196
 
147
197
  ### When to use `modProps` vs `mods`
148
198
 
149
- | Use case | Recommendation |
150
- |---|---|
199
+ | Use case | Recommendation |
200
+ | -------------------------------------------- | -------------------------------------------------------- |
151
201
  | Component has a fixed set of known modifiers | `modProps` — cleaner API, better TypeScript autocomplete |
152
- | Component needs arbitrary/dynamic modifiers | `mods` — open-ended `Record<string, ModValue>` |
153
- | Both fixed and dynamic | Combine: `modProps` for known keys, `mods` for ad-hoc |
202
+ | Component needs arbitrary/dynamic modifiers | `mods` — open-ended `Record<string, ModValue>` |
203
+ | Both fixed and dynamic | Combine: `modProps` for known keys, `mods` for ad-hoc |
154
204
 
155
205
  For architecture guidance on when to use modifiers vs `styleProps`, see [Methodology — modProps and mods](methodology.md#modprops-and-mods).
156
206
 
@@ -180,7 +230,7 @@ const ProgressBar = tasty({
180
230
 
181
231
  <ProgressBar progress="75%" accentColor="#purple" />
182
232
  // 'progress' → $progress → --progress
183
- // 'accentColor' → #accent → --accent-color + --accent-color-oklch
233
+ // 'accentColor' → #accent → --accent-color
184
234
  ```
185
235
 
186
236
  ### Object form
@@ -196,7 +246,7 @@ const Card = tasty({
196
246
  styles: { padding: '$card-size', fill: '#card-accent' },
197
247
  });
198
248
 
199
- <Card size="4x" color="#purple" />
249
+ <Card size="4x" color="#purple" />;
200
250
  ```
201
251
 
202
252
  ### Merge with `tokens`
@@ -215,11 +265,11 @@ const Bar = tasty({
215
265
 
216
266
  ### When to use `tokenProps` vs `tokens`
217
267
 
218
- | Use case | Recommendation |
219
- |---|---|
220
- | Component has a fixed set of known token keys | `tokenProps` — cleaner API, better TypeScript autocomplete |
221
- | Component needs arbitrary/dynamic token values | `tokens` — open-ended `Record<string, TokenValue>` |
222
- | Both fixed and dynamic | Combine: `tokenProps` for known keys, `tokens` for ad-hoc |
268
+ | Use case | Recommendation |
269
+ | ---------------------------------------------- | ---------------------------------------------------------- |
270
+ | Component has a fixed set of known token keys | `tokenProps` — cleaner API, better TypeScript autocomplete |
271
+ | Component needs arbitrary/dynamic token values | `tokens` — open-ended `Record<string, TokenValue>` |
272
+ | Both fixed and dynamic | Combine: `tokenProps` for known keys, `tokens` for ad-hoc |
223
273
 
224
274
  For architecture guidance, see [Methodology — tokenProps](methodology.md#tokenprops).
225
275
 
@@ -238,11 +288,11 @@ const Button = tasty({
238
288
  variants: {
239
289
  default: { fill: '#blue', color: '#white' },
240
290
  danger: { fill: '#red', color: '#white' },
241
- outline: { fill: 'transparent', color: '#blue', border: '1bw solid #blue' },
291
+ outline: { fill: '#clear', color: '#blue', border: '1bw solid #blue' },
242
292
  },
243
293
  });
244
294
 
245
- <Button variant="danger">Delete</Button>
295
+ <Button variant="danger">Delete</Button>;
246
296
  ```
247
297
 
248
298
  ### Extending Variants with Base State Maps
@@ -298,7 +348,7 @@ const Card = tasty({
298
348
  <Card>
299
349
  <Card.Title>Card Title</Card.Title>
300
350
  <Card.Content>Card content</Card.Content>
301
- </Card>
351
+ </Card>;
302
352
  ```
303
353
 
304
354
  Each entry in `elements` can be a tag name string or a config object:
@@ -324,7 +374,7 @@ const Card = tasty({
324
374
 
325
375
  <Card>
326
376
  <div data-element="Title">Card Title</div>
327
- </Card>
377
+ </Card>;
328
378
  ```
329
379
 
330
380
  ### Selector Affix (`$`)
@@ -357,7 +407,7 @@ On the client, CSS is injected synchronously into the DOM (idempotent via the in
357
407
 
358
408
  All style functions below are plain functions (not React hooks) and can be used in any environment: client components, SSR with a `ServerStyleCollector`, and React Server Components. They retain their `use` prefix for backward compatibility, but do not use any React hooks internally.
359
409
 
360
- In server-only contexts (Next.js RSC without `'use client'`, Astro without `client:*` directives, SSG), components that use only Tasty style functions produce zero client JavaScript. Tasty never forces the `'use client'` boundary — that decision belongs to your component when it needs React interactivity (state, effects, event handlers).
410
+ In server-only contexts, components that use only Tasty style functions ship no Tasty styling runtime. Astro without `client:*` directives produces no client JavaScript; server-only Next.js RSC follows the same Tasty architecture, while final output depends on the application deployment. Tasty never forces the `'use client'` boundary — that decision belongs to your component when it needs React interactivity (state, effects, event handlers).
361
411
 
362
412
  ### useStyles
363
413
 
@@ -395,6 +445,15 @@ function ThemeStyles() {
395
445
  }
396
446
  ```
397
447
 
448
+ A slot — the `id`, or the selector when no `id` is given — holds exactly one
449
+ injection **per `root`**. So:
450
+
451
+ - Changing the styles replaces the previous CSS rather than adding to it.
452
+ - Passing styles that produce no CSS (for example `{}`) clears the slot.
453
+ - The same selector used in two shadow roots keeps a separate injection in each.
454
+ - Two independent call sites that share a selector share a slot, and the last
455
+ render wins. Give them distinct `id`s if they should coexist.
456
+
398
457
  ### useRawCSS
399
458
 
400
459
  Inject raw CSS strings. Accepts an optional `id` in the options for update tracking — when the CSS changes for the same id, the previous injection is replaced:
@@ -411,6 +470,9 @@ function GlobalReset() {
411
470
  }
412
471
  ```
413
472
 
473
+ An `id` slot holds one injection per `root`. Without an `id` the CSS is deduped
474
+ by content and permanent — there is nothing to replace it with later.
475
+
414
476
  ### useKeyframes
415
477
 
416
478
  Inject `@keyframes` rules and return the generated animation name:
@@ -424,14 +486,14 @@ function Spinner() {
424
486
  from: { transform: 'rotate(0deg)' },
425
487
  to: { transform: 'rotate(360deg)' },
426
488
  },
427
- { name: 'spin' }
489
+ { name: 'spin' },
428
490
  );
429
491
 
430
492
  return <div style={{ animation: `${spin} 1s linear infinite` }} />;
431
493
  }
432
494
  ```
433
495
 
434
- `useKeyframes()` also supports a factory function. The deps array is accepted for backward compatibility but the factory is called on every invocation — deduplication is handled internally by content hash:
496
+ `useKeyframes()` also supports a factory function. Without a `name` the factory runs on every invocation and deduplication is handled internally by content hash; with a `name`, matching deps skip the factory entirely:
435
497
 
436
498
  ```tsx
437
499
  function Pulse({ scale }: { scale: number }) {
@@ -440,13 +502,24 @@ function Pulse({ scale }: { scale: number }) {
440
502
  '0%': { transform: 'scale(1)' },
441
503
  '100%': { transform: `scale(${scale})` },
442
504
  }),
443
- [scale]
505
+ [scale],
506
+ { name: 'pulse' },
444
507
  );
445
508
 
446
- return <div style={{ animation: `${pulse} 500ms ease-in-out alternate infinite` }} />;
509
+ return (
510
+ <div
511
+ style={{ animation: `${pulse} 500ms ease-in-out alternate infinite` }}
512
+ />
513
+ );
447
514
  }
448
515
  ```
449
516
 
517
+ Passing `name` claims a slot owned by that one call site, per `root` — much like
518
+ `id` in `useGlobalStyles()` and `useRawCSS()`. When the steps change the previous
519
+ `@keyframes` rule is disposed and the name is reused, so the rules don't
520
+ accumulate and the returned name stays stable. Anonymous keyframes are permanent
521
+ and shared by content.
522
+
450
523
  ### useProperty
451
524
 
452
525
  Register a CSS `@property` rule so a custom property can animate smoothly:
@@ -493,8 +566,16 @@ For multiple weights/styles, pass an array:
493
566
 
494
567
  ```tsx
495
568
  useFontFace('Brand Sans', [
496
- { src: 'url("/fonts/brand-regular.woff2") format("woff2")', fontWeight: 400, fontDisplay: 'swap' },
497
- { src: 'url("/fonts/brand-bold.woff2") format("woff2")', fontWeight: 700, fontDisplay: 'swap' },
569
+ {
570
+ src: 'url("/fonts/brand-regular.woff2") format("woff2")',
571
+ fontWeight: 400,
572
+ fontDisplay: 'swap',
573
+ },
574
+ {
575
+ src: 'url("/fonts/brand-bold.woff2") format("woff2")',
576
+ fontWeight: 700,
577
+ fontDisplay: 'swap',
578
+ },
498
579
  ]);
499
580
  ```
500
581
 
@@ -512,11 +593,14 @@ Inject a `@counter-style` rule and get back the counter style name. Permanent
512
593
  import { useCounterStyle } from '@tenphi/tasty';
513
594
 
514
595
  function EmojiList() {
515
- const styleName = useCounterStyle({
516
- system: 'cyclic',
517
- symbols: '"👍"',
518
- suffix: '" "',
519
- }, { name: 'thumbs' });
596
+ const styleName = useCounterStyle(
597
+ {
598
+ system: 'cyclic',
599
+ symbols: '"👍"',
600
+ suffix: '" "',
601
+ },
602
+ { name: 'thumbs' },
603
+ );
520
604
 
521
605
  return (
522
606
  <ol style={{ listStyleType: styleName }}>
@@ -536,6 +620,46 @@ function useCounterStyle(
536
620
  ): string;
537
621
  ```
538
622
 
623
+ ### useFunction
624
+
625
+ Register a CSS `@function` (custom function). Permanent — no cleanup on unmount. Deduplicates by function name. The function name accepts `$$name` (matching the call site `$$name(...)`), `$name`, or `--name`.
626
+
627
+ ```tsx
628
+ import { tasty, useFunction } from '@tenphi/tasty';
629
+
630
+ const Box = tasty({ styles: { margin: '$$negative(10px) top' } });
631
+
632
+ function Layout() {
633
+ useFunction('$$negative', { args: ['$value'], result: '(-1 * $value)' });
634
+ return <Box />;
635
+ }
636
+ ```
637
+
638
+ Call the function through the Tasty DSL rather than a raw `style` prop. An inline `style` value reaches the browser unparsed, so the `$$name(...)` sugar is never expanded — and under `configure({ polyfills: { functions: true } })` it silently does nothing, because the polyfill rewrites calls at parse time.
639
+
640
+ Inside a `tasty()` component you call functions with the same `$$name(...)` sugar:
641
+
642
+ ```tsx
643
+ const Box = tasty({
644
+ styles: {
645
+ '@function': { $$negative: { args: ['$value'], result: '(-1 * $value)' } },
646
+ margin: '$$negative(10px) top',
647
+ },
648
+ });
649
+ ```
650
+
651
+ Signature:
652
+
653
+ ```ts
654
+ function useFunction(
655
+ name: string,
656
+ definition: FunctionDefinition,
657
+ options?: { root?: Document | ShadowRoot },
658
+ ): void;
659
+ ```
660
+
661
+ See the [Functions section of the DSL reference](dsl.md#functions-function) for the full descriptor shape, token conventions, and value-sugar support. `@function` is an experimental CSS feature — unsupported browsers safely ignore the native rule, or enable the [`polyfills.functions`](configuration.md#polyfills) inline polyfill for full cross-browser support.
662
+
539
663
  ### Troubleshooting
540
664
 
541
665
  - Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
@@ -552,6 +676,6 @@ function useCounterStyle(
552
676
  - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
553
677
  - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
554
678
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
555
- - **[Zero Runtime (tastyStatic)](tasty-static.md)** — Build-time static styling with Babel plugin
679
+ - **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin
556
680
  - **[Server-Side Rendering](ssr.md)** — SSR setup for Next.js, Astro, and generic frameworks
557
681
  - **[Debug Utilities](debug.md)** — Inspect injected CSS, cache state, and active styles at runtime
@@ -0,0 +1,216 @@
1
+ # Runtime Benchmarks
2
+
3
+ Tasty keeps its performance claims in reproducible benchmarks rather than
4
+ combining unlike measurements into one score. The repository measures four
5
+ different costs:
6
+
7
+ 1. Style parsing and generation in Node.
8
+ 2. The React overhead of an empty `tasty({})` wrapper.
9
+ 3. Cold browser generation and injection compared with equivalent CSS that is
10
+ already on the page.
11
+ 4. A representative React update that combines wrapper, cache, generation,
12
+ injection, and style-resolution work.
13
+
14
+ These are focused microbenchmarks, not page-load or interaction scores. Run
15
+ them several times on an otherwise idle machine and use a production profile
16
+ to decide whether any cost matters in an application.
17
+
18
+ ## Reproducing the Results
19
+
20
+ ```bash
21
+ pnpm bench
22
+ pnpm bench:overhead
23
+ pnpm bench:injection
24
+ pnpm bench:tree
25
+ ```
26
+
27
+ `pnpm bench` runs the core pipeline benchmarks in Node. The other three commands
28
+ use production code paths in headless Chromium. The first checkout may require
29
+ `pnpm test:setup` to download Chromium.
30
+
31
+ Run the Node and browser suites separately so they do not compete for CPU. The
32
+ browser timer has 0.1 ms resolution, so both browser benchmarks perform many
33
+ matched operations per sample and divide the absolute difference by the number
34
+ of elements or rules. Machine load, browser versions, and CPU power will move
35
+ the results.
36
+
37
+ ## Core Style Pipeline
38
+
39
+ The following numbers are single-call throughput measured on an Apple M1 Max
40
+ with Node 22:
41
+
42
+ | Operation | ops/sec | Latency (mean) |
43
+ | ----------------------------------------------------------- | -------------------: | -------------: |
44
+ | `renderStyles` — 5 flat properties (cold) | ~60,000 | ~17 us |
45
+ | `renderStyles` — state map with media/hover/modifier (cold) | ~18,500 | ~54 us |
46
+ | `renderStyles` — same styles (cached) | ~5,800,000 | ~0.17 us |
47
+ | `parseStateKey` — simple key like `:hover` (cold) | ~790,000 | ~1.3 us |
48
+ | `parseStateKey` — complex OR/AND/NOT key (cold) | ~140,000 | ~7 us |
49
+ | `parseStateKey` — any key (cached) | ~3,400,000–8,300,000 | ~0.1–0.3 us |
50
+ | `parseStyle` — value tokens like `2x 4x` (cold) | ~344,000 | ~2.9 us |
51
+ | `parseStyle` — color tokens (cold) | ~567,000 | ~1.8 us |
52
+ | `parseStyle` — any value (cached) | ~15,250,000 | ~0.07 us |
53
+
54
+ “Cold” cases use unique inputs to bypass the relevant caches. Cached cases
55
+ reuse one input and measure the LRU hot path. Expect roughly ±10% between runs.
56
+ These benchmarks do not include React, DOM work, stylesheet injection, style
57
+ resolution, layout, or paint.
58
+
59
+ The benchmark sources are colocated with the code they exercise:
60
+ [`pipeline.bench.ts`](../src/pipeline/pipeline.bench.ts),
61
+ [`parseStateKey.bench.ts`](../src/pipeline/parseStateKey.bench.ts), and the
62
+ parser benchmark files under [`src/parser`](../src/parser).
63
+
64
+ ## Empty Wrapper Overhead
65
+
66
+ Skipping the style pipeline does not make a `tasty()` component free. Even
67
+ `tasty({})` is a React component between its parent and the host element. React
68
+ tracks another fiber, and Tasty still processes and forwards the element's
69
+ props.
70
+
71
+ [`tasty-overhead.bench.tsx`](../src/tasty-overhead.bench.tsx) compares 10,000
72
+ raw `<div className>` siblings with 10,000 instances of one module-scoped
73
+ `tasty({})` component. Both receive the same props, and the benchmark fails if
74
+ they do not produce equivalent DOM.
75
+
76
+ The benchmark uses production React in headless Chromium. The factory is
77
+ created and its empty class-name cache is warmed before timing, so factory
78
+ creation, style generation, and injection are excluded. A detached container
79
+ excludes layout, paint, and stylesheet matching. Every commit is wrapped in
80
+ `flushSync`, keeping its synchronous reconciliation and commit inside the
81
+ sample. This does not estimate React's concurrent scheduling latency.
82
+
83
+ On an Apple M3 Pro with React 19.2.4 and Chromium 151, three consecutive runs
84
+ produced these ranges:
85
+
86
+ | Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
87
+ | ----------------------------------- | -----------: | -----------: | ------------------------: |
88
+ | Mount + remove | 4.5–4.9 ms | 14.0–14.9 ms | 0.95–1.00 us |
89
+ | Rerender, same host props | 1.3–1.4 ms | 10.9–11.4 ms | 0.96–1.00 us |
90
+ | Rerender, change one host attribute | 2.8–3.4 ms | 15.0–15.7 ms | 1.21–1.27 us |
91
+
92
+ The useful result is the raw/Tasty time difference divided by 10,000, not the
93
+ ratio between the two times. The ratio becomes large because the raw baseline
94
+ is tiny. In this synthetic workload, an empty wrapper adds roughly 1 us per
95
+ participating element, or 1.2–1.3 us when React also changes a DOM attribute.
96
+
97
+ This is the floor Tasty consumes when it has no styling job. It is not a
98
+ page-level score. Real trees include application components, effects, layout,
99
+ paint, and usually far fewer simultaneous styled-element updates. The benchmark
100
+ also does not measure retained memory; that requires a matched-tree heap
101
+ snapshot experiment with controlled garbage collection.
102
+
103
+ ## Cold Generation and Injection
104
+
105
+ [`tasty-injection.bench.ts`](../src/tasty-injection.bench.ts) measures the extra
106
+ work when Tasty must generate and inject CSS that an otherwise equivalent page
107
+ already has. It does not compare different stylesheet insertion techniques.
108
+
109
+ The benchmark covers four group sizes with one style-resolution boundary per
110
+ group:
111
+
112
+ - **One new rule:** add one rule, append its element, and immediately read its
113
+ computed style. Each sample performs 50 independent groups for timer
114
+ stability.
115
+ - **10 new rules together:** generate and insert 10 rules, append their 10
116
+ elements, then read every computed color without another write. Each sample
117
+ performs 10 independent groups.
118
+ - **100 or 1,000 new rules together:** perform the same sequence once per
119
+ sample with 100 or 1,000 rules before the resolution boundary.
120
+
121
+ Repeating the smaller groups inside one sample only raises the measurement
122
+ above Chromium's timer resolution. Results are divided by that repetition
123
+ count, so every row below describes one group with one resolution boundary.
124
+
125
+ For every transaction, the existing-CSS control has the equivalent stylesheet
126
+ parsed, adopted, and attached before timing. The runtime root has a Tasty
127
+ stylesheet pre-created with an unrelated sentinel rule, but not the measured
128
+ rules. Both paths perform the same class assignment, DOM commit, and
129
+ computed-style reads. Only the runtime path calls `computeStyles()` and inserts
130
+ the new rules.
131
+
132
+ Preparation and cleanup happen outside the sample timer. Every runtime style
133
+ value is unique within a cycle, the relevant caches are cleared between cycles,
134
+ and a guard verifies that both paths resolve to the same color. React and the
135
+ `tasty()` wrapper are absent so their independently measured costs do not enter
136
+ the result. Pre-creating both stylesheets also excludes one-time sheet creation
137
+ and adoption from the subtraction.
138
+
139
+ On an Apple M3 Pro with Chromium 151, three consecutive runs produced these
140
+ normalized ranges:
141
+
142
+ | New rules before one resolution | Groups per sample | Incremental cost per group | Incremental cost per rule |
143
+ | ------------------------------: | ----------------: | -------------------------: | ------------------------: |
144
+ | 1 | 50 | 0.111–0.114 ms | 111–114 us |
145
+ | 10 | 10 | 0.142–0.159 ms | 14.2–15.9 us |
146
+ | 100 | 1 | 0.90–0.94 ms | 9.0–9.4 us |
147
+ | 1,000 | 1 | 7.58–8.10 ms | 7.6–8.1 us |
148
+
149
+ The curve makes the fixed work visible. Delivering 1,000 rules before one
150
+ resolution boundary cost roughly 67–73 times as much in total as delivering
151
+ one rule and resolving it, not 1,000 times as much. The average incremental
152
+ cost per rule was roughly 14–15 times lower because the group amortized fixed
153
+ transaction work and let the browser resolve all stylesheet writes together.
154
+
155
+ The subtraction is the meaningful result. It includes Tasty's cold style
156
+ generation, cache and injector bookkeeping, rule insertion, and any additional
157
+ style invalidation exposed by that workload's resolution boundary. It does not
158
+ pretend to isolate `insertRule()` from the system that calls it.
159
+
160
+ This is a deliberately cold workload. Reused styles resolve from cache and do
161
+ not inject another rule. Different rule complexity, DOM shape, stylesheet size,
162
+ browser, and hardware will change the number. Because the same group and
163
+ resolution pattern is present in each workload's control, the difference
164
+ answers the narrower delivery question: how much extra work did Tasty perform
165
+ when the same CSS was not already there?
166
+
167
+ ## Representative React Tree
168
+
169
+ [`tasty-tree.bench.tsx`](../src/tasty-tree.bench.tsx) bridges the isolated
170
+ measurements with one production React update. Every path updates 1,000 host
171
+ elements sharing 20 combinations of `color` and `padding`, then reads computed
172
+ styles after the commit:
173
+
174
+ - **CSS already present:** raw elements switch between classes whose rules were
175
+ attached before timing.
176
+ - **Tasty, warm:** the elements switch between two sets of Tasty styles that
177
+ were generated before timing.
178
+ - **Tasty, cold:** every update introduces 20 new combinations shared across
179
+ the 1,000 elements.
180
+
181
+ The Tasty paths use the normal component API and `TastyBatchProvider`. On an
182
+ Apple M3 Pro with production React 19.2.4 and Chromium 151, three consecutive
183
+ runs produced these mean ranges:
184
+
185
+ | Update path | Total time for 1,000 elements |
186
+ | ----------------------------------- | ----------------------------: |
187
+ | CSS already present | 1.64–1.70 ms |
188
+ | Tasty, shared styles already cached | 4.17–4.43 ms |
189
+ | Tasty, 20 new shared styles | 4.62–4.80 ms |
190
+
191
+ In this workload, the warmed Tasty path added 2.52–2.73 ms over raw elements
192
+ with existing CSS. Introducing 20 new shared combinations added another
193
+ 0.38–0.55 ms over the warm Tasty update. This is a synthetic update, not an
194
+ application score: it excludes effects, layout reads, paint, and concurrent
195
+ scheduling, while deliberately making all 1,000 elements participate in every
196
+ commit.
197
+
198
+ ## Reading the Results Together
199
+
200
+ Do not add all three benchmark numbers to estimate an application blindly.
201
+ They describe different paths:
202
+
203
+ - A stable `tasty()` factory can skip the style pipeline on later renders, but
204
+ its React wrapper still participates in reconciliation.
205
+ - A cached style avoids cold parsing and generation and does not insert a new
206
+ rule.
207
+ - A genuinely new style pays generation and injection once, then becomes
208
+ reusable.
209
+ - A tree that shares a small style vocabulary pays the wrapper path per element
210
+ but cold generation only per new combination.
211
+ - Browser style resolution, layout, and paint depend on the actual document and
212
+ need application-level profiling.
213
+
214
+ The practical optimization target is therefore repeated work: keep style input
215
+ stable when possible, reuse generated chunks, and generate CSS at build or
216
+ server time when runtime flexibility is unnecessary.