@tenphi/tasty 3.9.11 → 3.11.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 +399 -380
- package/dist/babel-types-BKDTT4--.d.ts +18 -0
- package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
- package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
- package/dist/chunks/build-config-5d8WlQbq.js +60 -0
- package/dist/chunks/build-config-5d8WlQbq.js.map +1 -0
- package/dist/chunks/{collector-D2ARGT2L.js → collector-CjCak4iE.js} +8 -6
- package/dist/chunks/collector-CjCak4iE.js.map +1 -0
- package/dist/chunks/{config-engine-DEmmPPdp.js → config-engine-uOFYZidB.js} +4 -4
- package/dist/chunks/{config-engine-DEmmPPdp.js.map → config-engine-uOFYZidB.js.map} +1 -1
- package/dist/chunks/config-file-DlZXJMj8.js +25 -0
- package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
- package/dist/chunks/{css-definitions-DotDkIXt.js → css-definitions-ubrHP97m.js} +130 -96
- package/dist/chunks/css-definitions-ubrHP97m.js.map +1 -0
- package/dist/chunks/{debug-D8pg9RGI.js → debug-CJ31U_WE.js} +6 -6
- package/dist/chunks/{debug-D8pg9RGI.js.map → debug-CJ31U_WE.js.map} +1 -1
- package/dist/chunks/{dsl-DRhFWQzo.js → dsl-DYSJc1-T.js} +11 -6
- package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
- package/dist/chunks/{react-runtime-BpxlOTbO.js → react-runtime-CyyCI02N.js} +190 -63
- package/dist/chunks/react-runtime-CyyCI02N.js.map +1 -0
- package/dist/chunks/{runtime-engine-CKFXUH-A.js → runtime-engine-BQmOu5-W.js} +69 -53
- package/dist/chunks/runtime-engine-BQmOu5-W.js.map +1 -0
- package/dist/chunks/{runtime-state-BxaLhVdP.js → runtime-state-DEsyUMU-.js} +28 -3
- package/dist/chunks/runtime-state-DEsyUMU-.js.map +1 -0
- package/dist/chunks/{style-engine-BBbBuSkT.js → style-engine-BX2LshDZ.js} +309 -103
- package/dist/chunks/style-engine-BX2LshDZ.js.map +1 -0
- package/dist/chunks/{zero-engine-gJrKBRiw.js → zero-engine-Dw0P4yGM.js} +62 -27
- package/dist/chunks/zero-engine-Dw0P4yGM.js.map +1 -0
- package/dist/{collector-w4mBj5gd.d.ts → collector-CTU-nl0g.d.ts} +2 -2
- package/dist/{config-CZWCzBr6.d.ts → config-CcxqSZI2.d.ts} +35 -16
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +7 -7
- package/dist/hydrate-CRyjjAyl.d.ts +22 -0
- package/dist/{index-G9JuwM9p.d.ts → index-xtey-KDu.d.ts} +43 -13
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/{merge-styles-VXwJ0Frr.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -2
- package/dist/ssr/astro.js +4 -4
- package/dist/ssr/client.d.ts +2 -0
- package/dist/ssr/client.js +2 -0
- package/dist/ssr/index.d.ts +3 -22
- package/dist/ssr/index.js +4 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next-config.d.ts +1 -1
- package/dist/ssr/next-config.js +13 -18
- package/dist/ssr/next-config.js.map +1 -1
- package/dist/ssr/next.d.ts +1 -1
- package/dist/ssr/next.js +3 -3
- package/dist/static/index.d.ts +2 -2
- package/dist/{types-JEY8PVYP.d.ts → types-DJEM_BOJ.d.ts} +6 -6
- package/dist/zero/babel.d.ts +67 -2
- package/dist/zero/babel.js +47 -44
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +58 -3
- package/dist/zero/index.js +24 -2
- package/dist/zero/index.js.map +1 -0
- package/dist/zero/next.d.ts +2 -1
- package/dist/zero/next.js +3 -7
- package/dist/zero/next.js.map +1 -1
- package/docs/README.md +4 -1
- package/docs/adoption.md +25 -17
- package/docs/ai-agents.md +37 -34
- package/docs/comparison.md +9 -8
- package/docs/configuration.md +174 -59
- package/docs/debug.md +3 -2
- package/docs/design-system.md +117 -15
- package/docs/dsl.md +122 -81
- package/docs/getting-started.md +81 -24
- package/docs/injector.md +39 -40
- package/docs/methodology.md +45 -20
- package/docs/migration-v3.md +2 -2
- package/docs/pipeline.md +20 -17
- package/docs/plugins.md +20 -40
- package/docs/react-api.md +28 -8
- package/docs/runtime-benchmarks.md +4 -4
- package/docs/ssr.md +83 -63
- package/docs/styles.md +27 -17
- package/docs/tasty-static.md +142 -55
- package/package.json +24 -10
- package/dist/babel-rZcEwPLM.d.ts +0 -82
- package/dist/chunks/async-storage-CG5HnUT5.js +0 -20
- package/dist/chunks/async-storage-CG5HnUT5.js.map +0 -1
- package/dist/chunks/build-config-CL4eRu2z.js +0 -42
- package/dist/chunks/build-config-CL4eRu2z.js.map +0 -1
- package/dist/chunks/collector-D2ARGT2L.js.map +0 -1
- package/dist/chunks/css-definitions-DotDkIXt.js.map +0 -1
- package/dist/chunks/dsl-DRhFWQzo.js.map +0 -1
- package/dist/chunks/react-runtime-BpxlOTbO.js.map +0 -1
- package/dist/chunks/runtime-engine-CKFXUH-A.js.map +0 -1
- package/dist/chunks/runtime-state-BxaLhVdP.js.map +0 -1
- package/dist/chunks/style-engine-BBbBuSkT.js.map +0 -1
- package/dist/chunks/zero-engine-gJrKBRiw.js.map +0 -1
package/docs/injector.md
CHANGED
|
@@ -10,10 +10,10 @@ 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
|
+
- **Selector assembly** - Prepends the doubled `.tX.tX` class and root prefix to the pipeline's selector fragments (sub-element and `&` nesting is flattened earlier, by the pipeline)
|
|
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
|
|
16
|
-
- **SSR support** - Deterministic class names and
|
|
16
|
+
- **SSR support** - Deterministic class names shared with `ServerStyleCollector`, and hydration of server-rendered classes
|
|
17
17
|
- **Multiple roots** - Works with Document and ShadowRoot
|
|
18
18
|
- **Non-stacking cleanups** - Prevents timeout accumulation for better performance
|
|
19
19
|
|
|
@@ -55,13 +55,15 @@ Injects CSS rules and returns a className with dispose function.
|
|
|
55
55
|
```typescript
|
|
56
56
|
import { inject } from '@tenphi/tasty';
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Rules flagged needsClassName get the generated class prepended (doubled)
|
|
59
59
|
const result = inject([{
|
|
60
|
-
selector: '
|
|
60
|
+
selector: '',
|
|
61
61
|
declarations: 'color: red; padding: 10px;',
|
|
62
|
+
needsClassName: true,
|
|
62
63
|
}]);
|
|
63
64
|
|
|
64
|
-
console.log(result.className); // 't
|
|
65
|
+
console.log(result.className); // 'th8n0uh' — `t` + base36 hash of the rules
|
|
66
|
+
// Injected: .th8n0uh.th8n0uh { color: red; padding: 10px; }
|
|
65
67
|
|
|
66
68
|
// Release the pin; the class becomes collectible once nothing renders it
|
|
67
69
|
result.dispose();
|
|
@@ -175,7 +177,7 @@ const result = testInjector.inject(rules);
|
|
|
175
177
|
Injects CSS keyframes with automatic deduplication.
|
|
176
178
|
|
|
177
179
|
```typescript
|
|
178
|
-
// Generated name (
|
|
180
|
+
// Generated name (tk0, tk1, … — namePrefix + 'k' + counter)
|
|
179
181
|
const fadeIn = keyframes({
|
|
180
182
|
from: { opacity: 0 },
|
|
181
183
|
to: { opacity: 1 },
|
|
@@ -253,6 +255,7 @@ Dispose, ref-counted cleanup and GC therefore behave identically in every mode.
|
|
|
253
255
|
- **Configuration is locked after styles are generated** - calling `configure()` after first render will emit a warning and be ignored
|
|
254
256
|
- `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
257
|
- `gc.grace`: How long a class is left alone after collection first notices nothing is carrying it, in milliseconds (default `10000`).
|
|
258
|
+
- Automatic sweeps are opt-in: they run only when `configure({ gc })` is set (`gc: {}` enables them with these defaults). Without it nothing is collected until you call `gc()` or `cleanup()`.
|
|
256
259
|
|
|
257
260
|
**What a sweep does.** Everything the injector holds falls into one of five bands, and only the last is ever deleted:
|
|
258
261
|
|
|
@@ -289,7 +292,7 @@ Beyond ordinary rules, the injector has a dedicated entry point per at-rule. All
|
|
|
289
292
|
|
|
290
293
|
Every one of these takes an optional `root` (a `Document` or `ShadowRoot`).
|
|
291
294
|
|
|
292
|
-
`func()` also
|
|
295
|
+
`counterStyle()` and `func()` also accept `weak: true`, which registers the rule **without** overriding an existing one of the same name. `configure()` uses it for global `@counter-style` and `@function` definitions so a component-local definition always wins over the global default, regardless of which is injected first.
|
|
293
296
|
|
|
294
297
|
```ts
|
|
295
298
|
import { counterStyle, fontFace, func, property } from '@tenphi/tasty';
|
|
@@ -306,22 +309,24 @@ func('$$negative', { args: ['$value'], result: '(-1 * $value)' });
|
|
|
306
309
|
|
|
307
310
|
### Style Result Format
|
|
308
311
|
|
|
309
|
-
The injector works with `StyleResult` objects from the
|
|
312
|
+
The injector works with `StyleResult` objects from the pipeline (`renderStyles`):
|
|
310
313
|
|
|
311
314
|
```typescript
|
|
312
315
|
interface StyleResult {
|
|
313
|
-
selector: string;
|
|
314
|
-
declarations: string;
|
|
315
|
-
atRules?: string[];
|
|
316
|
-
|
|
316
|
+
selector: string; // full selector, or a fragment when needsClassName
|
|
317
|
+
declarations: string; // CSS declarations
|
|
318
|
+
atRules?: string[]; // @media, @container, @supports, …
|
|
319
|
+
needsClassName?: boolean; // prepend the generated `.tX.tX` (OR'd fragments joined with '|||')
|
|
320
|
+
rootPrefix?: string; // root-state prefix placed before the class
|
|
321
|
+
startingStyle?: boolean; // wrap declarations in @starting-style
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
// Example StyleResult
|
|
320
325
|
const styleRule: StyleResult = {
|
|
321
|
-
selector: '
|
|
326
|
+
selector: ':where([data-hovered])',
|
|
322
327
|
declarations: 'padding: 8px 16px; background: blue; color: white;',
|
|
323
328
|
atRules: ['@media (min-width: 768px)'],
|
|
324
|
-
|
|
329
|
+
needsClassName: true,
|
|
325
330
|
};
|
|
326
331
|
```
|
|
327
332
|
|
|
@@ -339,28 +344,24 @@ API here calls it for you.
|
|
|
339
344
|
### Deduplication & Performance
|
|
340
345
|
|
|
341
346
|
```typescript
|
|
342
|
-
|
|
343
|
-
const button1 = inject([{
|
|
344
|
-
selector: '.t-btn1',
|
|
345
|
-
declarations: 'padding: 8px; color: red;'
|
|
346
|
-
}]);
|
|
347
|
+
const rule = { selector: '', declarations: 'padding: 8px; color: red;', needsClassName: true };
|
|
347
348
|
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
declarations: 'padding: 8px; color: red;' // Same declarations
|
|
351
|
-
}]);
|
|
349
|
+
const button1 = inject([rule], { cacheKey: 'button' });
|
|
350
|
+
const button2 = inject([rule], { cacheKey: 'button' });
|
|
352
351
|
|
|
353
|
-
//
|
|
352
|
+
// One class, one copy of the CSS
|
|
354
353
|
console.log(button1.className === button2.className); // true
|
|
355
354
|
```
|
|
356
355
|
|
|
356
|
+
Without `cacheKey` the class name still matches (it hashes the rules), but the rule is inserted again and the pin count restarts at 1.
|
|
357
|
+
|
|
357
358
|
### Pinning
|
|
358
359
|
|
|
359
360
|
```typescript
|
|
360
361
|
// Multiple callers using the same styles
|
|
361
|
-
const comp1 = inject([commonStyle]);
|
|
362
|
-
const comp2 = inject([commonStyle]);
|
|
363
|
-
const comp3 = inject([commonStyle]);
|
|
362
|
+
const comp1 = inject([commonStyle], { cacheKey: 'common' });
|
|
363
|
+
const comp2 = inject([commonStyle], { cacheKey: 'common' });
|
|
364
|
+
const comp3 = inject([commonStyle], { cacheKey: 'common' });
|
|
364
365
|
|
|
365
366
|
// Style is pinned while any caller holds a handle
|
|
366
367
|
comp1.dispose(); // pins: 3 → 2
|
|
@@ -439,15 +440,16 @@ const shadowAnimation = keyframes({
|
|
|
439
440
|
|
|
440
441
|
### Server-Side Rendering
|
|
441
442
|
|
|
443
|
+
`getCSSText()` reads the injector's stylesheets, so it needs a DOM; on a server it throws. Collect server CSS with `ServerStyleCollector` from `@tenphi/tasty/ssr` (`collector.getCSS()`) — see [SSR](./ssr.md).
|
|
444
|
+
|
|
442
445
|
```typescript
|
|
443
446
|
import { getCSSText, getCSSTextForNode } from '@tenphi/tasty';
|
|
447
|
+
import { render } from '@testing-library/react';
|
|
444
448
|
|
|
445
|
-
//
|
|
446
|
-
const cssText = getCSSText();
|
|
449
|
+
const cssText = getCSSText(); // everything the injector holds (browser / test DOM)
|
|
447
450
|
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
const componentCSS = getCSSTextForNode(container);
|
|
451
|
+
const { container } = render(<MyComponent />);
|
|
452
|
+
const componentCSS = getCSSTextForNode(container); // only the CSS this subtree uses
|
|
451
453
|
```
|
|
452
454
|
|
|
453
455
|
### Test Environment Detection
|
|
@@ -520,17 +522,14 @@ console.log({
|
|
|
520
522
|
### Debug Information
|
|
521
523
|
|
|
522
524
|
```typescript
|
|
523
|
-
|
|
524
|
-
const debugInfo = injector.instance.getDebugInfo();
|
|
525
|
+
import { tastyDebug } from '@tenphi/tasty';
|
|
525
526
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
unusedStyles: debugInfo.unusedStyles, // Styles marked for cleanup
|
|
529
|
-
totalSheets: debugInfo.totalSheets, // Number of stylesheets
|
|
530
|
-
totalRules: debugInfo.totalRules, // Total CSS rules
|
|
531
|
-
});
|
|
527
|
+
const { activeClasses, unusedClasses, hotClasses, totalRuleCount } =
|
|
528
|
+
tastyDebug.summary({ raw: true });
|
|
532
529
|
```
|
|
533
530
|
|
|
531
|
+
See [Debug Utilities](./debug.md).
|
|
532
|
+
|
|
534
533
|
### Cleanup History
|
|
535
534
|
|
|
536
535
|
```typescript
|
package/docs/methodology.md
CHANGED
|
@@ -119,16 +119,17 @@ Style props accept state maps, so responsive values work through the same API:
|
|
|
119
119
|
|
|
120
120
|
Tasty exports predefined style prop lists that group properties by role. Use them instead of hand-picking arrays:
|
|
121
121
|
|
|
122
|
-
| Preset | Properties
|
|
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, physical/logical margin, inset, scroll margin/padding, position
|
|
126
|
-
| `DIMENSION_STYLES` | width, height, blockSize, inlineSize, flexBasis, flexGrow, flexShrink, flex
|
|
127
|
-
| `COLOR_STYLES` | color, fill, fade, image
|
|
128
|
-
| `BLOCK_STYLES` | padding, blockPadding, inlinePadding, overflow, scrollbar, textAlign, border, blockBorder, inlineBorder, radius, shadow, outline
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
122
|
+
| Preset | Properties | Typical use |
|
|
123
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
|
124
|
+
| `FLOW_STYLES` | flow, gap, columnGap, rowGap, align, justify, place, 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, physical/logical margin, inset, scroll margin/padding, position | Positioned elements (`Button`, `Badge`) |
|
|
126
|
+
| `DIMENSION_STYLES` | width, height, blockSize, inlineSize, flexBasis, flexGrow, flexShrink, flex | Sized elements |
|
|
127
|
+
| `COLOR_STYLES` | color, fill, fade, image | Color-customizable elements |
|
|
128
|
+
| `BLOCK_STYLES` | padding, blockPadding, inlinePadding, overflow, scrollbar, textAlign, border, blockBorder, inlineBorder, radius, shadow, outline | Block-level containers |
|
|
129
|
+
| `TEXT_STYLES` | textTransform, fontWeight, fontStyle | Text elements |
|
|
130
|
+
| `CONTAINER_STYLES` | All of the above combined (+ BASE_STYLES) | Fully flexible containers |
|
|
131
|
+
| `OUTER_STYLES` | POSITION_STYLES + DIMENSION_STYLES + block outer (border, radius, shadow, outline) | Components whose outer shell is customizable |
|
|
132
|
+
| `INNER_STYLES` | BASE_STYLES + COLOR_STYLES + block inner (padding, overflow, scrollbar) + FLOW_STYLES | Components whose inner layout is customizable |
|
|
132
133
|
|
|
133
134
|
```tsx
|
|
134
135
|
import { tasty, FLOW_STYLES, POSITION_STYLES } from '@tenphi/tasty';
|
|
@@ -156,7 +157,10 @@ Match the preset to the component's role:
|
|
|
156
157
|
|
|
157
158
|
- **Layout containers** (`Space`, `Box`, `Grid`) — `FLOW_STYLES`, optionally with `DIMENSION_STYLES`
|
|
158
159
|
- **Positioned elements** (`Button`, `Badge`) — `POSITION_STYLES`
|
|
159
|
-
- **Text elements** —
|
|
160
|
+
- **Text elements** — `['color', ...TEXT_STYLES]`
|
|
161
|
+
|
|
162
|
+
`BASE_STYLES` (`display`, `font`, `preset`, `hide`, `whiteSpace`, `opacity`, `transition`) are accepted as props on every component without being listed; extend that set app-wide with [`configure({ baseStyleProps })`](configuration.md#base-style-props).
|
|
163
|
+
|
|
160
164
|
- **Compound components** — typically none; styling happens via sub-elements and extension
|
|
161
165
|
|
|
162
166
|
### The governance trade-off
|
|
@@ -220,6 +224,26 @@ modProps: ['isLoading', 'isSelected'] as const,
|
|
|
220
224
|
|
|
221
225
|
For the full API reference, see [React API — Mod Props](react-api.md#mod-props).
|
|
222
226
|
|
|
227
|
+
### variants vs modProps
|
|
228
|
+
|
|
229
|
+
Use `variants` for looks that replace each other (primary, danger, ghost) and `modProps` for states that combine (`isLoading`, `size`):
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const Button = tasty({
|
|
233
|
+
as: 'button',
|
|
234
|
+
modProps: { isLoading: Boolean, size: ['sm', 'md', 'lg'] as const },
|
|
235
|
+
styles: { padding: { '': '1.5x 3x', 'size=sm': '1x 2x' }, radius: '1r' },
|
|
236
|
+
variants: {
|
|
237
|
+
default: { fill: '#primary', color: '#on-primary' },
|
|
238
|
+
danger: { fill: '#danger', color: '#on-danger' },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
<Button variant="danger" size="sm" isLoading />;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`variant` is typed from the variant keys, the `default` variant applies when no `variant` prop is passed, and only variants that render emit CSS. See [React API — Variants](react-api.md#variants).
|
|
246
|
+
|
|
223
247
|
---
|
|
224
248
|
|
|
225
249
|
## tokens prop for dynamic values
|
|
@@ -335,10 +359,9 @@ const LargeCard = tasty(Card, {
|
|
|
335
359
|
|
|
336
360
|
Why? Styled wrappers are:
|
|
337
361
|
|
|
338
|
-
- **
|
|
339
|
-
- **Stable** — the style object is defined once, not recreated on every render
|
|
362
|
+
- **Stable** — the style object is defined once at module level, not recreated on every render
|
|
340
363
|
- **Composable** — another engineer can extend `LargeCard` further
|
|
341
|
-
- **Inspectable** —
|
|
364
|
+
- **Inspectable** — give it a `qa` (`tasty(Card, { qa: 'LargeCard', styles })`) and it appears as `TastyWrappedComponent(LargeCard)` in React DevTools and as `data-qa="LargeCard"` in the DOM
|
|
342
365
|
- **Lint-friendly** — the ESLint plugin's `no-styles-prop` rule flags direct usage
|
|
343
366
|
|
|
344
367
|
The `styles` prop exists as an escape hatch — for prototyping, one-off overrides during development, or cases where wrapping is impractical. It should not be the default way product engineers customize components.
|
|
@@ -453,11 +476,13 @@ const DangerCard = tasty(Card, {
|
|
|
453
476
|
|
|
454
477
|
This is preferred over `<Card styles={{ border: '1bw solid #danger' }}>` because:
|
|
455
478
|
|
|
456
|
-
1.
|
|
479
|
+
1. The styles live in one module-level definition instead of being rebuilt inside render
|
|
457
480
|
2. `DangerCard` can be extended further by others
|
|
458
|
-
3. It
|
|
481
|
+
3. It is searchable in code, and named in DevTools once you give it a `qa`
|
|
459
482
|
4. The ESLint `no-styles-prop` rule encourages this pattern
|
|
460
483
|
|
|
484
|
+
A wrapper does not re-expose the base's sub-element components: `DangerCard.Title` is `undefined` and not part of its type. Keep using the base's parts inside it — `<DangerCard><Card.Title>…</Card.Title></DangerCard>`. The wrapper's `Title` styles still apply, because they target `[data-element="Title"]`.
|
|
485
|
+
|
|
461
486
|
---
|
|
462
487
|
|
|
463
488
|
## How configuration simplifies components
|
|
@@ -554,7 +579,7 @@ fill: 'oklch(55% 0.25 265)',
|
|
|
554
579
|
fill: '#primary',
|
|
555
580
|
```
|
|
556
581
|
|
|
557
|
-
Tokens ensure consistency across components and make theme changes a one-line update.
|
|
582
|
+
Tokens ensure consistency across components and make theme changes a one-line update. The recommended lint config flags this with `tasty/no-raw-color-values` and `tasty/consistent-token-usage`.
|
|
558
583
|
|
|
559
584
|
### Using CSS property names when Tasty alternatives exist
|
|
560
585
|
|
|
@@ -570,7 +595,7 @@ radius: '1r',
|
|
|
570
595
|
flow: 'column',
|
|
571
596
|
```
|
|
572
597
|
|
|
573
|
-
Tasty's enhanced properties provide concise syntax, better composability, and simpler overrides. See [recommended props](styles.md#recommended-props) for the full mapping.
|
|
598
|
+
Tasty's enhanced properties provide concise syntax, better composability, and simpler overrides. See [recommended props](styles.md#recommended-props) for the full mapping. `tasty/prefer-shorthand-property` flags the native spellings.
|
|
574
599
|
|
|
575
600
|
### Changing styles prop at runtime
|
|
576
601
|
|
|
@@ -593,7 +618,7 @@ const Card = tasty({
|
|
|
593
618
|
});
|
|
594
619
|
```
|
|
595
620
|
|
|
596
|
-
Modifiers are compiled into exclusive selectors once. Changing `styles` at runtime forces Tasty to regenerate and re-inject CSS.
|
|
621
|
+
Modifiers are compiled into exclusive selectors once. Changing `styles` at runtime forces Tasty to regenerate and re-inject CSS. `tasty/no-styles-prop` and `tasty/no-runtime-styles-mutation` flag these patterns.
|
|
597
622
|
|
|
598
623
|
### Overusing style prop
|
|
599
624
|
|
|
@@ -607,7 +632,7 @@ const DangerButton = tasty(Button, {
|
|
|
607
632
|
});
|
|
608
633
|
```
|
|
609
634
|
|
|
610
|
-
The `style` prop bypasses tokens, units, and state maps. It should only be used for third-party library integration.
|
|
635
|
+
The `style` prop bypasses tokens, units, and state maps. It should only be used for third-party library integration. `tasty/no-style-prop` flags it on Tasty components.
|
|
611
636
|
|
|
612
637
|
### Skipping elements for compound components
|
|
613
638
|
|
package/docs/migration-v3.md
CHANGED
|
@@ -188,8 +188,8 @@ A key whose prefix doesn't match its value type is ignored with a development wa
|
|
|
188
188
|
## 7. Moved subpath
|
|
189
189
|
|
|
190
190
|
```diff
|
|
191
|
-
- import {
|
|
192
|
-
+ import {
|
|
191
|
+
- import { withTastyZero } from '@tenphi/tasty/next';
|
|
192
|
+
+ import { withTastyZero } from '@tenphi/tasty/zero/next';
|
|
193
193
|
```
|
|
194
194
|
|
|
195
195
|
The build-time extraction Next.js wrapper already lived under the `zero` namespace internally; the top-level `/next` path collided confusingly with `@tenphi/tasty/ssr/next`, which is a different integration.
|
package/docs/pipeline.md
CHANGED
|
@@ -76,7 +76,7 @@ Output: CSSRule[]
|
|
|
76
76
|
|
|
77
77
|
**Simplification** (`simplifyCondition` in `simplify.ts`) is not a separate numbered stage. It runs inside OR expansion, exclusive building, `expandExclusiveOrs` branch cleanup, combination ANDs, merge-by-value ORs, and materialization. Every call is cached by condition unique-id, so the repetition is cheap.
|
|
78
78
|
|
|
79
|
-
**Post-pass:** After `processStyles` collects rules from every handler, `runPipeline` (`index.ts`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle`, then **stable-sorts rules by their cascade `order` hint (ascending source priority)** so lower-priority rules come first and higher-priority rules win, and finally emits every `@starting-style` rule **after** all normal rules.
|
|
79
|
+
**Post-pass:** After `processStyles` collects rules from every handler, `runPipeline` (`index.ts`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle`, then **stable-sorts rules by their cascade `order` hint (ascending source priority)** so lower-priority rules come first and higher-priority rules win, and finally emits every `@starting-style` rule **after** all normal rules. Each Stage 4 snapshot takes the highest source priority among its entries as its `order` hint; when Stage 6 merges same-output rules, the merged rule keeps the lowest `order` of its group, so a rule carrying the `_` floor's value keeps the floor's early position (threaded `ComputedRule → CSSRule`). This ordering is cascade-critical because Stage 7 equalizes specificity with `:where()`: once every state selector carries zero specificity, source order alone decides which of two overlapping rules wins — most importantly the `_` fallback floor (low order, emitted first) versus the states layered over it, and `@starting-style` versus its normal counterpart. Mutually-exclusive rules are unaffected by ordering (they never both match), so reordering them is safe.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -197,7 +197,9 @@ Merging two same-value entries with conditions `C_h` (higher priority) and `C_l`
|
|
|
197
197
|
|
|
198
198
|
The merge is safe iff for every entry `e_m` strictly between them in priority with a different value,
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
```
|
|
201
|
+
simplify(C_m & C_l & !C_h) = FALSE
|
|
202
|
+
```
|
|
201
203
|
|
|
202
204
|
i.e. there is no scenario where the intermediate state could have matched, the lower same-value entry would also have matched, and the higher one would not. This is the only way the merge could leak through and shadow the intermediate state.
|
|
203
205
|
|
|
@@ -362,7 +364,7 @@ This is the companion to **Stage 2a** (user-OR expansion). The split exists beca
|
|
|
362
364
|
|
|
363
365
|
| Stage | Runs on | Sees ORs from | Sorts branches? |
|
|
364
366
|
|---|---|---|---|
|
|
365
|
-
| 2a `expandOrConditions` | `ParsedStyleEntry.condition` | User-authored
|
|
367
|
+
| 2a `expandOrConditions` | `ParsedStyleEntry.condition` | User-authored `\|` in state keys | No — user order is stable |
|
|
366
368
|
| 3 `expandExclusiveOrs` | `ExclusiveStyleEntry.exclusiveCondition` | De Morgan negation inside `buildExclusiveConditions` | Yes — at-rule branches first |
|
|
367
369
|
|
|
368
370
|
### How It Works
|
|
@@ -522,7 +524,7 @@ Converts condition trees into actual CSS selectors and at-rules.
|
|
|
522
524
|
- the flat root modifiers + pseudos and the root element's `:is()`/`:not()` groups are combined into one `:where(...)` (`buildSelectorFromVariant`);
|
|
523
525
|
- the sub-element (`@own`) groups are wrapped in a **separate** `:where(...)` so the sub-element's structural `[data-element="X"]` specificity is preserved while its states are zeroed;
|
|
524
526
|
- `@parent` ancestors (`parentGroupsToCSS`) become `:where(... *)` / `:where(:not(... *))`;
|
|
525
|
-
- the `@root` context prefix (`
|
|
527
|
+
- the `@root` context prefix (`rootStateToPrefix`) becomes `:where(:root...)` (zeroing the `:root` pseudo too). When a direct selector can only match the root itself (`:root`, `html`, `:where(:root)`), the state segment joins it instead — `:root:where([data-schema="dark"])` — since an ancestor prefix would ask for a `:root` inside the root.
|
|
526
528
|
|
|
527
529
|
The only specificity anchors that remain are the doubled component class (`.tXX.tXX`, added by the injector) and sub-element `[data-element]` attributes. Because Stage 2b already makes ordinary rules mutually exclusive, zeroing specificity never changes *which* rule matches — it only makes additive layering (`_` fallback floor, `@starting-style`) resolve deterministically by source order (see the post-pass). Canonical sorting/dedup/subsumption still run first; `:where()` is only the final wrapper, and both output paths (injector and direct-selector / SSR) consume the same wrapped fragments.
|
|
528
530
|
|
|
@@ -545,12 +547,13 @@ interface CSSRule {
|
|
|
545
547
|
declarations: string; // CSS declarations (e.g. 'color: red;')
|
|
546
548
|
atRules?: string[]; // Wrapping at-rules
|
|
547
549
|
rootPrefix?: string; // Root state prefix
|
|
550
|
+
rootState?: string; // Root-state segment without its :root anchor; lets a root selector take states directly
|
|
548
551
|
startingStyle?: boolean; // Wrap declarations in @starting-style
|
|
549
552
|
order?: number; // Internal cascade order hint (ascending source priority); stripped before injection
|
|
550
553
|
}
|
|
551
554
|
```
|
|
552
555
|
|
|
553
|
-
When `renderStyles` runs **without** a selector, returned rules include `needsClassName: true` (compatibility field for the injector); that flag is not part of `CSSRule`
|
|
556
|
+
When `renderStyles` runs **without** a selector, returned rules include `needsClassName: true` (compatibility field for the injector); that flag is not part of `CSSRule` (defined in `materialize-types.ts`).
|
|
554
557
|
|
|
555
558
|
---
|
|
556
559
|
|
|
@@ -618,7 +621,7 @@ Applies boolean algebra rules to reduce condition complexity and detect impossib
|
|
|
618
621
|
- `A & (A | B) = A`
|
|
619
622
|
- `A | (A & B) = A`
|
|
620
623
|
|
|
621
|
-
7. **Range intersection**: For **media and container** dimension queries, impossible ranges simplify to `FALSE` (e.g. `@media(w > 400px) & @media(w < 300px)`). Ranges with compatible bounds are also merged in place (`w >= 400 & w <= 800` → a single bounded range).
|
|
624
|
+
7. **Range intersection**: For **media and container** dimension queries, impossible ranges simplify to `FALSE` (e.g. `@media(w > 400px) & @media(w < 300px)`). Ranges with compatible bounds are also merged in place (`w >= 400 & w <= 800` → a single bounded range). Only bounds in the same unit are compared: `600px` and `40em` are never merged or found contradictory, because a query `em` follows the user's default font size. A bound with no numeric value (`calc(…)`) and a range whose two bounds use different units are left as they are.
|
|
622
625
|
|
|
623
626
|
8. **Container style queries**: Conflicting or redundant `@container` style conditions on the same property can be reduced (see `simplify.ts` around the container-style conflict pass).
|
|
624
627
|
|
|
@@ -642,7 +645,7 @@ LRU and small auxiliary caches:
|
|
|
642
645
|
| Cache | Size | Key | Purpose |
|
|
643
646
|
|-------|------|-----|---------|
|
|
644
647
|
| `pipelineCache` | 5000 | `pipelineCacheKey \|\| stringifyStyles(styles)` | Skip full pipeline for identical styles |
|
|
645
|
-
| `parseCache` | 5000 | `trimmedStateKey + '
|
|
648
|
+
| `parseCache` | 5000 | `trimmedStateKey + '\0' + (isSubElement ? '1' : '0') + '\0' + (localPredefinedStates non-empty ? JSON.stringify(localPredefinedStates) : '')` | Skip re-parsing identical state keys in context |
|
|
646
649
|
| `simplifyCache` | 5000 | `getConditionUniqueId(node)` | Skip re-simplifying identical conditions |
|
|
647
650
|
| `conditionCache` | 3000 | `getConditionUniqueId(node)` in `conditionToCSS` | Skip re-materializing identical conditions |
|
|
648
651
|
| `variantKeyCache` | — | `WeakMap<SelectorVariant, string>` | Stable string keys for variants during materialization |
|
|
@@ -702,24 +705,24 @@ Each snapshot yields distinct declarations; no merge.
|
|
|
702
705
|
|
|
703
706
|
### Stage 7: Materialize CSS
|
|
704
707
|
|
|
705
|
-
Using `renderStyles(styles, '.t1')
|
|
708
|
+
Using `renderStyles(styles, '.t1', { doubleSelector: true })`. The injector doubles the class the same way (`.t1.t1`) so Tasty owns specificity without `!important`, and every state is wrapped in `:where()` so it adds none. Rules come out in ascending priority:
|
|
706
709
|
|
|
707
710
|
```css
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
+
@media (not (prefers-color-scheme: dark)) {
|
|
712
|
+
.t1.t1:where(:not([data-hovered])) {
|
|
713
|
+
color: var(--white-color);
|
|
714
|
+
--current-color: var(--white-color);
|
|
715
|
+
}
|
|
711
716
|
}
|
|
712
717
|
@media (prefers-color-scheme: dark) {
|
|
713
|
-
.t1:not([data-hovered]) {
|
|
718
|
+
.t1.t1:where(:not([data-hovered])) {
|
|
714
719
|
color: var(--dark-color);
|
|
715
720
|
--current-color: var(--dark-color);
|
|
716
721
|
}
|
|
717
722
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
--current-color: var(--white-color);
|
|
722
|
-
}
|
|
723
|
+
.t1.t1:where([data-hovered]) {
|
|
724
|
+
color: var(--highlight-color);
|
|
725
|
+
--current-color: var(--highlight-color);
|
|
723
726
|
}
|
|
724
727
|
```
|
|
725
728
|
|
package/docs/plugins.md
CHANGED
|
@@ -37,7 +37,7 @@ configure({ plugins: [spacingPlugin()] });
|
|
|
37
37
|
|
|
38
38
|
**Configuration is locked after the first render.** `configure()` warns and does nothing once any style has been generated, so it must run before your first component renders — at module scope in your entry file, or in a design-system module every component imports. `resetConfig()` reopens it, but it exists for tests.
|
|
39
39
|
|
|
40
|
-
**Repeated `configure()` calls merge
|
|
40
|
+
**Repeated `configure()` calls merge.** Each call adds to what earlier calls registered, key by key, in every registry (`states`, `units`, `functions`, `handlers`, `propHandlers`, `baseStyleProps`, `tokens`, `replaceTokens`, `recipes`, `keyframes`, `properties`, `fontFaces`, `counterStyles`); `globalStyles` merges per selector. A later value for the same key wins.
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -108,6 +108,7 @@ configure({
|
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
110
|
import { configure, mergeStyles } from '@tenphi/tasty';
|
|
111
|
+
import type { Styles } from '@tenphi/tasty';
|
|
111
112
|
|
|
112
113
|
configure({
|
|
113
114
|
propHandlers: {
|
|
@@ -115,7 +116,11 @@ configure({
|
|
|
115
116
|
const { glaze, ...rest } = props;
|
|
116
117
|
if (!glaze) return rest;
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
// glazeStyles: see the worked example below
|
|
120
|
+
return {
|
|
121
|
+
...rest,
|
|
122
|
+
styles: mergeStyles(glazeStyles(glaze), rest.styles as Styles),
|
|
123
|
+
};
|
|
119
124
|
},
|
|
120
125
|
},
|
|
121
126
|
});
|
|
@@ -159,7 +164,7 @@ configure({ baseStyleProps: ['radius', 'shadow'] });
|
|
|
159
164
|
<Card radius="1r" shadow />
|
|
160
165
|
```
|
|
161
166
|
|
|
162
|
-
Names must
|
|
167
|
+
Names must start with a lowercase letter and contain only letters and digits, and must not collide with a prop `tasty()` consumes itself (`as`, `styles`, `variant`, `mods`, `tokens`, …); invalid entries are dropped with a development warning. Names are not checked against the style registry, so a misspelled name is still harvested as a style prop.
|
|
163
168
|
|
|
164
169
|
`configure()` may run _after_ your components are defined — each factory resolves its prop list lazily and refreshes when the registry changes.
|
|
165
170
|
|
|
@@ -169,39 +174,9 @@ Names must be real style properties, must start with a lowercase letter, and mus
|
|
|
169
174
|
|
|
170
175
|
## Typing your extension
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
// tasty.d.ts
|
|
176
|
-
import type { StylePropValue } from '@tenphi/tasty';
|
|
177
|
-
|
|
178
|
-
declare module '@tenphi/tasty' {
|
|
179
|
-
// configure({ handlers }) — a new style property
|
|
180
|
-
interface StylesInterface {
|
|
181
|
-
stripe?: StylePropValue<string>;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// configure({ propHandlers }) — a new component prop
|
|
185
|
-
interface TastyCustomProps {
|
|
186
|
-
glaze: 'soft' | 'strong' | { tone: string; intensity?: number };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// configure({ baseStyleProps }) — promoted style names, typed like the style
|
|
190
|
-
interface TastyBaseStylePropNames {
|
|
191
|
-
radius: true;
|
|
192
|
-
shadow: true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// configure({ tokens }) / recipes / presets — autocomplete for names
|
|
196
|
-
interface TastyNamedColors {
|
|
197
|
-
'glaze-bg': true;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
177
|
+
Every extension point has a matching augmentation interface: `StylesInterface` for `handlers`, `TastyCustomProps` for `propHandlers`, `TastyBaseStylePropNames` for `baseStyleProps`, and `TastyNamedColors`, `TastyPresetNames`, and `TastyThemeNames` for the names your `tokens` and `presets` define. [Configuration → TypeScript](configuration.md#typescript) covers all six and the rules that go with them.
|
|
201
178
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
**Also update `tasty.config.ts`.** The ESLint plugin and VS Code extension validate style and token names against it, and neither is derived from the runtime registries:
|
|
179
|
+
**Also update `tasty.config.ts`.** The ESLint plugin and VS Code extension validate style and token names against it, and neither reads the runtime registries or your augmentations:
|
|
205
180
|
|
|
206
181
|
```ts
|
|
207
182
|
// tasty.config.ts
|
|
@@ -299,6 +274,8 @@ configure({ plugins: [glazePlugin()] });
|
|
|
299
274
|
|
|
300
275
|
```ts
|
|
301
276
|
// tasty.d.ts
|
|
277
|
+
import '@tenphi/tasty';
|
|
278
|
+
|
|
302
279
|
declare module '@tenphi/tasty' {
|
|
303
280
|
interface TastyCustomProps {
|
|
304
281
|
glaze: import('./glaze-plugin').GlazeConfig | string;
|
|
@@ -327,17 +304,20 @@ The object value is unambiguous here precisely because `glaze` is a **prop**. A
|
|
|
327
304
|
## Testing a plugin
|
|
328
305
|
|
|
329
306
|
```ts
|
|
330
|
-
import {
|
|
307
|
+
import { resetConfig, renderStyles } from '@tenphi/tasty';
|
|
308
|
+
import type { PropHandler, Styles } from '@tenphi/tasty';
|
|
331
309
|
|
|
332
310
|
describe('glaze plugin', () => {
|
|
333
311
|
beforeEach(() => resetConfig());
|
|
334
312
|
afterEach(() => resetConfig());
|
|
335
313
|
|
|
336
|
-
it('
|
|
337
|
-
|
|
314
|
+
it('turns the glaze prop into styles', () => {
|
|
315
|
+
const glaze = glazePlugin().propHandlers!.glaze as PropHandler;
|
|
316
|
+
const out = glaze({ glaze: 'purple' })!;
|
|
338
317
|
|
|
339
|
-
|
|
318
|
+
expect(out).not.toHaveProperty('glaze');
|
|
340
319
|
|
|
320
|
+
const rules = renderStyles(out.styles as Styles, '.test');
|
|
341
321
|
expect(rules[0].declarations).toContain('--glaze-bg-color');
|
|
342
322
|
});
|
|
343
323
|
});
|
|
@@ -345,7 +325,7 @@ describe('glaze plugin', () => {
|
|
|
345
325
|
|
|
346
326
|
Two things to know:
|
|
347
327
|
|
|
348
|
-
- **Create components inside each test.** `resetConfig()` does not clear
|
|
328
|
+
- **Create components inside each test.** `resetConfig()` does not clear a component's own class-name cache, which `tasty()` keeps in DOM environments, so a module-scope component can keep returning a class whose CSS the reset removed.
|
|
349
329
|
- **Dev-mode warnings may not fire.** `isDevEnv()` reports `false` for `NODE_ENV=test`. Warnings that check it lazily can be enabled with `vi.stubEnv('NODE_ENV', 'development')`; some older ones capture it at module load and cannot be triggered from a test at all.
|
|
350
330
|
|
|
351
331
|
---
|