@tenphi/tasty 0.0.0-snapshot.37c3068 → 0.0.0-snapshot.61865f6

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 CHANGED
@@ -24,9 +24,10 @@ On top of that foundation, Tasty gives you a concise, CSS-like DSL with design t
24
24
  ## Why Tasty
25
25
 
26
26
  - **Deterministic at any scale** — Exclusive selector generation eliminates the entire class of cascade/specificity bugs. Every state combination resolves to exactly one CSS rule per property. Refactor freely.
27
+ - **AI-friendly by design** — Style definitions are declarative, self-contained, and structurally consistent. AI tools can read, understand, and refactor even advanced state bindings as confidently as a human — because there's no hidden cascade logic or implicit ordering to second-guess.
27
28
  - **DSL that feels like CSS** — Property names you already know (`padding`, `color`, `display`) with syntax sugar that removes boilerplate. Learn the DSL in minutes, not days.
28
29
  - **Design-system native** — Color tokens (`#primary`), spacing units (`2x`), typography presets (`h1`, `t2`), border radius (`1r`), and recipes are first-class primitives, not afterthoughts.
29
- - **Full modern CSS coverage** — Media queries, container queries, `@supports`, `:has()`, `@starting-style`, `@property`, keyframes, boolean state logic with `&`, `|`, `!` operators. If CSS can do it, Tasty can express it — concisely.
30
+ - **Near-complete modern CSS coverage** — Media queries, container queries, `@supports`, `:has()`, `@starting-style`, `@property`, `@keyframes`, etc. Some features that don't fit Tasty's component model (such as `@layer` and `!important`) are intentionally omitted, but real-world use cases are covered almost completely.
30
31
  - **Runtime or zero-runtime — your call** — Use `tasty()` for dynamic React components with runtime injection, or `tastyStatic()` with the Babel plugin for zero-runtime CSS extraction. Same DSL, same tokens, same output.
31
32
  - **Only generate what is used** — In runtime mode, Tasty injects CSS on demand for mounted components/variants, so your app avoids shipping style rules for UI states that are never rendered.
32
33
  - **Runtime performance that holds at scale** — The runtime path is tested against enterprise-scale applications and tuned with multi-level caching, chunk-level style reuse, style garbage collection, and a dedicated injector.
@@ -133,9 +134,13 @@ Predefined states turn complex selector logic into single tokens. Use `@mobile`
133
134
 
134
135
  This is the core idea that makes everything else possible.
135
136
 
136
- Traditional CSS uses the cascade to resolve conflicts: when multiple selectors match, the one with the highest specificity wins, or — if specificity is equal — the last one in source order wins. This makes styles inherently fragile. Reordering imports, adding a new media query, or composing components from different libraries can silently break styling.
137
+ Traditional CSS has two structural problems.
137
138
 
138
- Tasty takes a fundamentally different approach: **every state mapping compiles into selectors that are guaranteed to never overlap.**
139
+ First, the **cascade** resolves conflicts by specificity and source order: when multiple selectors match, the one with the highest specificity wins, or — if specificity is equal — the last one in source order wins. That makes styles inherently fragile. Reordering imports, adding a media query, or composing components from different libraries can silently break styling.
140
+
141
+ Second, **authoring selectors that capture real-world state logic is fundamentally hard.** A single state like "dark mode" may depend on a root attribute, an OS preference, or both — each branch needing its own selector, proper negation of competing branches, and correct `@media` nesting. The example below shows the CSS you'd write by hand for just *one* property with *one* state. Scale that across dozens of properties, then add breakpoints and container queries, and the selector logic quickly becomes unmanageable.
142
+
143
+ Tasty solves both problems at once: **every state mapping compiles into mutually exclusive selectors.**
139
144
 
