@tenphi/tasty 2.6.5 → 2.8.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/dist/{collector-c00_hT9R.js → collector-BEF4F_PE.js} +3 -3
- package/dist/{collector-c00_hT9R.js.map → collector-BEF4F_PE.js.map} +1 -1
- package/dist/{collector-osfWTeRd.d.ts → collector-CpU85p2G.d.ts} +2 -2
- package/dist/{config-BoZDUHW5.d.ts → config-3h7BtX4l.d.ts} +2 -2
- package/dist/{config-IzenlK2R.js → config-BDnvK42H.js} +308 -74
- package/dist/config-BDnvK42H.js.map +1 -0
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +6 -6
- package/dist/{core-J9U8fXzr.js → core-BiFQGi4v.js} +5 -5
- package/dist/{core-J9U8fXzr.js.map → core-BiFQGi4v.js.map} +1 -1
- package/dist/{css-writer-CCyaR6ZM.js → css-writer-4IMPW4i1.js} +3 -3
- package/dist/{css-writer-CCyaR6ZM.js.map → css-writer-4IMPW4i1.js.map} +1 -1
- package/dist/{format-rules-CCb7qNPt.js → format-rules-DrpEA0CZ.js} +2 -2
- package/dist/{format-rules-CCb7qNPt.js.map → format-rules-DrpEA0CZ.js.map} +1 -1
- package/dist/{hydrate-DEsdmcdy.js → hydrate-DItCQmmZ.js} +2 -2
- package/dist/{hydrate-DEsdmcdy.js.map → hydrate-DItCQmmZ.js.map} +1 -1
- package/dist/{index-tcHuMPFt.d.ts → index-BsJz5xBF.d.ts} +2 -2
- package/dist/{index-D-OA_O6i.d.ts → index-C_SRmAWj.d.ts} +193 -163
- package/dist/index.d.ts +5 -5
- package/dist/index.js +7 -7
- package/dist/{keyframes-DxAp6xwG.js → keyframes-DYGxQPjN.js} +2 -2
- package/dist/{keyframes-DxAp6xwG.js.map → keyframes-DYGxQPjN.js.map} +1 -1
- package/dist/{merge-styles-BMWcH6MF.d.ts → merge-styles-BzOwGOGC.d.ts} +2 -2
- package/dist/{merge-styles-Ugifi570.js → merge-styles-oU4CfZY7.js} +2 -2
- package/dist/{merge-styles-Ugifi570.js.map → merge-styles-oU4CfZY7.js.map} +1 -1
- package/dist/{resolve-recipes-DEmQhiop.js → resolve-recipes-PwM-R6Jc.js} +3 -3
- package/dist/{resolve-recipes-DEmQhiop.js.map → resolve-recipes-PwM-R6Jc.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro.js +3 -3
- package/dist/ssr/index.d.ts +1 -1
- package/dist/ssr/index.js +3 -3
- 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/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/docs/dsl.md +110 -1
- package/docs/pipeline.md +40 -2
- package/package.json +5 -5
- package/dist/config-IzenlK2R.js.map +0 -1
package/docs/dsl.md
CHANGED
|
@@ -41,6 +41,20 @@ fill: {
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
#### Default State Ordering
|
|
45
|
+
|
|
46
|
+
Key order sets priority — later keys win and turn off earlier ones via negation. The bare default (`''`) is the lowest-priority state, so it **must be the first key**. If it appears after other states, Tasty moves it to the front and emits a dev warning (`MISPLACED_DEFAULT_STATE`); otherwise it would override every state above it.
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
// Correct — default first
|
|
50
|
+
color: { '': '#text', hovered: '#accent' }
|
|
51
|
+
|
|
52
|
+
// Auto-corrected with a warning — '' moved to the front
|
|
53
|
+
color: { hovered: '#accent', '': '#text' }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The bare `''` default still **receives negation** (it is turned off when a higher-priority state matches). For a value that must always apply as a guaranteed floor — even where a query is *unknown* — use the [`_` fallback floor](#_--fallback-floor) instead. The two can coexist: `''` is the negated default, `_` is the always-on floor. If a map contains only `_` and `''` (no other states), the `''` default is redundant — Tasty keeps the `_` value and drops `''` with a `REDUNDANT_DEFAULT_STATE` warning.
|
|
57
|
+
|
|
44
58
|
### Sub-element
|
|
45
59
|
|
|
46
60
|
Element styled using a capitalized key. Identified by `data-element` attribute:
|
|
@@ -288,11 +302,21 @@ const SimpleButton = tasty(Button, {
|
|
|
288
302
|
| `@parent` | Parent/ancestor element states | `@parent(hovered)` |
|
|
289
303
|
| `@own` | Sub-element's own state | `@own(hovered)` |
|
|
290
304
|
| `@starting` | Entry animation | `@starting` |
|
|
305
|
+
| `_` | Fallback floor (always-on, never negated) | `_` |
|
|
291
306
|
| `:is()` | CSS `:is()` structural pseudo-class | `:is(fieldset > label)` |
|
|
292
307
|
| `:has()` | CSS `:has()` relational pseudo-class | `:has(> Icon)` |
|
|
293
308
|
| `:not()` | CSS `:not()` negation (prefer `!:is()`) | `:not(:first-child)` |
|
|
294
309
|
| `:where()` | CSS `:where()` (zero specificity) | `:where(Section)` |
|
|
295
310
|
|
|
311
|
+
> **Specificity.** All state selectors Tasty generates (modifiers, pseudo-classes,
|
|
312
|
+
> `:is()`/`:not()` groups, and `@root` / `@parent` context) are wrapped in
|
|
313
|
+
> `:where(...)` so they carry **zero specificity**. The only specificity anchors
|
|
314
|
+
> are the doubled component class (`.t0.t0`) and sub-element `[data-element]`
|
|
315
|
+
> attributes. This means overlapping rules (e.g. a `_` fallback floor and the
|
|
316
|
+
> states layered over it) resolve purely by **source order** — Tasty emits
|
|
317
|
+
> lower-priority states first and higher-priority states last so the cascade
|
|
318
|
+
> produces the intended winner.
|
|
319
|
+
|
|
296
320
|
### `@media(...)` — Media Queries
|
|
297
321
|
|
|
298
322
|
Media queries support dimension shorthands and custom unit expansion:
|
|
@@ -374,6 +398,91 @@ display: {
|
|
|
374
398
|
}
|
|
375
399
|
```
|
|
376
400
|
|
|
401
|
+
### `_` — Fallback Floor
|
|
402
|
+
|
|
403
|
+
By default Tasty makes states **mutually exclusive**: a higher-priority state
|
|
404
|
+
turns the lower-priority ones off via negation, so exactly one branch applies.
|
|
405
|
+
This relies on `A | !A` always being true. CSS feature/container queries break
|
|
406
|
+
that assumption: `@supports(...)` and `@(...)` queries can be **unknown** (not
|
|
407
|
+
just true/false), and `not(unknown)` is also unknown — so a negated default
|
|
408
|
+
branch silently never applies. The classic case is `scroll-state`: a browser can
|
|
409
|
+
support `container-type: scroll-state` while a specific `scroll-state(...)` query
|
|
410
|
+
is unknown, leaving *no* branch active.
|
|
411
|
+
|
|
412
|
+
The `_` fallback floor solves this. Use `_` as a **standalone key** and its value
|
|
413
|
+
**always applies** as a guaranteed floor: it never receives negation, and
|
|
414
|
+
higher-priority states simply layer over it via the cascade.
|
|
415
|
+
|
|
416
|
+
```jsx
|
|
417
|
+
inset: {
|
|
418
|
+
_: '0 top',
|
|
419
|
+
'@supports(container-type: scroll-state) & @(scroll-state(scrolled: block-end))':
|
|
420
|
+
'-80x top',
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```css
|
|
425
|
+
.t0.t0 { inset: 0 ...; }
|
|
426
|
+
@container scroll-state(scrolled: block-end) {
|
|
427
|
+
@supports (container-type: scroll-state) {
|
|
428
|
+
.t0.t0 { inset: -80px ...; }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The floor is emitted as a bare rule (no negated `@supports` / container branches),
|
|
434
|
+
so it always applies. When the override matches it wins because it is emitted
|
|
435
|
+
later and all state selectors share the same specificity (see the note on
|
|
436
|
+
`:where()` below).
|
|
437
|
+
|
|
438
|
+
Notes:
|
|
439
|
+
|
|
440
|
+
- `_` is a **standalone key only** — it defines a single map-wide floor and
|
|
441
|
+
cannot be combined with state logic. Keys like `_ & hovered` or `_ | focused`
|
|
442
|
+
are ignored with an `INVALID_FALLBACK_KEY` dev warning.
|
|
443
|
+
- `_` can coexist with the bare `''` default when other states exist: `''` is the
|
|
444
|
+
negated default, `_` is the always-on floor. With only `_` and `''` (no other
|
|
445
|
+
states) the `''` default is redundant — Tasty keeps the `_` value and drops
|
|
446
|
+
`''` with a `REDUNDANT_DEFAULT_STATE` warning.
|
|
447
|
+
- `_` is position-independent: it is always placed at the lowest priority
|
|
448
|
+
regardless of where it appears in the map.
|
|
449
|
+
|
|
450
|
+
#### `_` vs the `''` default
|
|
451
|
+
|
|
452
|
+
In simple maps where every other state is a plain modifier (always cleanly
|
|
453
|
+
on/off), `_` and `''` produce the **same visible result** — the base value shows
|
|
454
|
+
whenever no other state wins. They diverge in three situations, and the root
|
|
455
|
+
cause is always the same: **`''` is mutually exclusive (turned off by negation),
|
|
456
|
+
while `_` is always on (layered underneath).**
|
|
457
|
+
|
|
458
|
+
- **Unknown / invalid branches.** If a higher-priority branch sits behind a query
|
|
459
|
+
that can be *unknown* (`@supports`, container, `scroll-state`) or uses a
|
|
460
|
+
selector the browser drops, the negated `''` default disappears along with it
|
|
461
|
+
(`not(unknown) = unknown`), leaving the property with no rule. The `_` floor
|
|
462
|
+
survives because it is an unconditional bare rule. This is the case `_` exists
|
|
463
|
+
for.
|
|
464
|
+
|
|
465
|
+
- **Empty / "unset" states.** A state can intentionally produce *no* output (an
|
|
466
|
+
empty value) to leave a property unset while that state is active. With `''`
|
|
467
|
+
this works — the default is negated away and nothing replaces it, so the
|
|
468
|
+
property unsets. With `_` the floor can never be turned off, so its value keeps
|
|
469
|
+
applying and the "unset on this state" intent is lost.
|
|
470
|
+
|
|
471
|
+
```jsx
|
|
472
|
+
// '' : color unsets while loading. '_' : color stays #text while loading.
|
|
473
|
+
color: { '': '#text', loading: '' }
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
- **States with different output shapes.** Because `_` layers additively, when
|
|
477
|
+
different states emit *different sets* of declarations, the floor's
|
|
478
|
+
declarations bleed through wherever the winning state does not override the
|
|
479
|
+
same property — which can produce inconsistent combinations. A `''` default is
|
|
480
|
+
swapped out cleanly by its mutually-exclusive condition.
|
|
481
|
+
|
|
482
|
+
Rule of thumb: **use `''` for the normal default** (clean mutual exclusivity,
|
|
483
|
+
supports unsetting), and reach for `_` only when a value must survive an
|
|
484
|
+
*unknown* higher-priority branch.
|
|
485
|
+
|
|
377
486
|
### `@root(...)` — Root Element States
|
|
378
487
|
|
|
379
488
|
Root states generate selectors on the `:root` element. They are useful for theme modes, feature flags, and other page-level conditions:
|
|
@@ -565,7 +674,7 @@ CSS cannot transition or animate custom properties unless the browser knows thei
|
|
|
565
674
|
const AnimatedGradient = tasty({
|
|
566
675
|
styles: {
|
|
567
676
|
'$gradient-angle': '0deg',
|
|
568
|
-
'#theme': '
|
|
677
|
+
'#theme': 'okhst(280 80% 50%)',
|
|
569
678
|
background: 'linear-gradient($gradient-angle, #theme, transparent)',
|
|
570
679
|
transition: '$$gradient-angle 0.3s, ##theme 0.3s',
|
|
571
680
|
},
|
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
|
|
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. The `order` hint is the highest source priority among the entries that produced each rule (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
|
|
|
@@ -120,6 +120,8 @@ Removing don't-care dimensions before parsing prevents combinatorial blowup in l
|
|
|
120
120
|
|
|
121
121
|
Converts each state key in a style value map (like `'hovered & !disabled'`, `'@media(w < 768px)'`) into `ConditionNode` trees. `parseStyleEntries` walks the object keys in source order and assigns priorities; `parseStateKey` parses a single key string.
|
|
122
122
|
|
|
123
|
+
`parseStyleEntries` also handles the bare `''` default and the standalone `_` fallback floor before the priority order is finalized. The bare `''` default only behaves correctly as the lowest-priority state, so a misplaced one is moved to the front (`MISPLACED_DEFAULT_STATE` warning). The `_` key is pulled out as a separate floor entry (`floor: true`, `TrueCondition`) and always assigned the lowest priority. If a map defines only `_` and a bare `''` (no other states), the `''` default is redundant — it is dropped in favor of the `_` value (`REDUNDANT_DEFAULT_STATE` warning). A `_` combined with state logic (`_ & hovered`, `_ | x`) is ignored (`INVALID_FALLBACK_KEY` warning).
|
|
124
|
+
|
|
123
125
|
### How It Works
|
|
124
126
|
|
|
125
127
|
1. **Tokenization**: The state key is split into tokens using a regex pattern that recognizes:
|
|
@@ -293,6 +295,32 @@ C: C & !A & !B (applies only when neither A nor B)
|
|
|
293
295
|
|
|
294
296
|
Each exclusive condition is passed through `simplifyCondition`. Entries that simplify to `FALSE` (impossible) are filtered out. The default state (`''` → `TrueCondition`) is not added to the “prior” list for negation (see `buildExclusiveConditions`).
|
|
295
297
|
|
|
298
|
+
### `_` fallback floor
|
|
299
|
+
|
|
300
|
+
An entry flagged `floor: true` (from the standalone `_` key) is the one
|
|
301
|
+
exception to the negation cascade above. `buildExclusiveConditions` **pulls the
|
|
302
|
+
floor entries out** before the negation pass, builds the remaining entries with a
|
|
303
|
+
plain unconditional negation loop, then re-appends each floor with its own
|
|
304
|
+
(`TRUE`) condition as the exclusive condition. So the floor **always applies**,
|
|
305
|
+
never receives `!prior`, and never negates lower-priority entries. Because its
|
|
306
|
+
condition is `TRUE`, Stage 1 `mergeEntriesByValue` already leaves it untouched
|
|
307
|
+
(same guard as the bare `''` default), so no floor-specific merge handling is
|
|
308
|
+
needed.
|
|
309
|
+
|
|
310
|
+
This exists to fix the *unknown-query hole*: CSS three-valued logic makes
|
|
311
|
+
`not(unknown) = unknown`, so a negated `@supports(...)` / container-query default
|
|
312
|
+
branch silently never applies. A `_` floor sidesteps negation entirely and lets
|
|
313
|
+
the positive override layer on top via the cascade (see Stage 7's `:where()`
|
|
314
|
+
equalization and the ascending-priority post-pass).
|
|
315
|
+
|
|
316
|
+
Because the floor is **additive** (always emitted, never negated) rather than
|
|
317
|
+
mutually exclusive, it differs observably from the `''` default in two ways
|
|
318
|
+
beyond the unknown-query fix: an "empty" higher-priority state (one that emits no
|
|
319
|
+
declarations) cannot unset the property — the floor still applies — and when
|
|
320
|
+
states emit different declaration sets, the floor's declarations bleed through
|
|
321
|
+
wherever a winning state does not override the same property. The `''` default,
|
|
322
|
+
being negated, swaps out cleanly. See the DSL guide's "`_` vs the `''` default".
|
|
323
|
+
|
|
296
324
|
### Why
|
|
297
325
|
|
|
298
326
|
This eliminates CSS specificity wars. Instead of relying on cascade order, each CSS rule matches in exactly one scenario. Benefits:
|
|
@@ -477,7 +505,15 @@ Converts condition trees into actual CSS selectors and at-rules.
|
|
|
477
505
|
|
|
478
506
|
3. **Contradiction detection**: During variant merging, impossible combinations are dropped (e.g. conflicting media, root, or modifier negations).
|
|
479
507
|
|
|
480
|
-
4. **`materializeComputedRule`**: Groups variants by sorted at-rules plus root-prefix key; within each group, `mergeVariantsIntoSelectorGroups` merges variants that differ only in flat modifier/pseudo parts; builds selector strings and emits one or more `CSSRule` objects.
|
|
508
|
+
4. **`materializeComputedRule`**: Groups variants by sorted at-rules plus root-prefix key; within each group, `mergeVariantsIntoSelectorGroups` merges variants that differ only in flat modifier/pseudo parts; builds selector strings and emits one or more `CSSRule` objects. The rule's `order` hint (cascade priority) is propagated here for the post-pass sort.
|
|
509
|
+
|
|
510
|
+
5. **`:where()` specificity equalization**: Every stateful part of the selector is wrapped in `:where(...)` so it contributes **zero specificity**:
|
|
511
|
+
- the flat root modifiers + pseudos and the root element's `:is()`/`:not()` groups are combined into one `:where(...)` (`buildSelectorFromVariant`);
|
|
512
|
+
- 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;
|
|
513
|
+
- `@parent` ancestors (`parentGroupsToCSS`) become `:where(... *)` / `:where(:not(... *))`;
|
|
514
|
+
- the `@root` context prefix (`rootGroupsToCSS`) becomes `:where(:root...)` (zeroing the `:root` pseudo too).
|
|
515
|
+
|
|
516
|
+
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.
|
|
481
517
|
|
|
482
518
|
### Why
|
|
483
519
|
|
|
@@ -498,6 +534,8 @@ interface CSSRule {
|
|
|
498
534
|
declarations: string; // CSS declarations (e.g. 'color: red;')
|
|
499
535
|
atRules?: string[]; // Wrapping at-rules
|
|
500
536
|
rootPrefix?: string; // Root state prefix
|
|
537
|
+
startingStyle?: boolean; // Wrap declarations in @starting-style
|
|
538
|
+
order?: number; // Internal cascade order hint (ascending source priority); stripped before injection
|
|
501
539
|
}
|
|
502
540
|
```
|
|
503
541
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/tasty",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "A design-system-integrated styling system and DSL for concise, state-aware UI styling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -165,13 +165,13 @@
|
|
|
165
165
|
"name": "main (import *)",
|
|
166
166
|
"path": "dist/index.js",
|
|
167
167
|
"import": "*",
|
|
168
|
-
"limit": "
|
|
168
|
+
"limit": "53 kB"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"name": "core (import *)",
|
|
172
172
|
"path": "dist/core/index.js",
|
|
173
173
|
"import": "*",
|
|
174
|
-
"limit": "
|
|
174
|
+
"limit": "50.5 kB"
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
"name": "static",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"path",
|
|
189
189
|
"crypto"
|
|
190
190
|
],
|
|
191
|
-
"limit": "
|
|
191
|
+
"limit": "32 kB"
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"name": "babel-plugin",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"path",
|
|
200
200
|
"crypto"
|
|
201
201
|
],
|
|
202
|
-
"limit": "
|
|
202
|
+
"limit": "46.65 kB"
|
|
203
203
|
}
|
|
204
204
|
],
|
|
205
205
|
"scripts": {
|