@tenphi/tasty 3.3.1 → 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.
- package/README.md +147 -129
- package/dist/astro-ib7E7V4Y.js +307 -0
- package/dist/astro-ib7E7V4Y.js.map +1 -0
- package/dist/{babel-BUQGeOXA.d.ts → babel-DWCsafYw.d.ts} +2 -2
- package/dist/{collector-DUaHCcTS.d.ts → collector-BTVXj2hS.d.ts} +21 -3
- package/dist/{collector-BKqNBmzA.js → collector-C6TtL8HJ.js} +36 -13
- package/dist/collector-C6TtL8HJ.js.map +1 -0
- package/dist/{config-BCdCTIED.js → config-B3gPdCqd.js} +344 -209
- package/dist/config-B3gPdCqd.js.map +1 -0
- package/dist/{config-YsxGv4tq.d.ts → config-LfIDmVHx.d.ts} +132 -32
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +6 -6
- package/dist/{core-wxP3GHQu.js → core-Bq7w2kti.js} +219 -98
- package/dist/core-Bq7w2kti.js.map +1 -0
- package/dist/{css-writer-D64NY9AX.js → css-writer-Bgowobbx.js} +3 -3
- package/dist/{css-writer-D64NY9AX.js.map → css-writer-Bgowobbx.js.map} +1 -1
- package/dist/{format-rules-Bo_e2u7r.js → format-rules-rCZ37rqY.js} +2 -2
- package/dist/format-rules-rCZ37rqY.js.map +1 -0
- package/dist/{hydrate-CMKOuKAx.js → hydrate-CNOmZprz.js} +3 -3
- package/dist/{hydrate-CMKOuKAx.js.map → hydrate-CNOmZprz.js.map} +1 -1
- package/dist/{index-DhhUI0yi.d.ts → index-Bm9ltvdH.d.ts} +45 -16
- package/dist/{index-Cd45t5NM.d.ts → index-BmogEzTo.d.ts} +69 -15
- package/dist/index.d.ts +5 -5
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/{keyframes-D737PShJ.js → keyframes-DE-OE76F.js} +132 -21
- package/dist/keyframes-DE-OE76F.js.map +1 -0
- package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BcLl1u_d.d.ts} +2 -2
- package/dist/{merge-styles-CUIQcs5v.js → merge-styles-DuoZEsm9.js} +2 -2
- package/dist/{merge-styles-CUIQcs5v.js.map → merge-styles-DuoZEsm9.js.map} +1 -1
- package/dist/{resolve-recipes-Df1Ta-Q0.js → resolve-recipes-H9NqOQuP.js} +3 -3
- package/dist/{resolve-recipes-Df1Ta-Q0.js.map → resolve-recipes-H9NqOQuP.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract-static.js +9 -0
- package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
- package/dist/ssr/astro-middleware-extract.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract.js +9 -0
- package/dist/ssr/astro-middleware-extract.js.map +1 -0
- package/dist/ssr/astro-middleware-static.d.ts +3 -1
- package/dist/ssr/astro-middleware-static.js +1 -1
- package/dist/ssr/astro-middleware.d.ts +3 -1
- package/dist/ssr/astro-middleware.js +1 -1
- package/dist/ssr/astro.d.ts +37 -3
- package/dist/ssr/astro.js +1 -156
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/index.js +3 -3
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next.d.ts +1 -1
- package/dist/ssr/next.js +4 -4
- package/dist/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +4 -4
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/dist/zero/next.d.ts +1 -1
- package/docs/README.md +13 -8
- package/docs/adoption.md +56 -31
- package/docs/ai-agents.md +78 -77
- package/docs/comparison.md +69 -40
- package/docs/configuration.md +97 -87
- package/docs/debug.md +4 -2
- package/docs/design-system.md +41 -22
- package/docs/dsl.md +225 -195
- package/docs/getting-started.md +60 -39
- package/docs/injector.md +63 -26
- package/docs/methodology.md +42 -36
- package/docs/migration-v3.md +49 -49
- package/docs/plugins.md +37 -33
- package/docs/react-api.md +65 -44
- package/docs/runtime-benchmarks.md +178 -0
- package/docs/ssr.md +99 -61
- package/docs/styles.md +1 -1
- package/docs/tasty-static.md +137 -104
- package/package.json +26 -8
- package/dist/collector-BKqNBmzA.js.map +0 -1
- package/dist/config-BCdCTIED.js.map +0 -1
- package/dist/core-wxP3GHQu.js.map +0 -1
- package/dist/format-rules-Bo_e2u7r.js.map +0 -1
- package/dist/keyframes-D737PShJ.js.map +0 -1
- package/dist/ssr/astro.js.map +0 -1
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
31
|
-
as: '
|
|
32
|
+
const Button = tasty({
|
|
33
|
+
as: 'button',
|
|
32
34
|
styles: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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.
|
|
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
|
-
##
|
|
75
|
+
## Add your design system’s language
|
|
53
76
|
|
|
54
|
-
Use `configure()` once, before your app renders, when your app or design system
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
| Property validation | `known-property`, `valid-boolean-property`, `valid-sub-element`
|
|
154
|
-
| Value validation
|
|
155
|
-
| State validation
|
|
156
|
-
| Structure
|
|
157
|
-
| Static mode
|
|
158
|
-
| Style properties
|
|
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'`.
|
|
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 |
|
|
185
|
-
|
|
186
|
-
| **
|
|
187
|
-
| **
|
|
188
|
-
| **
|
|
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
|
-
- **
|
|
193
|
-
- **
|
|
194
|
-
- **
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
//
|
|
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
|
|
249
|
-
- `gc.
|
|
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
|
-
###
|
|
357
|
+
### Pinning
|
|
333
358
|
|
|
334
359
|
```typescript
|
|
335
|
-
// Multiple
|
|
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
|
|
341
|
-
comp1.dispose(); //
|
|
342
|
-
comp2.dispose(); //
|
|
343
|
-
comp3.dispose(); //
|
|
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
|
-
//
|
|
346
|
-
//
|
|
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
|
-
//
|
|
371
|
-
|
|
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
|
|
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, //
|
|
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, //
|
|
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 =
|
|
558
|
+
const buttonBase = 'padding: 8px 16px; border-radius: 4px;';
|
|
522
559
|
|
|
523
560
|
// ✅ Avoid frequent disposal and re-injection
|
|
524
|
-
// Let the
|
|
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.
|
|
551
|
-
// 3.
|
|
552
|
-
// 4.
|
|
553
|
-
// 5.
|
|
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(); //
|
|
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.
|
|
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.
|
package/docs/methodology.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Methodology
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
| `FLOW_STYLES`
|
|
123
|
-
| `POSITION_STYLES`
|
|
124
|
-
| `DIMENSION_STYLES` | width, height, flexBasis, flexGrow, flexShrink, flex
|
|
125
|
-
| `COLOR_STYLES`
|
|
126
|
-
| `BLOCK_STYLES`
|
|
127
|
-
| `CONTAINER_STYLES` | All of the above combined (+ BASE_STYLES)
|
|
128
|
-
| `OUTER_STYLES`
|
|
129
|
-
| `INNER_STYLES`
|
|
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
|
|
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>
|
|
185
|
+
<Card isLoading isSelected>
|
|
186
|
+
Content
|
|
187
|
+
</Card>;
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
### When to use which
|
|
187
191
|
|
|
188
|
-
| Pattern
|
|
189
|
-
|
|
190
|
-
| `modProps`
|
|
191
|
-
| `mods` prop
|
|
192
|
-
| Both
|
|
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={{
|
|
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
|
|
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)
|
|
253
|
-
| Value should be inlined at parse time (alias for another token)
|
|
254
|
-
| Value changes based on component state (hover, disabled, breakpoint)
|
|
255
|
-
| Value changes based on a variant (primary, danger, outline)
|
|
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: {
|
|
424
|
+
styles: { preset: 'strong' },
|
|
421
425
|
});
|
|
422
426
|
|
|
423
|
-
<Link href="/blog" padding="1x">
|
|
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.
|