@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.
Files changed (43) hide show
  1. package/dist/{collector-c00_hT9R.js → collector-BEF4F_PE.js} +3 -3
  2. package/dist/{collector-c00_hT9R.js.map → collector-BEF4F_PE.js.map} +1 -1
  3. package/dist/{collector-osfWTeRd.d.ts → collector-CpU85p2G.d.ts} +2 -2
  4. package/dist/{config-BoZDUHW5.d.ts → config-3h7BtX4l.d.ts} +2 -2
  5. package/dist/{config-IzenlK2R.js → config-BDnvK42H.js} +308 -74
  6. package/dist/config-BDnvK42H.js.map +1 -0
  7. package/dist/core/index.d.ts +5 -5
  8. package/dist/core/index.js +6 -6
  9. package/dist/{core-J9U8fXzr.js → core-BiFQGi4v.js} +5 -5
  10. package/dist/{core-J9U8fXzr.js.map → core-BiFQGi4v.js.map} +1 -1
  11. package/dist/{css-writer-CCyaR6ZM.js → css-writer-4IMPW4i1.js} +3 -3
  12. package/dist/{css-writer-CCyaR6ZM.js.map → css-writer-4IMPW4i1.js.map} +1 -1
  13. package/dist/{format-rules-CCb7qNPt.js → format-rules-DrpEA0CZ.js} +2 -2
  14. package/dist/{format-rules-CCb7qNPt.js.map → format-rules-DrpEA0CZ.js.map} +1 -1
  15. package/dist/{hydrate-DEsdmcdy.js → hydrate-DItCQmmZ.js} +2 -2
  16. package/dist/{hydrate-DEsdmcdy.js.map → hydrate-DItCQmmZ.js.map} +1 -1
  17. package/dist/{index-tcHuMPFt.d.ts → index-BsJz5xBF.d.ts} +2 -2
  18. package/dist/{index-D-OA_O6i.d.ts → index-C_SRmAWj.d.ts} +193 -163
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +7 -7
  21. package/dist/{keyframes-DxAp6xwG.js → keyframes-DYGxQPjN.js} +2 -2
  22. package/dist/{keyframes-DxAp6xwG.js.map → keyframes-DYGxQPjN.js.map} +1 -1
  23. package/dist/{merge-styles-BMWcH6MF.d.ts → merge-styles-BzOwGOGC.d.ts} +2 -2
  24. package/dist/{merge-styles-Ugifi570.js → merge-styles-oU4CfZY7.js} +2 -2
  25. package/dist/{merge-styles-Ugifi570.js.map → merge-styles-oU4CfZY7.js.map} +1 -1
  26. package/dist/{resolve-recipes-DEmQhiop.js → resolve-recipes-PwM-R6Jc.js} +3 -3
  27. package/dist/{resolve-recipes-DEmQhiop.js.map → resolve-recipes-PwM-R6Jc.js.map} +1 -1
  28. package/dist/ssr/astro-client.js +1 -1
  29. package/dist/ssr/astro.js +3 -3
  30. package/dist/ssr/index.d.ts +1 -1
  31. package/dist/ssr/index.js +3 -3
  32. package/dist/ssr/next.d.ts +1 -1
  33. package/dist/ssr/next.js +4 -4
  34. package/dist/static/index.d.ts +2 -2
  35. package/dist/static/index.js +1 -1
  36. package/dist/zero/babel.d.ts +1 -1
  37. package/dist/zero/babel.js +4 -4
  38. package/dist/zero/index.d.ts +1 -1
  39. package/dist/zero/index.js +1 -1
  40. package/docs/dsl.md +110 -1
  41. package/docs/pipeline.md +40 -2
  42. package/package.json +5 -5
  43. 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': 'okhsl(280 80% 50%)',
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:188`) filters duplicates using a key of `selector|declarations|atRules|rootPrefix|startingStyle` and then reorders rules so every `@starting-style` rule is emitted **after** all normal rules. This ordering is cascade-critical: `@starting-style` rules share specificity with their normal counterparts, and source order decides which value wins.
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.6.5",
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": "52 kB"
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": "49.5 kB"
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": "31 kB"
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": "45.65 kB"
202
+ "limit": "46.65 kB"
203
203
  }
204
204
  ],
205
205
  "scripts": {