140
145
  ```tsx
141
146
  const Text = tasty({
@@ -180,7 +185,9 @@ If `@dark` expands to `@root(schema=dark) | (!@root(schema) & @media(prefers-col
180
185
  }
181
186
  ```
182
187
 
183
- Every rule is guarded by the negation of all higher-priority rules. No two rules can ever match simultaneously. No specificity arithmetic. No source-order dependence. Components compose and extend without ever colliding.
188
+ Every rule is guarded by the negation of higher-priority rules. No two rules can match at the same time. No specificity arithmetic. No source-order dependence. Components compose and extend without collisions.
189
+
190
+ By absorbing selector complexity, Tasty makes advanced CSS patterns practical again — nested container queries, multi-condition `@supports` gates, and combined root-state/media branches. You stay in pure CSS instead of relying on JavaScript workarounds, so the browser can optimize layout, painting, and transitions natively. Tasty doesn't limit CSS; it unlocks its full potential by removing the complexity that held teams back.
184
191
 
185
192
  ## Capabilities
186
193
 
@@ -412,11 +419,12 @@ If you choose the runtime approach, performance is usually a non-issue in practi
412
419
 
413
420
  | Import | Description | Platform |
414
421
  |--------|-------------|----------|
415
- | `@tenphi/tasty` | Runtime style engine | Browser |
416
- | `@tenphi/tasty/static` | Zero-runtime static styles | Browser |
417
- | `@tenphi/tasty/babel-plugin` | Babel plugin for CSS extraction | Node |
422
+ | `@tenphi/tasty` | Runtime style engine (`tasty`, hooks, `configure`) | Browser |
423
+ | `@tenphi/tasty/static` | Zero-runtime static styles (`tastyStatic`) | Browser |
424
+ | `@tenphi/tasty/core` | Lower-level internals (config, parser, pipeline, injector, style handlers) for tooling and advanced use | Browser / Node |
425
+ | `@tenphi/tasty/babel-plugin` | Babel plugin for zero-runtime CSS extraction | Node |
418
426
  | `@tenphi/tasty/zero` | Programmatic extraction API | Node |
419
- | `@tenphi/tasty/next` | Next.js integration | Node |
427
+ | `@tenphi/tasty/next` | Next.js integration wrapper | Node |
420
428
 
421
429
  ## Ecosystem
422
430
 
@@ -455,14 +463,21 @@ const tokens = theme.tasty(); // Ready-to-use Tasty tokens
455
463
 
456
464
  Syntax highlighting for Tasty styles in TypeScript, TSX, JavaScript, and JSX. Highlights color tokens, custom units, state keys, presets, and style properties inside `tasty()`, `tastyStatic()`, and related APIs.
457
465
 
466
+ <p align="center">
467
+ <img src="assets/tasty-vscode-highlight.png" width="512" alt="Tasty VS Code syntax highlighting example">
468
+ </p>
469
+
458
470
  ### [Cube UI Kit](https://github.com/cube-js/cube-ui-kit)
459
471
 
460
472
  Open-source React UI kit built on Tasty + React Aria. 100+ production components proving Tasty works at design-system scale. A reference implementation and a ready-to-use component library.
461
473
 
462
474
  ## Documentation
463
475
 
464
- - **[Runtime API (tasty)](docs/tasty.md)** — Full runtime styling documentation: component creation, state mappings, sub-elements, variants, hooks, configuration, and style property reference
476
+ - **[Runtime API (tasty)](docs/tasty.md)** — Full runtime styling documentation: component creation, state mappings, sub-elements, variants, hooks, and configuration
477
+ - **[Style Properties](docs/styles.md)** — Complete reference for all enhanced style properties: syntax, values, modifiers, and recommendations
465
478
  - **[Zero Runtime (tastyStatic)](docs/tasty-static.md)** — Build-time static styling: Babel plugin setup, Next.js integration, and static style patterns
479
+ - **[Style Injector](docs/injector.md)** — Internal CSS injection engine: `inject()`, `injectGlobal()`, `injectRawCSS()`, `keyframes()`, deduplication, reference counting, cleanup, SSR support, and Shadow DOM
480
+ - **[Debug Utilities](docs/debug.md)** — Runtime CSS inspection via `tastyDebug`: CSS extraction, element inspection, cache metrics, chunk breakdown, and performance monitoring
466
481
 
467
482
  ## License
468
483
 
@@ -1,4 +1,5 @@
1
1
  import { Lru } from "../parser/lru.js";
2
+ import { not } from "./conditions.js";
2
3
 
3
4
  //#region src/pipeline/materialize.ts
4
5
  /**
@@ -68,27 +69,43 @@ function stateToCSS(state) {
68
69
  case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootConditions");
69
70
  case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
70
71
  case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownConditions");
71
- default: {
72
- const variant = emptyVariant();
73
- switch (state.type) {
74
- case "modifier":
75
- variant.modifierConditions.push(modifierToParsed(state));
76
- break;
77
- case "pseudo":
78
- variant.pseudoConditions.push(pseudoToParsed(state));
79
- break;
80
- case "container":
81
- variant.containerConditions.push(containerToParsed(state));
82
- break;
83
- case "supports":
84
- variant.supportsConditions.push(supportsToParsed(state));
85
- break;
86
- case "starting":
87
- variant.startingStyle = !state.negated;
88
- break;
89
- }
72
+ case "modifier": {
73
+ const v = emptyVariant();
74
+ v.modifierConditions.push(modifierToParsed(state));
90
75
  return {
91
- variants: [variant],
76
+ variants: [v],
77
+ isImpossible: false
78
+ };
79
+ }
80
+ case "pseudo": {
81
+ const v = emptyVariant();
82
+ v.pseudoConditions.push(pseudoToParsed(state));
83
+ return {
84
+ variants: [v],
85
+ isImpossible: false
86
+ };
87
+ }
88
+ case "container": {
89
+ const v = emptyVariant();
90
+ v.containerConditions.push(containerToParsed(state));
91
+ return {
92
+ variants: [v],
93
+ isImpossible: false
94
+ };
95
+ }
96
+ case "supports": {
97
+ const v = emptyVariant();
98
+ v.supportsConditions.push(supportsToParsed(state));
99
+ return {
100
+ variants: [v],
101
+ isImpossible: false
102
+ };
103
+ }
104
+ case "starting": {
105
+ const v = emptyVariant();
106
+ v.startingStyle = !state.negated;
107
+ return {
108
+ variants: [v],
92
109
  isImpossible: false
93
110
  };
94
111
  }
@@ -227,35 +244,46 @@ function supportsToParsed(state) {
227
244
  };
228
245
  }
229
246
  /**
247
+ * Collect all modifier and pseudo conditions from a variant as a flat array.
248
+ */
249
+ function collectSelectorConditions(variant) {
250
+ return [...variant.modifierConditions, ...variant.pseudoConditions];
251
+ }
252
+ /**
230
253
  * Convert an inner condition tree into SelectorVariants.
231
254
  * Each inner OR branch becomes a separate variant, preserving disjunction.
232
255
  * Shared by @root() and @own().
233
256
  */
234
257
  function innerConditionToVariants(innerCondition, negated, target) {
235
- const innerCSS = conditionToCSS(innerCondition);
258
+ const innerCSS = conditionToCSS(negated ? not(innerCondition) : innerCondition);
236
259
  if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
237
260
  variants: [],
238
261
  isImpossible: true
239
262
  };
240
- return {
241
- variants: innerCSS.variants.map((innerVariant) => {
263
+ const variants = [];
264
+ for (const innerVariant of innerCSS.variants) {
265
+ const conditions = collectSelectorConditions(innerVariant);
266
+ if (conditions.length > 0) {
242
267
  const v = emptyVariant();
243
- for (const mod of innerVariant.modifierConditions) v[target].push({
244
- ...mod,
245
- negated: negated ? !mod.negated : mod.negated
246
- });
247
- for (const pseudo of innerVariant.pseudoConditions) v[target].push({
248
- ...pseudo,
249
- negated: negated ? !pseudo.negated : pseudo.negated
250
- });
251
- return v;
252
- }),
268
+ v[target].push(...conditions);
269
+ variants.push(v);
270
+ }
271
+ }
272
+ if (variants.length === 0) return {
273
+ variants: [emptyVariant()],
274
+ isImpossible: false
275
+ };
276
+ return {
277
+ variants,
253
278
  isImpossible: false
254
279
  };
255
280
  }
256
281
  /**
257
282
  * Convert a @parent() inner condition into SelectorVariants with ParentGroups.
258
- * Each inner OR branch becomes a separate variant, each containing one ParentGroup.
283
+ *
284
+ * Positive: each inner OR branch becomes a separate variant with one :is() group.
285
+ * Negated: !(A | B) = !A & !B — all branches become :not() groups collected
286
+ * into a single variant so they produce :not([a] *):not([b] *) on one element.
259
287
  */
260
288
  function parentConditionToVariants(innerCondition, negated, direct) {
261
289
  const innerCSS = conditionToCSS(innerCondition);
@@ -263,19 +291,44 @@ function parentConditionToVariants(innerCondition, negated, direct) {
263
291
  variants: [],
264
292
  isImpossible: true
265
293
  };
266
- return {
267
- variants: innerCSS.variants.map((innerVariant) => {
294
+ if (negated) {
295
+ const v = emptyVariant();
296
+ for (const innerVariant of innerCSS.variants) {
297
+ const conditions = collectSelectorConditions(innerVariant);
298
+ if (conditions.length > 0) v.parentGroups.push({
299
+ conditions,
300
+ direct,
301
+ negated: true
302
+ });
303
+ }
304
+ if (v.parentGroups.length === 0) return {
305
+ variants: [emptyVariant()],
306
+ isImpossible: false
307
+ };
308
+ return {
309
+ variants: [v],
310
+ isImpossible: false
311
+ };
312
+ }
313
+ const variants = [];
314
+ for (const innerVariant of innerCSS.variants) {
315
+ const conditions = collectSelectorConditions(innerVariant);
316
+ if (conditions.length > 0) {
268
317
  const v = emptyVariant();
269
- const group = {
270
- conditions: [],
318
+ v.parentGroups.push({
319
+ conditions,
271
320
  direct,
272
- negated
273
- };
274
- for (const mod of innerVariant.modifierConditions) group.conditions.push(mod);
275
- for (const pseudo of innerVariant.pseudoConditions) group.conditions.push(pseudo);
276
- v.parentGroups.push(group);
277
- return v;
278
- }),
321
+ negated: false
322
+ });
323
+ variants.push(v);
324
+ }
325
+ }
326
+ if (variants.length === 0) return {
327
+ variants: [emptyVariant()],
328
+ isImpossible: false
329
+ };
330
+ return {
331
+ variants,
279
332
  isImpossible: false
280
333
  };
281
334
  }
@@ -330,36 +383,6 @@ function getSelectorConditionKey(cond) {
330
383
  return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
331
384
  }
332
385
  /**
333
- * Deduplicate modifier conditions
334
- */
335
- function dedupeModifierConditions(conditions) {
336
- const seen = /* @__PURE__ */ new Set();
337
- const result = [];
338
- for (const c of conditions) {
339
- const key = getModifierKey(c);
340
- if (!seen.has(key)) {
341
- seen.add(key);
342
- result.push(c);
343
- }
344
- }
345
- return result;
346
- }
347
- /**
348
- * Deduplicate pseudo conditions
349
- */
350
- function dedupePseudoConditions(conditions) {
351
- const seen = /* @__PURE__ */ new Set();
352
- const result = [];
353
- for (const c of conditions) {
354
- const key = getPseudoKey(c);
355
- if (!seen.has(key)) {
356
- seen.add(key);
357
- result.push(c);
358
- }
359
- }
360
- return result;
361
- }
362
- /**
363
386
  * Deduplicate selector conditions (modifiers or pseudos).
364
387
  * Shared by root, parent, and own conditions.
365
388
  */
@@ -374,15 +397,16 @@ function dedupeSelectorConditions(conditions) {
374
397
  }
375
398
  }
376
399
  const negatedBooleanAttrs = /* @__PURE__ */ new Set();
377
- const positiveValuesByAttr = /* @__PURE__ */ new Map();
400
+ const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
378
401
  for (const c of result) {
379
402
  if (!("attribute" in c)) continue;
380
403
  if (c.negated && c.value === void 0) negatedBooleanAttrs.add(c.attribute);
381
- if (!c.negated && c.value !== void 0) {
382
- let values = positiveValuesByAttr.get(c.attribute);
404
+ const op = c.operator ?? "=";
405
+ if (!c.negated && c.value !== void 0 && op === "=") {
406
+ let values = positiveExactValuesByAttr.get(c.attribute);
383
407
  if (!values) {
384
408
  values = /* @__PURE__ */ new Set();
385
- positiveValuesByAttr.set(c.attribute, values);
409
+ positiveExactValuesByAttr.set(c.attribute, values);
386
410
  }
387
411
  values.add(c.value);
388
412
  }
@@ -390,7 +414,8 @@ function dedupeSelectorConditions(conditions) {
390
414
  result = result.filter((c) => {
391
415
  if (!("attribute" in c) || !c.negated || c.value === void 0) return true;
392
416
  if (negatedBooleanAttrs.has(c.attribute)) return false;
393
- const positiveValues = positiveValuesByAttr.get(c.attribute);
417
+ if ((c.operator ?? "=") !== "=") return true;
418
+ const positiveValues = positiveExactValuesByAttr.get(c.attribute);
394
419
  if (positiveValues !== void 0 && positiveValues.size === 1 && !positiveValues.has(c.value)) return false;
395
420
  return true;
396
421
  });
@@ -433,6 +458,21 @@ function hasSelectorConditionContradiction(conditions) {
433
458
  return hasModifierContradiction(modifiers) || hasPseudoContradiction(pseudos);
434
459
  }
435
460
  /**
461
+ * Check for parent group contradiction: same target (direct + conditions)
462
+ * with opposite negation. E.g. :not([data-hovered] *) and :is([data-hovered] *)
463
+ * in the same variant is impossible.
464
+ */
465
+ function hasParentGroupContradiction(groups) {
466
+ const byBaseKey = /* @__PURE__ */ new Map();
467
+ for (const g of groups) {
468
+ const baseKey = `${g.direct ? ">" : ""}(${g.conditions.map(getSelectorConditionKey).sort().join(",")})`;
469
+ const existing = byBaseKey.get(baseKey);
470
+ if (existing !== void 0 && existing !== !g.negated) return true;
471
+ byBaseKey.set(baseKey, !g.negated);
472
+ }
473
+ return false;
474
+ }
475
+ /**
436
476
  * Merge two selector variants (AND operation)
437
477
  * Deduplicates conditions and checks for contradictions
438
478
  */
@@ -441,11 +481,11 @@ function mergeVariants(a, b) {
441
481
  if (hasMediaContradiction(mergedMedia)) return null;
442
482
  const mergedRoots = dedupeSelectorConditions([...a.rootConditions, ...b.rootConditions]);
443
483
  if (hasSelectorConditionContradiction(mergedRoots)) return null;
444
- const mergedModifiers = dedupeModifierConditions([...a.modifierConditions, ...b.modifierConditions]);
445
- if (hasModifierContradiction(mergedModifiers)) return null;
446
- const mergedPseudos = dedupePseudoConditions([...a.pseudoConditions, ...b.pseudoConditions]);
447
- if (hasPseudoContradiction(mergedPseudos)) return null;
484
+ const mergedModifiers = dedupeSelectorConditions([...a.modifierConditions, ...b.modifierConditions]);
485
+ const mergedPseudos = dedupeSelectorConditions([...a.pseudoConditions, ...b.pseudoConditions]);
486
+ if (hasSelectorConditionContradiction([...mergedModifiers, ...mergedPseudos])) return null;
448
487
  const mergedParentGroups = [...a.parentGroups, ...b.parentGroups];
488
+ if (hasParentGroupContradiction(mergedParentGroups)) return null;
449
489
  const mergedOwn = dedupeSelectorConditions([...a.ownConditions, ...b.ownConditions]);
450
490
  if (hasSelectorConditionContradiction(mergedOwn)) return null;
451
491
  const mergedContainers = dedupeContainerConditions([...a.containerConditions, ...b.containerConditions]);
@@ -620,7 +660,7 @@ function getVariantKey(v) {
620
660
  containerKey,
621
661
  v.supportsConditions.map((c) => `${c.subtype}:${c.negated ? "!" : ""}${c.condition}`).sort().join("|"),
622
662
  v.rootConditions.map(getSelectorConditionKey).sort().join("|"),
623
- v.parentGroups.map((g) => `${g.direct ? ">" : ""}(${g.conditions.map(getSelectorConditionKey).sort().join(",")})`).sort().join("|"),
663
+ v.parentGroups.map(getParentGroupKey).sort().join("|"),
624
664
  v.startingStyle ? "1" : "0"
625
665
  ].join("###");
626
666
  }