@tenphi/cookbook 0.28.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +13 -11
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +2 -2
  4. package/docs/ai-agents.md +5 -3
  5. package/docs/architecture.md +10 -7
  6. package/docs/authoring.mdx +94 -36
  7. package/docs/cli.md +92 -33
  8. package/docs/component-styles.md +286 -282
  9. package/docs/configuration.md +232 -153
  10. package/docs/content-sources.md +39 -21
  11. package/docs/custom-components.md +117 -169
  12. package/docs/customization-rules.md +23 -14
  13. package/docs/deployment.md +101 -53
  14. package/docs/es/getting-started.md +7 -5
  15. package/docs/es/index.md +1 -1
  16. package/docs/examples.md +65 -28
  17. package/docs/fonts-and-typography.md +32 -27
  18. package/docs/getting-started.md +17 -10
  19. package/docs/ja/getting-started.md +7 -5
  20. package/docs/ja/index.md +1 -1
  21. package/docs/migration.md +188 -65
  22. package/docs/plugins.md +28 -16
  23. package/docs/publishing.md +3 -2
  24. package/docs/quality-checks.md +18 -42
  25. package/docs/recipes.md +38 -54
  26. package/docs/ru/getting-started.md +7 -5
  27. package/docs/ru/index.md +1 -1
  28. package/docs/site-navigation.md +47 -47
  29. package/docs/theme-and-components.md +126 -59
  30. package/docs/troubleshooting.md +4 -3
  31. package/docs/upstream/manifest.json +2 -2
  32. package/docs/upstream/tasty/README.md +404 -381
  33. package/docs/upstream/tasty/docs/README.md +4 -1
  34. package/docs/upstream/tasty/docs/adoption.md +25 -17
  35. package/docs/upstream/tasty/docs/ai-agents.md +42 -34
  36. package/docs/upstream/tasty/docs/comparison.md +9 -8
  37. package/docs/upstream/tasty/docs/configuration.md +187 -69
  38. package/docs/upstream/tasty/docs/debug.md +3 -2
  39. package/docs/upstream/tasty/docs/design-system.md +117 -15
  40. package/docs/upstream/tasty/docs/dsl.md +211 -117
  41. package/docs/upstream/tasty/docs/getting-started.md +81 -24
  42. package/docs/upstream/tasty/docs/injector.md +41 -40
  43. package/docs/upstream/tasty/docs/methodology.md +46 -21
  44. package/docs/upstream/tasty/docs/migration-v3.md +3 -3
  45. package/docs/upstream/tasty/docs/pipeline.md +111 -116
  46. package/docs/upstream/tasty/docs/plugins.md +21 -41
  47. package/docs/upstream/tasty/docs/react-api.md +73 -10
  48. package/docs/upstream/tasty/docs/runtime-benchmarks.md +46 -46
  49. package/docs/upstream/tasty/docs/ssr.md +92 -66
  50. package/docs/upstream/tasty/docs/styles.md +37 -17
  51. package/docs/upstream/tasty/docs/tasty-static.md +175 -57
  52. package/docs/zh/getting-started.md +7 -5
  53. package/docs/zh/index.md +1 -1
  54. package/package.json +3 -3
@@ -2,7 +2,7 @@
2
2
 
3
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
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).
5
+ Every Tasty style function — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — works in React Server Components with no `'use client'` directive. All of them except `useStyles()` are hook-free. `useStyles()` reads the `TastyRegistry` collector from React context on the client, so it is a real hook there; outside React, call `computeStyles()` instead. 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).
6
6
 
7
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.
8
8
 
