@tenphi/tasty 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +147 -129
  2. package/dist/astro-ib7E7V4Y.js +307 -0
  3. package/dist/astro-ib7E7V4Y.js.map +1 -0
  4. package/dist/{babel-BUQGeOXA.d.ts → babel-DWCsafYw.d.ts} +2 -2
  5. package/dist/{collector-DUaHCcTS.d.ts → collector-BTVXj2hS.d.ts} +21 -3
  6. package/dist/{collector-DTahQUiV.js → collector-C6TtL8HJ.js} +36 -13
  7. package/dist/collector-C6TtL8HJ.js.map +1 -0
  8. package/dist/{config-B5kHzuNz.js → config-B3gPdCqd.js} +305 -153
  9. package/dist/config-B3gPdCqd.js.map +1 -0
  10. package/dist/{config-YsxGv4tq.d.ts → config-LfIDmVHx.d.ts} +132 -32
  11. package/dist/core/index.d.ts +5 -5
  12. package/dist/core/index.js +6 -6
  13. package/dist/{core-Dr4u1NVD.js → core-Bq7w2kti.js} +215 -95
  14. package/dist/core-Bq7w2kti.js.map +1 -0
  15. package/dist/{css-writer-B-J87ncv.js → css-writer-Bgowobbx.js} +3 -3
  16. package/dist/{css-writer-B-J87ncv.js.map → css-writer-Bgowobbx.js.map} +1 -1
  17. package/dist/{format-rules-DKOA-6qu.js → format-rules-rCZ37rqY.js} +2 -2
  18. package/dist/format-rules-rCZ37rqY.js.map +1 -0
  19. package/dist/{hydrate-OeMX99We.js → hydrate-CNOmZprz.js} +3 -3
  20. package/dist/{hydrate-OeMX99We.js.map → hydrate-CNOmZprz.js.map} +1 -1
  21. package/dist/{index-PqN-DIpn.d.ts → index-Bm9ltvdH.d.ts} +38 -16
  22. package/dist/{index-Cd45t5NM.d.ts → index-BmogEzTo.d.ts} +69 -15
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +7 -7
  25. package/dist/index.js.map +1 -1
  26. package/dist/{keyframes-CV8azJf3.js → keyframes-DE-OE76F.js} +45 -3
  27. package/dist/keyframes-DE-OE76F.js.map +1 -0
  28. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BcLl1u_d.d.ts} +2 -2
  29. package/dist/{merge-styles-oklji0KB.js → merge-styles-DuoZEsm9.js} +2 -2
  30. package/dist/{merge-styles-oklji0KB.js.map → merge-styles-DuoZEsm9.js.map} +1 -1
  31. package/dist/{resolve-recipes-DTG81rzl.js → resolve-recipes-H9NqOQuP.js} +3 -3
  32. package/dist/{resolve-recipes-DTG81rzl.js.map → resolve-recipes-H9NqOQuP.js.map} +1 -1
  33. package/dist/ssr/astro-client.js +1 -1
  34. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  35. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  36. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  37. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  38. package/dist/ssr/astro-middleware-extract.js +9 -0
  39. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  40. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  41. package/dist/ssr/astro-middleware-static.js +1 -1
  42. package/dist/ssr/astro-middleware.d.ts +3 -1
  43. package/dist/ssr/astro-middleware.js +1 -1
  44. package/dist/ssr/astro.d.ts +37 -3
  45. package/dist/ssr/astro.js +1 -156
  46. package/dist/ssr/index.d.ts +2 -2
  47. package/dist/ssr/index.js +3 -3
  48. package/dist/ssr/index.js.map +1 -1
  49. package/dist/ssr/next.d.ts +1 -1
  50. package/dist/ssr/next.js +4 -4
  51. package/dist/static/index.d.ts +2 -2
  52. package/dist/static/index.js +1 -1
  53. package/dist/zero/babel.d.ts +1 -1
  54. package/dist/zero/babel.js +4 -4
  55. package/dist/zero/babel.js.map +1 -1
  56. package/dist/zero/index.d.ts +1 -1
  57. package/dist/zero/index.js +1 -1
  58. package/dist/zero/next.d.ts +1 -1
  59. package/docs/README.md +13 -8
  60. package/docs/adoption.md +56 -31
  61. package/docs/ai-agents.md +78 -77
  62. package/docs/comparison.md +69 -40
  63. package/docs/configuration.md +97 -87
  64. package/docs/debug.md +4 -2
  65. package/docs/design-system.md +41 -22
  66. package/docs/dsl.md +225 -195
  67. package/docs/getting-started.md +60 -39
  68. package/docs/injector.md +63 -26
  69. package/docs/methodology.md +42 -36
  70. package/docs/migration-v3.md +49 -49
  71. package/docs/plugins.md +37 -33
  72. package/docs/react-api.md +65 -44
  73. package/docs/runtime-benchmarks.md +178 -0
  74. package/docs/ssr.md +99 -61
  75. package/docs/styles.md +1 -1
  76. package/docs/tasty-static.md +137 -104
  77. package/package.json +26 -8
  78. package/dist/collector-DTahQUiV.js.map +0 -1
  79. package/dist/config-B5kHzuNz.js.map +0 -1
  80. package/dist/core-Dr4u1NVD.js.map +0 -1
  81. package/dist/format-rules-DKOA-6qu.js.map +0 -1
  82. package/dist/keyframes-CV8azJf3.js.map +0 -1
  83. package/dist/ssr/astro.js.map +0 -1
@@ -1,6 +1,8 @@
1
1
  # Getting Started
2
2
 
3
- This guide walks you from zero to a working Tasty component, then through the optional shared configuration and tooling layers. It is the right starting point when you already want to try Tasty in code. If you are still deciding whether Tasty fits your team, start with [Comparison](comparison.md) and [Adoption Guide](adoption.md) first. For a feature overview, see the [README](../README.md). For the full style language reference, see the [Style DSL](dsl.md). For the React API, see the [React API](react-api.md). For the rest of the docs by role or task, see the [Docs Hub](README.md).
3
+ Build one stateful component first. You will see how Tasty expresses priority as a state map, why overlapping states do not compete, and where shared design-system configuration fits afterward.
4
+
5
+ This is the right starting point when you are ready to try Tasty in code. If you are still evaluating it, read the [Introduction](../README.md), [Comparison](comparison.md), and [Adoption Guide](adoption.md).
4
6
 
5
7
  ---
6
8
 
@@ -10,7 +12,7 @@ This guide walks you from zero to a working Tasty component, then through the op
10
12
  - **React** >= 18 (peer dependency)
11
13
  - **Package manager**: pnpm, npm, or yarn
12
14
 
13
- Tasty can be used immediately in a React app, but it is most compelling for teams building reusable components with intersecting states, variants, tokens, and design-system conventions.
15
+ No Tasty configuration is required for the first component.
14
16
 
15
17
  ---
16
18
 
@@ -22,36 +24,57 @@ pnpm add @tenphi/tasty
22
24
 
23
25
  ---
24
26
 
25
- ## Your first component
27
+ ## Build a button whose states don’t fight
26
28
 
27
29
  ```tsx
28
30
  import { tasty } from '@tenphi/tasty';
29
31
 
30
- const Card = tasty({
31
- as: 'div',
32
+ const Button = tasty({
33
+ as: 'button',
32
34
  styles: {
33
- display: 'flex',
34
- flow: 'column',
35
- padding: '4x',
36
- gap: '2x',
37
- fill: '#white',
38
- border: true,
39
- radius: true,
35
+ padding: '12px 20px',
36
+ radius: '8px',
37
+ border: '0',
38
+ fill: {
39
+ '': 'royalblue',
40
+ ':hover': 'blue',
41
+ ':active': 'navy',
42
+ disabled: 'lightgray',
43
+ },
44
+ color: {
45
+ '': 'white',
46
+ disabled: 'dimgray',
47
+ },
48
+ cursor: {
49
+ '': 'pointer',
50
+ disabled: 'not-allowed',
51
+ },
40
52
  },
41
53
  });
42
54
 
43
55
  export default function App() {
44
- return <Card>Hello, Tasty!</Card>;
56
+ return (
57
+ <>
58
+ <Button>Save changes</Button>
59
+ <Button disabled>Disabled</Button>
60
+ </>
61
+ );
45
62
  }
46
63
  ```
47
64
 
48
- `tasty()` returns a normal React component. Values like `4x`, `#white`, `true`, and `1r` are Tasty DSL they map to CSS custom properties, shorthand expansions, and design tokens. See [Style Properties](styles.md) for the full reference.
65
+ `tasty()` returns a normal React component. The keys in each state map define priority: later branches win over earlier ones. Because `disabled` is last, a disabled button keeps its disabled styles even while the pointer is over it.
66
+
67
+ `disabled` and `checked` are built-in automatic states. Tasty activates them from the corresponding native prop or attribute, so `disabled` is the concise Tasty form of `[disabled]`, and `checked` is the concise form of `[checked]`.
68
+
69
+ Tasty compiles that priority into selectors that exclude one another. The hover, active, and disabled background rules cannot all match and ask the CSS cascade to decide the winner.
70
+
71
+ The example uses ordinary CSS values so it works without setup. A design system will usually replace them with shared tokens, units, and state aliases in the next step. See the [Style DSL](dsl.md) for the complete state-map syntax and [Style Properties](styles.md) for Tasty’s enhanced CSS properties.
49
72
 
50
73
  ---
51
74
 
52
- ## Optional: add shared configuration
75
+ ## Add your design system’s language
53
76
 
54
- Use `configure()` once, before your app renders, when your app or design system needs shared state aliases, tokens, recipes, or parser extensions:
77
+ The first component needs no configuration. Use `configure()` once, before your app renders, when your app or design system is ready to share tokens, state aliases, recipes, units, or parser extensions:
55
78
 