@@ -44,6 +44,30 @@ const PrimaryButton = tasty(Button, {
44
44
 
45
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.
46
46
 
47
+ The extension form takes the same component options as `tasty({ … })`:
48
+
49
+ ```jsx
50
+ const ActionCard = tasty(Card, {
51
+ styles: { fill: { '': '#surface', isRaised: '#surface-raised' } },
52
+ styleProps: ['gap'] as const, // gap="2x" becomes a style
53
+ modProps: ['isRaised'] as const, // isRaised becomes a mod (data-is-raised)
54
+ tokenProps: ['accentColor'] as const, // accentColor sets the #accent token
55
+ variants: {
56
+ plain: { border: 0 },
57
+ framed: { border: '2bw #accent' },
58
+ },
59
+ variant: 'framed', // applied when no variant prop is passed
60
+ elements: { Footer: 'footer' },
61
+ });
62
+
63
+ <ActionCard gap="2x" isRaised accentColor="#purple">
64
+ <ActionCard.Title>Kept from Card</ActionCard.Title>
65
+ <ActionCard.Footer>Added here</ActionCard.Footer>
66
+ </ActionCard>;
67
+ ```
68
+
69
+ The extension turns its own `styleProps`, `modProps`, and `tokenProps` into the `styles`, `mods`, and `tokens` it passes to the base, which renders them as usual: mods as `data-*` attributes, tokens as inline custom properties. Its `variants` merge over its `styles`, and the `variant` prop is still forwarded, so it selects the base's own variants too and accepts either set's names. Both sets share that one prop, so a name only one of them defines leaves the other at its fallback: if `Card` had an `accent` variant, `variant="accent"` would apply it together with `framed`, and `variant="plain"` would leave `Card` on its default. For two independent choices, give one of them its own prop, such as a `modProps` value list. `elements` adds sub-components, and the base's are kept: `ActionCard.Title` is `Card.Title`. The returned component is typed with all of these props and sub-components.
70
+
47
71
  `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
72
 
49
73
  ```jsx
@@ -68,7 +92,7 @@ const Span = tasty({
68
92
  </Link>;
69
93
  ```
70
94
 
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.
95
+ The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty's own props (`styles`, `mods`, `tokens`, `qa`, `qaVal`, `theme`, `element`, the `is*` props) and the props you declared with `styleProps`, `modProps`, or `tokenProps` are consumed by Tasty; mods, `qa`, `qaVal`, `theme`, and `element` reach the DOM only as `data-*` attributes.
72
96
 
73
97
  Props forwarded to the `as` component keep their declared types and requiredness. For example, a component with `position: 'top-right' | 'bottom-left'` accepts those values through `tasty({ as: Component })`; CSS positioning can still be set through `styles.position`. A prop explicitly listed in `styleProps` is consumed as a style value instead. The always-available base style props below are also consumed by Tasty, so keep component-specific prop names separate from those reserved names.
74
98
 
@@ -112,12 +136,12 @@ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteS
112
136
 
113
137
  ## `Element`
114
138
 
115
- `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.
139
+ `Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts the props every Tasty component has (`as`, `styles`, `mods`, `tokens`, `qa`, `theme`, the `is*` props) plus the [always-available style props](#always-available-style-props), and is useful as a generic box or as a building block for layout primitives. Other style names, such as `padding`, are not props on it: create a component with `styleProps` for those, or pass them through `styles`.
116
140
 
117
141
  ```jsx
118
142
  import { Element } from '@tenphi/tasty';
119
143
 
120
- <Element as="section" padding="4x" fill="#surface">
144
+ <Element as="section" display="grid" preset="t2" styles={{ padding: '4x' }}>
121
145
  Content
122
146
  </Element>;
123
147
  ```
@@ -144,6 +168,7 @@ List modifier key names. Types default to `ModValue` (`boolean | string | number
144
168
 
145
169
  ```jsx
146
170
  const Button = tasty({
171
+ as: 'button',
147
172
  modProps: ['isLoading', 'isSelected'] as const,
148
173
  styles: {
149
174
  fill: { '': '#surface', isLoading: '#surface.5' },
@@ -161,6 +186,7 @@ Map modifier names to type descriptors for precise TypeScript types:
161
186
 
162
187
  ```tsx
163
188
  const Button = tasty({
189
+ as: 'button',
164
190
  modProps: {
165
191
  isLoading: Boolean, // isLoading?: boolean
166
192
  isSelected: Boolean, // isSelected?: boolean
@@ -297,6 +323,24 @@ const Button = tasty({
297
323
  <Button variant="danger">Delete</Button>;
298
324
  ```
299
325
 
326
+ With `variant` omitted, or set to a key `variants` does not define, the `default` variant applies; without a `default` key only the base `styles` apply. `variant` is typed from the variant keys.
327
+
328
+ To fall back to another variant instead, name it in the `variant` option:
329
+
330
+ ```jsx
331
+ const Button = tasty({
332
+ styles: { padding: '2x 4x' },
333
+ variants: {
334
+ primary: { fill: '#blue', color: '#white' },
335
+ danger: { fill: '#red', color: '#white' },
336
+ },
337
+ variant: 'primary',
338
+ });
339
+
340
+ <Button>Save</Button>; // primary
341
+ <Button variant="danger">Delete</Button>;
342
+ ```
343
+
300
344
  ### Extending Variants with Base State Maps
301
345
 
302
346
  When base `styles` contain an extend-mode state map (an object **without** a `''` key), it is applied **after** the variant merge. This lets you add or override states across all variants without repeating yourself:
@@ -362,7 +406,7 @@ elements: {
362
406
  }
363
407
  ```
364
408
 
365
- The sub-components produced by `elements` support `mods`, `tokens`, `isDisabled`, `isHidden`, and `isChecked` props — the same modifier interface as the root component.
409
+ The sub-components produced by `elements` support `mods`, `tokens`, `theme`, `isDisabled`, `isHidden`, and `isChecked` props — the same modifier interface as the root component. As on the root, `theme` renders as `data-theme`, and an explicit `data-theme` wins over it.
366
410
 
367
411
  If you don't need sub-components (e.g., the inner elements are already rendered by a third-party library), you can still style them by key alone — just omit `elements` and apply `data-element` manually:
368
412
 
@@ -401,19 +445,19 @@ const { className } = computeStyles({
401
445
  });
402
446
  ```
403
447
 
404
- On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS is collected via the SSR collector if one is available. This is the same function that `tasty()` components use internally.
448
+ On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server with an SSR collector (from `runWithCollector()` or `TastyRegistry`), the CSS goes to the collector. With neither a collector nor a `document` — a React Server Component — nothing is injected: the CSS comes back as `css`, and you render it in a `style` tag yourself, as `tasty()` components do. This is the same function that `tasty()` components use internally.
405
449
 
406
450
  ---
407
451
 
408
452
  ## Style Functions
409
453
 
410
- 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.
454
+ All style functions below work in client components, SSR with a `ServerStyleCollector`, and React Server Components. All except `useStyles()` are plain functions, not React hooks; they keep the `use` prefix for backward compatibility.
411
455
 
412
456
  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).
413
457
 
414
458
  ### useStyles
415
459
 
416
- Generate a className from a style object. Thin wrapper around `computeStyles()`:
460
+ Generate a className from a style object. A thin wrapper around `computeStyles()` that also picks up the `TastyRegistry` collector through React context. That makes it a hook on the client: call it inside a component, and use `computeStyles()` outside React. It also works in a Server Component, where it skips the context lookup; like the other style functions there, it cannot emit CSS itself, so the next `tasty()` element in the tree emits it.
417
461
 
418
462
  ```tsx
419
463
  import { useStyles } from '@tenphi/tasty';
@@ -447,6 +491,17 @@ function ThemeStyles() {
447
491
  }
448
492
  ```
449
493
 
494
+ The selector can be a list (`'.uni-checkbox, uni-checkbox'`) — every item gets
495
+ the states and sub-elements, with no `:is()` wrapping needed. Root states
496
+ (`@root(...)`) on a selector that is the root itself (`:root`, `html`) apply to
497
+ that element. A selector that ends in a pseudo-element takes its states on the
498
+ pseudo-element (`'.card::before'` with `:hover` gives
499
+ `.card::before:where(:hover)`) and `@parent()` on the element before it.
500
+ Attribute and modifier states, which a pseudo-element can't have, are ignored
501
+ with a warning. Local `@keyframes` are injected with the rules,
502
+ under the same resolved names `tasty()` components use, and `@starting` states
503
+ emit `@starting-style`.
504
+
450
505
  A slot — the `id`, or the selector when no `id` is given — holds exactly one
451
506
  injection **per `root`**. So:
452
507
 
@@ -475,6 +530,8 @@ function GlobalReset() {
475
530
  An `id` slot holds one injection per `root`. Without an `id` the CSS is deduped
476
531
  by content and permanent — there is nothing to replace it with later.
477
532
 
533
+ It also accepts a factory and deps — `useRawCSS(() => css, [deps], { id })` — and skips the factory while the deps for that `id` are unchanged.
534
+
478
535
  ### useKeyframes
479
536
 
480
537
  Inject `@keyframes` rules and return the generated animation name:
@@ -584,7 +641,11 @@ useFontFace('Brand Sans', [
584
641
  Signature:
585
642
 
586
643
  ```ts
587
- function useFontFace(family: string, input: FontFaceInput): void;
644
+ function useFontFace(
645
+ family: string,
646
+ input: FontFaceInput,
647
+ options?: { root?: Document | ShadowRoot },
648
+ ): void;
588
649
  ```
589
650
 
590
651
  ### useCounterStyle
@@ -666,9 +727,10 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
666
727
 
667
728
  - 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).
668
729
  - SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. All style functions discover the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
730
+ - "Server rendering without a style collector" in development: the server render has no request scope, so each `tasty()` element inlines its own CSS and configured globals are not emitted. Add `tastyIntegration()` (Astro), `TastyRegistry` (Next.js), or `runWithCollector()` — see the [SSR guide](ssr.md).
669
731
  - Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
670
732
  - For dynamic styles that change over the component lifecycle, use the `id` option in `useGlobalStyles()` and `useRawCSS()` to enable update tracking.
671
- - RSC inline mode: CSS accumulated by standalone style functions (`useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
733
+ - RSC inline mode: CSS accumulated by standalone style functions (`useStyles`, `useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
672
734
 
673
735
  ---
674
736
 
@@ -677,6 +739,7 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
677
739
  - **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
678
740
  - **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
679
741
  - **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
742
+ - **[Configuration → TypeScript](configuration.md#typescript)** — How props are typed and how to register your names
680
743
  - **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
681
744
  - **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin
682
745
  - **[Server-Side Rendering](ssr.md)** — SSR setup for Next.js, Astro, and generic frameworks
@@ -63,15 +63,15 @@ Single-call throughput, three consecutive runs on an Apple M3 Pro with Node
63
63
  | `renderStyles` — 5 flat properties (cold) | 143,000–158,000 | 6.3–7.0 us |
64
64
  | `renderStyles` — state map with media/hover/modifier (cold) | 31,000–36,000 | 28–32 us |
65
65
  | `renderStyles` — OR state map across contexts (cold) | 13,000–14,000 | 71–76 us |
66
- | `renderStyles` — same styles (cached) | 9,830,000–10,420,000 | \~0.10 us |
67
- | `parseStateKey` — simple key like `:hover` (cold) | 1,910,000–1,980,000 | \~0.50 us |
68
- | `parseStateKey` — value modifier key (cold) | 1,120,000–1,160,000 | \~0.90 us |
66
+ | `renderStyles` — same styles (cached) | 9,830,000–10,420,000 | ~0.10 us |
67
+ | `parseStateKey` — simple key like `:hover` (cold) | 1,910,000–1,980,000 | ~0.50 us |
68
+ | `parseStateKey` — value modifier key (cold) | 1,120,000–1,160,000 | ~0.90 us |
69
69
  | `parseStateKey` — complex OR/AND/NOT key (cold) | 341,000–357,000 | 2.8–2.9 us |
70
- | `parseStateKey` — any key (cached) | 4,900,000–16,530,000 | 0.10–0.20 us |
71
- | `parseStyle` — value tokens like `2x 4x` (cold) | 575,000–605,000 | \~1.7 us |
72
- | `parseStyle` — color tokens (cold) | 1,160,000–1,240,000 | 0.80–0.90 us |
70
+ | `parseStateKey` — any key (cached) | 4,900,000–16,530,000 | 0.10–0.20 us |
71
+ | `parseStyle` — value tokens like `2x 4x` (cold) | 575,000–605,000 | ~1.7 us |
72
+ | `parseStyle` — color tokens (cold) | 1,160,000–1,240,000 | 0.80–0.90 us |
73
73
  | `parseStyle` — layered functions (cold) | 175,000–184,000 | 5.4–5.7 us |
74
- | `parseStyle` — any value (cached) | 29,500,000–30,750,000 | \~0.03 us |
74
+ | `parseStyle` — any value (cached) | 29,500,000–30,750,000 | ~0.03 us |
75
75
 
76
76
  “Cold” cases use unique inputs to bypass the relevant caches. Cached cases
77
77
  reuse one input and measure the LRU hot path. Run-to-run spread on an idle
@@ -80,9 +80,9 @@ cold cases. These benchmarks do not include React, DOM work, stylesheet
80
80
  injection, style resolution, layout, or paint.
81
81
 
82
82
  The benchmark sources are colocated with the code they exercise:
83
- [`pipeline.bench.ts`](https://github.com/tenphi/tasty/blob/v3.9.10/src/pipeline/pipeline.bench.ts),
84
- [`parseStateKey.bench.ts`](https://github.com/tenphi/tasty/blob/v3.9.10/src/pipeline/parseStateKey.bench.ts), and the
85
- parser benchmark files under [`src/parser`](https://github.com/tenphi/tasty/blob/v3.9.10/src/parser).
83
+ [`pipeline.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/pipeline/pipeline.bench.ts),
84
+ [`parseStateKey.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/pipeline/parseStateKey.bench.ts), and the
85
+ parser benchmark files under [`src/parser`](https://github.com/tenphi/tasty/tree/main/src/parser).
86
86
 
87
87
  ## Empty Wrapper Overhead
88
88
 
@@ -91,7 +91,7 @@ Skipping the style pipeline does not make a `tasty()` component free. Even
91
91
  tracks another fiber, and Tasty still processes and forwards the element's
92
92
  props.
93
93
 
94
- [`tasty-overhead.bench.tsx`](https://github.com/tenphi/tasty/blob/v3.9.10/src/tasty-overhead.bench.tsx) compares 10,000
94
+ [`tasty-overhead.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-overhead.bench.tsx) compares 10,000
95
95
  raw `<div className>` siblings with 10,000 instances of one module-scoped
96
96
  `tasty({})` component. Both receive the same props, and the benchmark fails if
97
97
  they do not produce equivalent DOM.
@@ -106,16 +106,16 @@ sample. This does not estimate React's concurrent scheduling latency.
106
106
  On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs
107
107
  produced these ranges:
108
108
 
109
- | Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
110
- | ----------------------------------- | -----------: | -----------: | ------------------------: |
111
- | Mount + remove | 4.2–4.3 ms | 9.2–9.7 ms | 0.50–0.54 us |
112
- | Rerender, same host props | 1.2–1.3 ms | 6.3–6.5 ms | 0.51–0.52 us |
113
- | Rerender, change one host attribute | 2.5–2.7 ms | 11.2–11.6 ms | 0.86–0.91 us |
109
+ | Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
110
+ | ----------------------------------- | -----------: | ----------: | ------------------------: |
111
+ | Mount + remove | 4.2–4.3 ms | 9.2–9.7 ms | 0.50–0.54 us |
112
+ | Rerender, same host props | 1.2–1.3 ms | 6.3–6.5 ms | 0.51–0.52 us |
113
+ | Rerender, change one host attribute | 2.5–2.7 ms | 11.2–11.6 ms | 0.86–0.91 us |
114
114
 
115
115
  The useful result is the raw/Tasty time difference divided by 10,000, not the
116
116
  ratio between the two times. The ratio becomes large because the raw baseline
117
117
  is tiny. In this synthetic workload, an empty wrapper adds roughly 0.5 us per
118
- participating element, or \~0.9 us when React also changes a DOM attribute.
118
+ participating element, or ~0.9 us when React also changes a DOM attribute.
119
119
 
120
120
  This is the floor Tasty consumes when it has no styling job. It is not a
121
121
  page-level score. Real trees include application components, effects, layout,
@@ -125,18 +125,18 @@ snapshot experiment with controlled garbage collection.
125
125
 
126
126
  ## Cold Generation and Injection
127
127
 
128
- [`tasty-injection.bench.ts`](https://github.com/tenphi/tasty/blob/v3.9.10/src/tasty-injection.bench.ts) measures the extra
128
+ [`tasty-injection.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/tasty-injection.bench.ts) measures the extra
129
129
  work when Tasty must generate and inject CSS that an otherwise equivalent page
130
130
  already has. It does not compare different stylesheet insertion techniques.
131
131
 
132
132
  The benchmark covers two useful workloads:
133
133
 
134
- * **One new rule:** add one rule to an existing stylesheet, append its one
134
+ - **One new rule:** add one rule to an existing stylesheet, append its one
135
135
  element, and immediately read its computed style. Each timed sample performs
136
136
  50 independent one-rule transactions and reports their total; dividing the
137
137
  raw/Tasty difference by 50 produces a stable per-rule result despite
138
138
  Chromium's 0.1 ms timer resolution.
139
- * **1,000 new rules together:** generate and insert all 1,000 rules into one
139
+ - **1,000 new rules together:** generate and insert all 1,000 rules into one
140
140
  stylesheet, append all 1,000 elements, then read every computed color without
141
141
  another write in between. This gives the browser one style-resolution
142
142
  boundary for the group.
@@ -197,11 +197,11 @@ The benchmarks above measure mounting and whole-tree updates. A running
197
197
  application spends most of its time on neither. It flips mods — hovered,
198
198
  pressed, selected, expanded — on elements whose styles never change, one
199
199
  element at a time, and it mounts and unmounts small styled subtrees as menus
200
- and dialogs open. Both paths go through the state-map and ref-counting
201
- machinery rather than the parser, so a regression in them is invisible to every
200
+ and dialogs open. Both paths go through mod handling and the chunk cache
201
+ rather than the parser, so a regression in them is invisible to every
202
202
  other benchmark here.
203
203
 
204
- [`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/v3.9.10/src/tasty-interaction.bench.tsx) pairs each
204
+ [`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-interaction.bench.tsx) pairs each
205
205
  case with a raw-DOM equivalent driven by a hand-written stylesheet that
206
206
  produces the same computed color and background in both states. The benchmark
207
207
  fails if either arm resolves to anything else, so an arm that quietly rendered
@@ -216,12 +216,12 @@ performs 20 open/close cycles. Divide the raw/Tasty difference by those counts.
216
216
  Two things had to be sized deliberately, and both are the difference between a
217
217
  readable number and noise:
218
218
 
219
- * **The tree is small (100 elements), not large.** React locates a leaf's
219
+ - **The tree is small (100 elements), not large.** React locates a leaf's
220
220
  pending update by walking the sibling list, so in a 1,000-element tree a
221
- single-element update costs \~68 us of traversal — identical in both arms and
221
+ single-element update costs ~68 us of traversal — identical in both arms and
222
222
  an order of magnitude above anything the styling layer contributes.
223
- * **The sample resolves style once, not once per flip.** Forcing a recalc
224
- between flips costs \~70 us in both arms, which buries the delta the same way.
223
+ - **The sample resolves style once, not once per flip.** Forcing a recalc
224
+ between flips costs ~70 us in both arms, which buries the delta the same way.
225
225
  The browser's side of an interaction is real, but it is the browser's;
226
226
  resolution boundaries are what the injection benchmark above measures.
227
227
 
@@ -235,7 +235,7 @@ On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs:
235
235
  | Workload | Raw elements | Tasty mods | Extra per unit |
236
236
  | ---------------------------------------------------- | -----------: | ---------: | -----------------------: |
237
237
  | 300 single-element mod toggles in a 100-element tree | 1.7–1.8 ms | 2.0–2.1 ms | 1.2–1.3 us / interaction |
238
- | 20 mount + unmount cycles of a 200-element subtree | 5.2–5.4 ms | 8.3–8.6 ms | 0.77–0.81 us / element |
238
+ | 20 mount + unmount cycles of a 200-element subtree | 5.2–5.4 ms | 8.3–8.6 ms | 0.77–0.81 us / element |
239
239
 
240
240
  The absolute columns move several percent with machine load; the delta between
241
241
  the arms is the stable quantity, so read that rather than either column.
@@ -245,29 +245,29 @@ Two things are worth reading out of this.
245
245
  A mod flip on an already-mounted element costs about 1.2 us. The CSS for both
246
246
  states already exists — Tasty emits every state of a style map in one chunk on
247
247
  first render — so both arms perform the same commit, and what is left is
248
- Tasty's props and mod handling. That is the same order as the \~0.5 us empty
248
+ Tasty's props and mod handling. That is the same order as the ~0.5 us empty
249
249
  wrapper measured above, which is most of where it comes from.
250
250
 
251
- Subtree churn is not about styling at all. Its \~0.8 us per element sits right
251
+ Subtree churn is not about styling at all. Its ~0.8 us per element sits right
252
252
  on the empty-wrapper mount cost, because the styles are already cached:
253
253
  reopening a menu re-pays the React wrapper, not the style pipeline.
254
254
 
255
255
  ## Page-Load Cold Start
256
256
 
257
257
  Every benchmark above deliberately excludes the network, module compilation and
258
- the first render. [`scripts/cold-start`](https://github.com/tenphi/tasty/blob/v3.9.10/scripts/cold-start) measures exactly
258
+ the first render. [`scripts/cold-start`](https://github.com/tenphi/tasty/tree/main/scripts/cold-start) measures exactly
259
259
  those: what a visitor waits for between requesting a page and seeing styled
260
260
  content, in a real Chromium under CDP network and CPU throttling.
261
261
 
262
262
  Three pages render the same 50 styled components and are verified, before any
263
263
  timing, to produce the same 50 elements at the same computed color:
264
264
 
265
- * **baseline** — the components server-rendered: identical markup, identical
265
+ - **baseline** — the components server-rendered: identical markup, identical
266
266
  class names, a linked stylesheet, and no Tasty on the page. Every other
267
267
  column is a delta against this one.
268
- * **runtime** — Tasty generates the CSS in the browser, as a client-rendered
268
+ - **runtime** — Tasty generates the CSS in the browser, as a client-rendered
269
269
  application does. The run asserts it really did (69 rules generated).
270
- * **prewarm** — the same, after one throwaway `computeStyles()` against a
270
+ - **prewarm** — the same, after one throwaway `computeStyles()` against a
271
271
  detached root before the first component renders.
272
272
 
273
273
  Each cell is the median of 5 uncached loads in a fresh browser context. The run
@@ -279,14 +279,14 @@ painted yet.
279
279
  Two things about the payload decide whether this measures a deployment or a
280
280
  straw man, so both are enforced rather than assumed:
281
281
 
282
- * **Assets are served brotli-compressed**, the way a static host serves them.
282
+ - **Assets are served brotli-compressed**, the way a static host serves them.
283
283
  The bundle is 51.5 KB on the wire and 180 KB decoded; putting the decoded
284
- bytes on a 1.6 Mbps link would add \~700 ms and charge it to Tasty. The run
284
+ bytes on a 1.6 Mbps link would add ~700 ms and charge it to Tasty. The run
285
285
  reads `encodedBodySize` back out of resource timing and fails if what
286
286
  crossed the wire is not the compressed size the table reports.
287
- * **The bundle is built from what the page imports** (`tasty`, `configure`,
287
+ - **The bundle is built from what the page imports** (`tasty`, `configure`,
288
288
  `computeStyles`, `tastyDebug`), so it is tree-shaken as an application's
289
- would be. Re-exporting the whole library adds \~4 KB brotli of code no page
289
+ would be. Re-exporting the whole library adds ~4 KB brotli of code no page
290
290
  here calls.
291
291
 
292
292
  On an Apple M3 Pro with React 19.2.8 and Chromium 151, first contentful paint,
@@ -340,7 +340,7 @@ delivery models, not a thumb on the scale, but it means the FCP delta is not
340
340
  purely "what Tasty costs to execute".
341
341
 
342
342
  **Prewarming moves the wake-up, it does not remove it.** The first styled render
343
- is \~5.4 ms more expensive than the ones after it, because that is when the
343
+ is ~5.4 ms more expensive than the ones after it, because that is when the
344
344
  engine's deferred payload is actually compiled. A throwaway `computeStyles()`
345
345
  against a detached root pays it early: `render 1st` drops from 7.3 ms to
346
346
  1.9 ms. The prewarm itself costs 3.3 ms, so the work is moved rather than
@@ -365,21 +365,21 @@ jitter.
365
365
  Do not add the microbenchmark numbers together to estimate an application
366
366
  blindly. They describe different paths:
367
367
 
368
- * A stable `tasty()` factory can skip the style pipeline on later renders, but
368
+ - A stable `tasty()` factory can skip the style pipeline on later renders, but
369
369
  its React wrapper still participates in reconciliation.
370
- * A cached style avoids cold parsing and generation and does not insert a new
370
+ - A cached style avoids cold parsing and generation and does not insert a new
371
371
  rule.
372
- * A genuinely new style pays generation and injection once, then becomes
372
+ - A genuinely new style pays generation and injection once, then becomes
373
373
  reusable.
374
- * A mod flip on an already-styled element pays neither; it is a class-name
375
- change.
376
- * Browser style resolution, layout, and paint depend on the actual document and
374
+ - A mod flip on an already-styled element pays neither; it toggles a `data-*`
375
+ attribute and the class stays the same.
376
+ - Browser style resolution, layout, and paint depend on the actual document and
377
377
  need application-level profiling.
378
378
 
379
379
  The cold-start measurement is the one that puts the rest in proportion. On a
380
380
  slow connection, nearly all of Tasty's page-load cost is transferring the
381
381
  library — 263 ms of a 264 ms delta — while the generation and injection the
382
- microbenchmarks obsess over is \~10 ms for 50 components. Bundle size is
382
+ microbenchmarks obsess over is ~10 ms for 50 components. Bundle size is
383
383
  therefore the lever with the largest effect on first paint, and the runtime
384
384
  levers matter for what happens after it.
385
385