56
79
  ```tsx
57
80
  // src/tasty-config.ts
@@ -91,8 +114,8 @@ configure({
91
114
  '#primary': 'oklch(55% 0.25 265)',
92
115
  '#surface': '#fff',
93
116
  '#text': '#111',
94
- '$gap': '8px',
95
- '$radius': '4px',
117
+ $gap: '8px',
118
+ $radius: '4px',
96
119
  '$border-width': '1px',
97
120
  '$outline-width': '2px',
98
121
  },
@@ -148,23 +171,21 @@ export default [
148
171
 
149
172
  The `recommended` config enables 18 of the plugin's 27 total rules. It covers the most common issues without turning on the stricter governance rules:
150
173
 
151
- | Category | Rules | Examples |
152
- |----------|-------|---------|
153
- | Property validation | `known-property`, `valid-boolean-property`, `valid-sub-element` | Flags typos like `pading` or invalid boolean usage |
154
- | Value validation | `valid-value`, `valid-color-token`, `valid-custom-unit` | Catches `#nonexistent` tokens, bad unit syntax |
155
- | State validation | `valid-state-key`, `no-nested-state-map`, `require-default-state` | Validates state key syntax, ensures `''` default exists |
156
- | Structure | `valid-styles-structure`, `no-important`, `no-nested-selector` | Prevents `!important`, invalid nesting |
157
- | Static mode | `static-no-dynamic-values`, `static-valid-selector` | Enforces build-time constraints in `tastyStatic()` |
158
- | Style properties | `valid-preset`, `valid-recipe`, `valid-transition`, `valid-directional-modifier`, `valid-radius-shape` | Validates preset names, recipe references, transition syntax |
174
+ | Category | Rules | Examples |
175
+ | ------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
176
+ | Property validation | `known-property`, `valid-boolean-property`, `valid-sub-element` | Flags typos like `pading` or invalid boolean usage |
177
+ | Value validation | `valid-value`, `valid-color-token`, `valid-custom-unit` | Catches `#nonexistent` tokens, bad unit syntax |
178
+ | State validation | `valid-state-key`, `no-nested-state-map`, `require-default-state` | Validates state key syntax, ensures `''` default exists |
179
+ | Structure | `valid-styles-structure`, `no-important`, `no-nested-selector` | Prevents `!important`, invalid nesting |
180
+ | Static mode | `static-no-dynamic-values`, `static-valid-selector` | Enforces build-time constraints in `tastyStatic()` |
181
+ | Style properties | `valid-preset`, `valid-recipe`, `valid-transition`, `valid-directional-modifier`, `valid-radius-shape` | Validates preset names, recipe references, transition syntax |
159
182
 
160
183
  ### Strict config
161
184
 
162
185
  For stricter governance, use `tasty.configs.strict`. It adds rules that enforce best practices like preferring shorthand properties, consistent token usage, and flagging direct `styles` prop usage:
163
186
 
164
187
  ```js
165
- export default [
166
- tasty.configs.strict,
167
- ];
188
+ export default [tasty.configs.strict];
168
189
  ```
169
190
 
170
191
  ---
@@ -179,19 +200,19 @@ export default [
179
200
 
180
201
  ## Choosing a rendering mode
181
202
 
182
- `tasty()` is the default for all React apps. All `tasty()` components and style functions are hook-free and work as React Server Components without `'use client'`. In server-only contexts, they produce zero client JavaScript with the full feature set.
203
+ `tasty()` is the default for all React apps. All `tasty()` components and style functions are hook-free and work as React Server Components without `'use client'`. Zero-runtime delivery is not tied to one API: both server-only `tasty()` and build-time `tastyStatic()` can ship no Tasty styling runtime to the browser.
183
204
 
184
- | Approach | Entry point | Best for | Trade-off |
185
- |------|-------------|----------|-----------|
186
- | **Runtime (default)** | `tasty()` from `@tenphi/tasty` | All React apps — server-rendered by default, zero client JS until you need interactivity | Full feature set (styleProps, sub-elements, variants); CSS computed during rendering |
187
- | **Runtime + SSR integration** | Add `@tenphi/tasty/ssr/*` | Apps with client-side hydration (Next.js client components, Astro islands) | Adds CSS batching, deduplication, FOUC prevention, and client cache hydration |
188
- | **Zero-runtime** | `tastyStatic()` from `@tenphi/tasty/static` | Non-React frameworks or build-time extraction without React | Requires Babel plugin; no `styleProps` or runtime-only APIs |
205
+ | Approach | Authoring API | CSS is generated | Tasty runtime in the browser | Best for |
206
+ | ------------------------- | ------------------------------------------- | ----------------------------------- | ---------------------------- | ---------------------------------------------------------------- |
207
+ | **Server-only React** | `tasty()`; optional `@tenphi/tasty/ssr/*` | During server or static rendering | None | Astro without islands, server-only RSC, SSG |
208
+ | **Hydrated React** | `tasty()` plus `@tenphi/tasty/ssr/*` | During SSR, then on demand in React | Only in hydrated components | Interactive React apps, Next.js client components, Astro islands |
209
+ | **Build-time extraction** | `tastyStatic()` from `@tenphi/tasty/static` | During the build | None in file mode | Non-React frameworks or extraction before rendering |
189
210
 
190
211
  Both `tasty()` and `tastyStatic()` share the same DSL, tokens, units, and state mappings.
191
212
 
192
- - **Runtime** is the default and requires no extra setup beyond `@tenphi/tasty`. In server-only contexts (Next.js RSC, Astro without `client:*` directives, SSG), `tasty()` produces static HTML + CSS with zero client JavaScript — the same end result as `tastyStatic()` but with the full feature set. For example, `tasty()` + `tastyIntegration()` in Astro without islands gives you the complete API with zero JS shipped.
193
- - **SSR integration** is only needed when your app also has client-side rendering. Add `@tenphi/tasty/ssr/next`, `@tenphi/tasty/ssr/astro`, or the core SSR API to get CSS deduplication and cache hydration. See [Server-Side Rendering](ssr.md).
194
- - **Zero-runtime** requires the Babel plugin and additional peer dependencies. Use it when you need build-time extraction without a React runtime. See [Zero Runtime (tastyStatic)](tasty-static.md).
213
+ - **Server-only `tasty()`** keeps the full feature set (`styleProps`, sub-elements, variants) while generating CSS during server rendering. `tasty()` plus `tastyIntegration({ islands: false })` in Astro is the verified concrete setup and ships no client JavaScript. The same server-only architecture applies to Next.js RSC; verify the generated output for your deployment.
214
+ - **Hydrated `tasty()`** uses the SSR integrations to batch and deduplicate server CSS, prevent FOUC, and hydrate the client cache. The browser styling pipeline is available only where React components hydrate. See [Server-Side Rendering](ssr.md).
215
+ - **Build-time extraction** uses the Babel plugin and additional peer dependencies. Choose `tastyStatic()` when CSS must be generated before rendering or when the consumer is not React. File mode ships no styling runtime; inject mode intentionally includes a tiny CSS injector. See [Build-Time Extraction (`tastyStatic`)](tasty-static.md).
195
216
 
196
217
  ---
197
218
 
@@ -214,4 +235,4 @@ Both `tasty()` and `tastyStatic()` share the same DSL, tokens, units, and state
214
235
 
215
236
  - Styles are missing on first render: make sure the file that calls `configure()` is imported before any `tasty()` component renders.
216
237
  - Token or unit values are not what you expect: check your `configure({ tokens, units })` setup, then inspect the generated CSS variables with [Debug Utilities](debug.md).
217
- - You need build-time extraction or server-rendered CSS delivery: use [Zero Runtime (tastyStatic)](tasty-static.md) for extraction, or add [Server-Side Rendering](ssr.md) on top of runtime `tasty()` when your framework renders on the server.
238
+ - You need zero-runtime delivery: use server-only [`tasty()` with server rendering](ssr.md) to keep the full React API, or use [`tastyStatic()` build-time extraction](tasty-static.md) when styles must be compiled before rendering.
package/docs/injector.md CHANGED
@@ -9,7 +9,7 @@ A high-performance CSS-in-JS solution that powers the Tasty design system with e
9
9
  The Style Injector is the core engine behind Tasty's styling system, providing:
10
10
 
11
11
  - **Hash-based deduplication** - Identical CSS gets the same className
12
- - **Reference counting** - Automatic cleanup when components unmount (refCount = 0)
12
+ - **DOM-driven lifetime** - Styles are collected once nothing renders them
13
13
  - **CSS nesting flattening** - Handles `&`, `.Class`, `SubElement` patterns
14
14
  - **At-rule injection** - First-class `@keyframes`, `@property`, `@font-face`, `@counter-style`, and `@function` support
15
15
  - **Smart cleanup** - CSS rules batched cleanup, keyframes disposed immediately
@@ -63,10 +63,16 @@ const result = inject([{
63
63
 
64
64
  console.log(result.className); // 't-abc123'
65
65
 
66
- // Cleanup when component unmounts (refCount decremented)
66
+ // Release the pin; the class becomes collectible once nothing renders it
67
67
  result.dispose();
68
68
  ```
69
69
 
70
+ `inject()` **pins** the class it returns, and `gc()` never evicts a pinned class
71
+ — that is what `dispose()` releases. Pass `{ pin: false }` when the caller keeps
72
+ no handle and the DOM is the only record that the class is in use; `dispose()`
73
+ is then a no-op. The render path (`tasty()` / `computeStyles()`) injects this
74
+ way, because a hook-free render has no unmount signal to dispose on.
75
+
70
76
  ### `injectGlobal(rules, options?): { dispose: () => void }`
71
77
 
72
78
  Injects global styles that don't reserve tasty class names.
@@ -245,8 +251,27 @@ Dispose, ref-counted cleanup and GC therefore behave identically in every mode.
245
251
  - Most options have sensible defaults and auto-detection
246
252
  - `configure()` is optional - the injector works with defaults
247
253
  - **Configuration is locked after styles are generated** - calling `configure()` after first render will emit a warning and be ignored
248
- - `gc.touchInterval`: Number of touch events between GC cycles. Each style render counts as a touch. When the counter reaches this value, GC is scheduled via `requestIdleCallback`.
249
- - `gc.capacity`: Maximum number of unused styles (refCount = 0, not in DOM) to retain. When exceeded, the oldest are evicted first. Actively referenced styles don't count against this limit.
254
+ - `gc.touchInterval`: Number of renders between sweeps. When the counter reaches this value, a sweep is scheduled via `requestIdleCallback`; without idle callbacks nothing is collected automatically.
255
+ - `gc.grace`: How long a class is left alone after collection first notices nothing is carrying it, in milliseconds (default `10000`).
256
+
257
+ **What a sweep does.** Everything the injector holds falls into one of five bands, and only the last is ever deleted:
258
+
259
+ | Band | | Deleted |
260
+ |---|---|---|
261
+ | 1 | **Rendered** — some element carries the class right now | never |
262
+ | 2 | **Not ours** — queued for a batched write, pre-allocated, or server-rendered | never |
263
+ | 3 | **Hot** — nothing carries it, but that was noticed less than `grace` ago | never |
264
+ | 4 | **Cached** — cold, but within `capacity` when ordered by when it went cold | never |
265
+ | 5 | Everything else | on every sweep |
266
+
267
+ `gc({ force: true })` and `cleanup()` take bands 4 and 5 together, ignoring capacity. Band 3 is spared even then: an explicit cleanup is still no reason to take rules from a render that has not committed yet.
268
+
269
+ > **Why band 3 exists.** Rendering is not commit-aware: a render can resolve a class and commit it a little later, and in between nothing on the page carries it. From outside React that is indistinguishable from a class that is finished, so collection does not try to tell them apart — it leaves alone anything only just noticed to be cold. A render would have to stay pending for the whole window to lose its rules, and it gets them back on its next render.
270
+ >
271
+ > The clock starts when a sweep *notices*, not when the element actually left — nothing observes that moment, so starting it at the sighting is what gives every class the same full window however long ago it went.
272
+ >
273
+ > This is also why collection costs nothing to run: the timestamps are written by the sweep's own DOM scan, so rendering tracks nothing per class.
274
+ - `gc.capacity`: Maximum number of unused styles (not in the DOM, not pinned) to retain. When exceeded, the least recently used are evicted first. Rendered and pinned styles don't count against this limit.
250
275
 
251
276
  ---
252
277
 
@@ -329,23 +354,28 @@ const button2 = inject([{
329
354
  console.log(button1.className === button2.className); // true
330
355
  ```
331
356
 
332
- ### Reference Counting
357
+ ### Pinning
333
358
 
334
359
  ```typescript
335
- // Multiple components using the same styles
360
+ // Multiple callers using the same styles
336
361
  const comp1 = inject([commonStyle]);
337
362
  const comp2 = inject([commonStyle]);
338
363
  const comp3 = inject([commonStyle]);
339
364
 
340
- // Style is kept alive while any component uses it
341
- comp1.dispose(); // refCount: 3 → 2
342
- comp2.dispose(); // refCount: 2 → 1
343
- comp3.dispose(); // refCount: 1 → 0, eligible for bulk cleanup
365
+ // Style is pinned while any caller holds a handle
366
+ comp1.dispose(); // pins: 3 → 2
367
+ comp2.dispose(); // pins: 2 → 1
368
+ comp3.dispose(); // pins: 1 → 0, now up to the DOM and gc()
344
369
 
345
- // Rule exists but refCount = 0 means unused
346
- // Next inject() with same styles will increment refCount and reuse immediately
370
+ // Unpinned does not mean deleted: the rule stays cached and is reused instantly
371
+ // by the next inject(). gc() decides when it actually goes.
347
372
  ```
348
373
 
374
+ Pinning covers callers that hold a handle. Styles that come from rendering are
375
+ never pinned — see `{ pin: false }` under
376
+ [`inject()`](#injectrules-options-injectresult) — so what keeps them alive is
377
+ being in the DOM, and nothing else.
378
+
349
379
  ### Garbage Collection
350
380
 
351
381
  ```typescript
@@ -353,6 +383,9 @@ import { configure, gc } from '@tenphi/tasty';
353
383
 
354
384
  // Keyframes: Disposed immediately when refCount = 0 (safer for global scope)
355
385
  // CSS rules: Tracked by touch count and cleaned up via gc()
386
+ //
387
+ // A CSS rule is collectible when no element carries its class AND nobody
388
+ // pinned it with inject().
356
389
 
357
390
  configure({
358
391
  gc: {
@@ -367,15 +400,19 @@ gc();
367
400
  // Force-remove ALL unused styles (e.g. on route change or test teardown):
368
401
  gc({ force: true });
369
402
 
370
- // GC is also triggered automatically by touch count during rendering.
371
- // Every `touchInterval` touches, GC is scheduled via requestIdleCallback.
403
+ // cleanup() is the same thing:
404
+ cleanup();
405
+
406
+ // Every `touchInterval` renders, a sweep is scheduled in idle time. It scans
407
+ // the DOM for the classes actually on the page and sorts everything the
408
+ // injector holds into five bands — only the last one is deleted. See below.
372
409
 
373
410
  // Benefits:
374
411
  // - Activity-proportional: busy apps trigger GC more often
375
412
  // - DOM-safe: styles currently in the DOM are never evicted
376
413
  // - Oldest-first: least recently used styles are evicted first
377
414
  // - Keyframes: Immediate cleanup prevents global namespace pollution
378
- // - Unused styles can be instantly reactivated (just increment refCount)
415
+ // - Unused styles stay cached until evicted, so re-rendering them is a cache hit
379
416
  ```
380
417
 
381
418
  ### Shadow DOM Support
@@ -470,11 +507,11 @@ const metrics = injector.instance.getMetrics();
470
507
  console.log({
471
508
  cacheHits: metrics.hits, // Successful cache hits
472
509
  cacheMisses: metrics.misses, // New styles injected
473
- unusedHits: metrics.unusedHits, // Current unused styles (calculated on demand)
510
+ unusedHits: metrics.unusedHits, // Styles currently eligible for eviction (scans the DOM)
474
511
  bulkCleanups: metrics.bulkCleanups, // Number of bulk cleanup operations
475
512
  stylesCleanedUp: metrics.stylesCleanedUp, // Total styles removed in bulk cleanups
476
513
  totalInsertions: metrics.totalInsertions, // Lifetime insertions
477
- totalUnused: metrics.totalUnused, // Total styles marked as unused (refCount = 0)
514
+ totalUnused: metrics.totalUnused, // Times a pinned style lost its last pin
478
515
  startTime: metrics.startTime, // Metrics collection start timestamp
479
516
  cleanupHistory: metrics.cleanupHistory, // Detailed cleanup operation history
480
517
  });
@@ -518,10 +555,10 @@ metrics.cleanupHistory.forEach(cleanup => {
518
555
 
519
556
  ```typescript
520
557
  // ✅ Reuse styles - identical CSS gets deduplicated
521
- const buttonBase = { padding: '8px 16px', borderRadius: '4px' };
558
+ const buttonBase = 'padding: 8px 16px; border-radius: 4px;';
522
559
 
523
560
  // ✅ Avoid frequent disposal and re-injection
524
- // Let the reference counting system handle cleanup
561
+ // Let the injector handle cleanup
525
562
 
526
563
  // ✅ Use bulk operations for global styles
527
564
  injectGlobal([
@@ -547,13 +584,13 @@ configure({
547
584
  // The injector automatically manages memory through:
548
585
 
549
586
  // 1. Hash-based deduplication - same CSS = same className
550
- // 2. Reference counting - styles stay alive while in use (refCount > 0)
551
- // 3. Immediate keyframes cleanup - disposed instantly when refCount = 0
552
- // 4. Touch-count GC - unused CSS rules are evicted oldest-first when over capacity
553
- // 5. DOM safety guard - styles visible in the DOM are never evicted
587
+ // 2. DOM-driven lifetime - a rendered class is never evicted
588
+ // 3. Pinning - inject() callers hold their classes until they dispose
589
+ // 4. Immediate keyframes cleanup - disposed instantly when refCount = 0
590
+ // 5. Touch-count GC - unused CSS rules are evicted oldest-first when over capacity
554
591
 
555
592
  // Manual cleanup is rarely needed but available:
556
- cleanup(); // Force immediate cleanup of all unused CSS rules (refCount = 0)
593
+ cleanup(); // Remove every rule that is neither rendered nor pinned
557
594
  destroy(); // Nuclear option: remove all stylesheets and reset
558
595
  ```
559
596
 
@@ -575,9 +612,9 @@ const StyledButton = tasty({
575
612
 
576
613
  // Internally uses the injector:
577
614
  // 1. Styles are parsed into StyleResult objects
578
- // 2. inject() is called with the parsed results
615
+ // 2. inject() is called with the parsed results, unpinned
579
616
  // 3. Component gets the returned className
580
- // 4. dispose() is called when component unmounts
617
+ // 4. gc() reclaims the class once no element carries it
581
618
  ```
582
619
 
583
620
  For most development, you'll use the [React API](./react-api.md) rather than the injector directly. The injector provides the high-performance foundation that makes Tasty's declarative styling possible.
@@ -1,6 +1,8 @@
1
1
  # Methodology
2
2
 
3
- Tasty has opinions about how components should be structured. The patterns described here are not mandatory — Tasty works without them — but following them gets the most out of the engine: deterministic state resolution, cleaner component APIs, simpler overrides, and fewer surprises as the system grows.
3
+ State maps keep a property’s branches from fighting. This guide applies the same goal to component architecture: keep state ownership explicit, expose intentional public APIs, and make extension predictable as a component system grows.
4
+
5
+ The patterns are recommended rather than required. Tasty works without them, but they help design-system teams carry its state-resolution guarantees across roots, sub-elements, typed props, tokens, and styled wrappers.
4
6
 
5
7
  ---
6
8
 
@@ -75,7 +77,7 @@ const Nav = tasty({
75
77
  '': '#text',
76
78
  '@own(:hover)': '#primary',
77
79
  '@own(:focus-visible)': '#primary',
78
- 'selected': '#primary',
80
+ selected: '#primary',
79
81
  },
80
82
  },
81
83
  },
@@ -117,16 +119,16 @@ Style props accept state maps, so responsive values work through the same API:
117
119
 
118
120
  Tasty exports predefined style prop lists that group properties by role. Use them instead of hand-picking arrays:
119
121
 
120
- | Preset | Properties | Typical use |
121
- |--------|-----------|-------------|
122
- | `FLOW_STYLES` | flow, gap, columnGap, rowGap, align, justify, placeItems, placeContent, alignItems, alignContent, justifyItems, justifyContent, gridColumns, gridRows, gridTemplate, gridAreas | Layout containers (`Space`, `Grid`) |
123
- | `POSITION_STYLES` | gridArea, gridColumn, gridRow, order, placeSelf, alignSelf, justifySelf, zIndex, margin, inset, position | Positioned elements (`Button`, `Badge`) |
124
- | `DIMENSION_STYLES` | width, height, flexBasis, flexGrow, flexShrink, flex | Sized elements |
125
- | `COLOR_STYLES` | color, fill, fade, image | Color-customizable elements |
126
- | `BLOCK_STYLES` | padding, paddingInline, paddingBlock, overflow, scrollbar, textAlign, border, radius, shadow, outline | Block-level containers |
127
- | `CONTAINER_STYLES` | All of the above combined (+ BASE_STYLES) | Fully flexible containers |
128
- | `OUTER_STYLES` | POSITION_STYLES + DIMENSION_STYLES + block outer (border, radius, shadow, outline) | Components whose outer shell is customizable |
129
- | `INNER_STYLES` | BASE_STYLES + COLOR_STYLES + block inner (padding, overflow, scrollbar) + FLOW_STYLES | Components whose inner layout is customizable |
122
+ | Preset | Properties | Typical use |
123
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
124
+ | `FLOW_STYLES` | flow, gap, columnGap, rowGap, align, justify, placeItems, placeContent, alignItems, alignContent, justifyItems, justifyContent, gridColumns, gridRows, gridTemplate, gridAreas | Layout containers (`Space`, `Grid`) |
125
+ | `POSITION_STYLES` | gridArea, gridColumn, gridRow, order, placeSelf, alignSelf, justifySelf, zIndex, margin, inset, position | Positioned elements (`Button`, `Badge`) |
126
+ | `DIMENSION_STYLES` | width, height, flexBasis, flexGrow, flexShrink, flex | Sized elements |
127
+ | `COLOR_STYLES` | color, fill, fade, image | Color-customizable elements |
128
+ | `BLOCK_STYLES` | padding, paddingInline, paddingBlock, overflow, scrollbar, textAlign, border, radius, shadow, outline | Block-level containers |
129
+ | `CONTAINER_STYLES` | All of the above combined (+ BASE_STYLES) | Fully flexible containers |
130
+ | `OUTER_STYLES` | POSITION_STYLES + DIMENSION_STYLES + block outer (border, radius, shadow, outline) | Components whose outer shell is customizable |
131
+ | `INNER_STYLES` | BASE_STYLES + COLOR_STYLES + block inner (padding, overflow, scrollbar) + FLOW_STYLES | Components whose inner layout is customizable |
130
132
 
131
133
  ```tsx
132
134
  import { tasty, FLOW_STYLES, POSITION_STYLES } from '@tenphi/tasty';
@@ -159,7 +161,7 @@ Match the preset to the component's role:
159
161
 
160
162
  ### The governance trade-off
161
163
 
162
- Exposing every CSS property as a prop defeats the purpose of a design system. The more props a component exposes, the more ways product engineers can deviate from the intended design. A good rule of thumb: expose props that product engineers *need* to adjust for layout and composition, and keep visual identity (colors, borders, typography) controlled through the component definition, variants, or styled wrappers.
164
+ Exposing every CSS property as a prop defeats the purpose of a design system. The more props a component exposes, the more ways product engineers can deviate from the intended design. A good rule of thumb: expose props that product engineers _need_ to adjust for layout and composition, and keep visual identity (colors, borders, typography) controlled through the component definition, variants, or styled wrappers.
163
165
 
164
166
  ---
165
167
 
@@ -180,17 +182,19 @@ const Card = tasty({
180
182
  });
181
183
 
182
184
  // Clean prop API — no mods object needed
183
- <Card isLoading isSelected>Content</Card>
185
+ <Card isLoading isSelected>
186
+ Content
187
+ </Card>;
184
188
  ```
185
189
 
186
190
  ### When to use which
187
191
 
188
- | Pattern | Use when |
189
- |---|---|
190
- | `modProps` | The component has a fixed set of known boolean/string states that drive styles. Provides TypeScript autocomplete and a cleaner JSX API. |
191
- | `mods` prop | The component needs arbitrary or dynamic modifiers that aren't known at definition time. |
192
- | Both | Combine `modProps` for the known states and `mods` for ad-hoc overrides. Mod props take precedence. |
193
- | `styleProps` | Exposing CSS properties (layout, sizing) for customization — different from modifiers. |
192
+ | Pattern | Use when |
193
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
194
+ | `modProps` | The component has a fixed set of known boolean/string states that drive styles. Provides TypeScript autocomplete and a cleaner JSX API. |
195
+ | `mods` prop | The component needs arbitrary or dynamic modifiers that aren't known at definition time. |
196
+ | Both | Combine `modProps` for the known states and `mods` for ad-hoc overrides. Mod props take precedence. |
197
+ | `styleProps` | Exposing CSS properties (layout, sizing) for customization — different from modifiers. |
194
198
 
195
199
  ### Typed modProps vs array form
196
200
 
@@ -239,20 +243,20 @@ const ProgressBar = tasty({
239
243
  });
240
244
 
241
245
  // Usage: the progress value comes from a prop, not from styles
242
- <ProgressBar tokens={{ '$progress': `${percent}%` }} />
246
+ <ProgressBar tokens={{ $progress: `${percent}%` }} />;
243
247
  ```
244
248
 
245
249
  The `tokens` prop sets `style="--progress: 75%"` on the DOM element. The `$progress` reference in styles maps to `var(--progress)`, so the bar width updates without regenerating any CSS.
246
250
 
247
251
  ### When to use tokens vs other mechanisms
248
252
 
249
- | Need | Use |
250
- |------|-----|
251
- | Value changes per instance at render time (progress, user color, avatar size) | `tokens` prop (on component) |
252
- | Value is constant across all instances (card padding, border radius) | `configure({ tokens })` for `:root` CSS custom properties |
253
- | Value should be inlined at parse time (alias for another token) | `configure({ replaceTokens })` |
254
- | Value changes based on component state (hover, disabled, breakpoint) | State map in `styles` |
255
- | Value changes based on a variant (primary, danger, outline) | `variants` option |
253
+ | Need | Use |
254
+ | ----------------------------------------------------------------------------- | --------------------------------------------------------- |
255
+ | Value changes per instance at render time (progress, user color, avatar size) | `tokens` prop (on component) |
256
+ | Value is constant across all instances (card padding, border radius) | `configure({ tokens })` for `:root` CSS custom properties |
257
+ | Value should be inlined at parse time (alias for another token) | `configure({ replaceTokens })` |
258
+ | Value changes based on component state (hover, disabled, breakpoint) | State map in `styles` |
259
+ | Value changes based on a variant (primary, danger, outline) | `variants` option |
256
260
 
257
261
  Design tokens (via `configure({ tokens })`) are injected as CSS custom properties on `:root`. Replace tokens (via `configure({ replaceTokens })`) are resolved at parse time and baked into the generated CSS. The `tokens` prop on components is resolved at render time via inline CSS custom properties. Use design tokens for design-system constants, replace tokens for value aliases, and the `tokens` prop for truly dynamic per-instance values.
258
262
 
@@ -271,7 +275,7 @@ const ProgressBar = tasty({
271
275
  });
272
276
 
273
277
  // Clean prop API — no tokens object needed
274
- <ProgressBar progress="75%" accentColor="#purple" />
278
+ <ProgressBar progress="75%" accentColor="#purple" />;
275
279
 
276
280
  // Conversion:
277
281
  // 'progress' → $progress → --progress
@@ -291,7 +295,7 @@ const Card = tasty({
291
295
  styles: { padding: '$card-size', fill: '#card-accent' },
292
296
  });
293
297
 
294
- <Card size="4x" color="#purple" />
298
+ <Card size="4x" color="#purple" />;
295
299
  ```
296
300
 
297
301
  #### Merge order
@@ -326,7 +330,7 @@ const LargeCard = tasty(Card, {
326
330
  styles: { padding: '6x', Title: { color: '#danger' } },
327
331
  });
328
332
 
329
- <LargeCard />
333
+ <LargeCard />;
330
334
  ```
331
335
 
332
336
  Why? Styled wrappers are:
@@ -417,10 +421,12 @@ const Link = tasty({
417
421
 
418
422
  const Span = tasty({
419
423
  as: 'span',
420
- styles: { fontWeight: 'bold' },
424
+ styles: { preset: 'strong' },
421
425
  });
422
426
 
423
- <Link href="/blog" padding="1x">Blog</Link>;
427
+ <Link href="/blog" padding="1x">
428
+ Blog
429
+ </Link>;
424
430
  ```
425
431
 
426
432
  The wrapped component only needs to forward `className` (and ideally `style` and `ref`) to its underlying DOM node. Tasty-specific props (`qa`, `qaVal`, `mods`, `tokens`, `isDisabled`, `isHidden`, `isChecked`, and any `styleProps`/`modProps`/`tokenProps` you declared) are consumed by the wrapper and converted to `data-*` attributes or CSS custom properties — they never leak to the DOM.
@@ -593,7 +599,7 @@ Modifiers are compiled into exclusive selectors once. Changing `styles` at runti
593
599
 
594
600
  ```tsx
595
601
  // Bad: bypassing Tasty for custom styling
596
- <Button style={{ backgroundColor: 'red', padding: '12px 24px' }} />
602
+ <Button style={{ backgroundColor: 'red', padding: '12px 24px' }} />;
597
603
 
598
604
  // Good: create a styled wrapper
599
605
  const DangerButton = tasty(Button, {
@@ -610,7 +616,7 @@ The `style` prop bypasses tokens, units, and state maps. It should only be used
610
616
  <Card>
611
617
  <div data-element="Title">Card Title</div>
612
618
  <div data-element="Content">Card content</div>
613
- </Card>
619
+ </Card>;
614
620
 
615
621
  // Better: declare elements for typed sub-components
616
622
  const Card = tasty({
@@ -624,7 +630,7 @@ const Card = tasty({
624
630
  <Card>
625
631
  <Card.Title>Card Title</Card.Title>
626
632
  <Card.Content>Card content</Card.Content>
627
- </Card>
633
+ </Card>;
628
634
  ```
629
635
 
630
636
  The `elements` prop gives you typed sub-components with automatic `data-element` attributes, `mods` support, and better discoverability.