clava 0.2.1 → 0.2.3

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/src/index.ts CHANGED
@@ -30,28 +30,54 @@ import {
30
30
  styleValueToJSXStyle,
31
31
  } from "./utils.ts";
32
32
 
33
- // Internal metadata stored on components but hidden from public types
33
+ // Internal compute path: pushes the variant classes contributed by this
34
+ // component (and its extends chain) into `classesOut` and merges any styles
35
+ // into `styleOut`. Base class is handled by callers via ComponentMeta.baseClass
36
+ // to avoid string-parsing round trips. Both outputs are mutated in place to
37
+ // avoid intermediate allocations.
38
+ type ComputeFn = (
39
+ resolved: Record<string, unknown>,
40
+ userVariantProps: Record<string, unknown>,
41
+ skipKeys: Set<string> | null,
42
+ skipValues: Record<string, Set<string>> | null,
43
+ classesOut: ClsxClassValue[],
44
+ styleOut: StyleValue,
45
+ ) => void;
46
+
47
+ // Internal metadata stored on components but hidden from public types.
34
48
  interface ComponentMeta {
35
49
  baseClass: string;
36
50
  staticDefaults: Record<string, unknown>;
37
- resolveDefaults: (
38
- childDefaults: Record<string, unknown>,
39
- userProps?: Record<string, unknown>,
40
- ) => Record<string, unknown>;
51
+ // Returns variants set via setDefaultVariants in the computed function chain.
52
+ // null when this component has no resolveDefaults work to do (no `computed`
53
+ // and no extends with work).
54
+ resolveDefaults:
55
+ | ((
56
+ childDefaults: Record<string, unknown>,
57
+ userProps?: Record<string, unknown>,
58
+ ) => Record<string, unknown>)
59
+ | null;
60
+ // Returns variant classes + style for this component, used by extending
61
+ // components. Top-level rendering also routes through this.
62
+ compute: ComputeFn;
63
+ // Reference identity is used to detect mixed-factory `extend`. When a
64
+ // component is extended by a parent from a different `create()` call, the
65
+ // parent applies this transform to the extend's contribution before joining,
66
+ // preserving each factory's transform boundary.
67
+ transformClass: (className: string) => string;
41
68
  }
42
69
 
43
70
  const META_KEY = "__meta";
44
71
 
45
- // Symbol property used to pass skip keys through the props object without
46
- // polluting the actual variant values. This allows the computed function to
47
- // see actual variant values while still skipping styling for overridden keys.
48
- const SKIP_STYLE_KEYS = Symbol("skipStyleKeys");
49
- const SKIP_STYLE_VARIANT_VALUES = Symbol("skipStyleVariantValues");
50
-
51
72
  // eslint-disable-next-line @typescript-eslint/unbound-method
52
73
  const hasOwn = Object.prototype.hasOwnProperty;
53
74
 
54
- // Dynamic property access on function requires cast through unknown
75
+ const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
76
+ string,
77
+ unknown
78
+ >;
79
+
80
+ // Dynamic property access on function requires cast through unknown.
55
81
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
56
82
  return (component as unknown as Record<string, unknown>)[META_KEY] as
57
83
  | ComponentMeta
@@ -62,19 +88,6 @@ function setComponentMeta(component: AnyComponent, meta: ComponentMeta): void {
62
88
  (component as unknown as Record<string, unknown>)[META_KEY] = meta;
63
89
  }
64
90
 
65
- /**
66
- * Mutates target by assigning all properties from source. Avoids object spread
67
- * overhead in hot paths where we're building up a result object.
68
- */
69
- function assign<T extends object>(target: T, source: T): void {
70
- for (const key in source) {
71
- if (!hasOwn.call(source, key)) continue;
72
- (target as Record<string, unknown>)[key] = (
73
- source as Record<string, unknown>
74
- )[key];
75
- }
76
- }
77
-
78
91
  export type {
79
92
  ClassValue,
80
93
  StyleValue,
@@ -280,28 +293,6 @@ function collectDisabledVariantValues(
280
293
  return values;
281
294
  }
282
295
 
283
- /**
284
- * Extracts classes from fullClass that are not in baseClass. Uses string
285
- * comparison optimization: if fullClass starts with baseClass, just take the
286
- * suffix.
287
- */
288
- function extractVariantClasses(fullClass: string, baseClass: string): string {
289
- if (!fullClass) return "";
290
- if (!baseClass) return fullClass;
291
-
292
- // Fast path: fullClass starts with baseClass (common case)
293
- if (fullClass.startsWith(baseClass)) {
294
- return fullClass.slice(baseClass.length).trim();
295
- }
296
-
297
- // Slow path: need to diff the class sets
298
- const baseClassSet = new Set(baseClass.split(" ").filter(Boolean));
299
- return fullClass
300
- .split(" ")
301
- .filter((c) => c && !baseClassSet.has(c))
302
- .join(" ");
303
- }
304
-
305
296
  interface NormalizedSource {
306
297
  keys: string[];
307
298
  variantKeys: string[];
@@ -352,7 +343,7 @@ function splitPropsImpl(
352
343
  selfKeys: string[],
353
344
  selfIsComponent: boolean,
354
345
  props: Record<string, unknown>,
355
- sources: NormalizedSource[],
346
+ sources: unknown[],
356
347
  ): Record<string, unknown>[] {
357
348
  const sourcesLength = sources.length;
358
349
  const results: Record<string, unknown>[] = [];
@@ -373,8 +364,7 @@ function splitPropsImpl(
373
364
  const effectiveKeyArrays: string[][] = [selfKeys];
374
365
 
375
366
  for (let s = 0; s < sourcesLength; s++) {
376
- const source = sources[s];
377
- if (source === undefined) continue;
367
+ const source = normalizeKeySource(sources[s]);
378
368
  const sourceResult: Record<string, unknown> = {};
379
369
 
380
370
  const effectiveKeys =
@@ -431,16 +421,11 @@ export const splitProps: SplitPropsFunction = ((
431
421
  ...sources: unknown[]
432
422
  ) => {
433
423
  const normalizedSource1 = normalizeKeySource(source1);
434
- const sourcesLength = sources.length;
435
- const normalizedSources: NormalizedSource[] = [];
436
- for (let i = 0; i < sourcesLength; i++) {
437
- normalizedSources.push(normalizeKeySource(sources[i]));
438
- }
439
424
  return splitPropsImpl(
440
425
  normalizedSource1.keys,
441
426
  normalizedSource1.isComponent,
442
427
  props,
443
- normalizedSources,
428
+ sources,
444
429
  );
445
430
  }) as SplitPropsFunction;
446
431
 
@@ -486,75 +471,6 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
486
471
  };
487
472
  }
488
473
 
489
- /**
490
- * Creates the resolveDefaults function for a component. This function returns
491
- * only the variants set via setDefaultVariants in the computed function. Used
492
- * by child components to get parent's computed defaults.
493
- */
494
- function createResolveDefaults(
495
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
496
- staticDefaults: Record<string, unknown>,
497
- ): ComponentMeta["resolveDefaults"] {
498
- const computed = config.computed;
499
- const extend = config.extend;
500
- return (childDefaults, userProps = {}) => {
501
- // Merge: parent static < child static < user props
502
- // This is what parent's computed will see in `variants`
503
- const resolvedVariants: Record<string, unknown> = {};
504
- Object.assign(resolvedVariants, staticDefaults);
505
- for (const key in childDefaults) {
506
- if (!hasOwn.call(childDefaults, key)) continue;
507
- const v = childDefaults[key];
508
- if (v === undefined) continue;
509
- resolvedVariants[key] = v;
510
- }
511
- for (const key in userProps) {
512
- if (!hasOwn.call(userProps, key)) continue;
513
- const v = userProps[key];
514
- if (v === undefined) continue;
515
- resolvedVariants[key] = v;
516
- }
517
-
518
- // Track which keys are set via setDefaultVariants
519
- const computedDefaults: Record<string, unknown> = {};
520
-
521
- // Propagate to extended components so their computed functions can run
522
- if (extend) {
523
- for (const ext of extend) {
524
- const meta = getComponentMeta(ext);
525
- if (!meta) continue;
526
- const extDefaults = meta.resolveDefaults(childDefaults, userProps);
527
- for (const k in extDefaults) {
528
- if (hasOwn.call(extDefaults, k)) {
529
- computedDefaults[k] = extDefaults[k];
530
- }
531
- }
532
- }
533
- }
534
-
535
- if (computed) {
536
- computed({
537
- variants: resolvedVariants as VariantValues<Record<string, unknown>>,
538
- setVariants: () => {},
539
- setDefaultVariants: (newDefaults) => {
540
- for (const key in newDefaults) {
541
- if (!hasOwn.call(newDefaults, key)) continue;
542
- const value = (newDefaults as Record<string, unknown>)[key];
543
- if (userProps[key] !== undefined) continue;
544
- if (isVariantDisabled(config, key)) continue;
545
- if (isVariantValueDisabled(config, key, value)) continue;
546
- computedDefaults[key] = value;
547
- }
548
- },
549
- addClass: () => {},
550
- addStyle: () => {},
551
- });
552
- }
553
-
554
- return computedDefaults;
555
- };
556
- }
557
-
558
474
  /**
559
475
  * Creates the cv and cx functions.
560
476
  */
@@ -574,11 +490,12 @@ export function create({
574
490
 
575
491
  // ----- Pre-computed at creation time -----
576
492
  const variantKeys = collectVariantKeys(config);
493
+ const variantKeysLength = variantKeys.length;
577
494
  const disabledVariantKeys = collectDisabledVariantKeys(config);
578
495
  const disabledVariantValues = collectDisabledVariantValues(config);
579
496
  const hasDisabledVariantKeys = disabledVariantKeys.size > 0;
580
- const hasDisabledVariantValues =
581
- Object.keys(disabledVariantValues).length > 0;
497
+ const disabledVariantValueKeys = Object.keys(disabledVariantValues);
498
+ const hasDisabledVariantValues = disabledVariantValueKeys.length > 0;
582
499
  const hasAnyDisabled = hasDisabledVariantKeys || hasDisabledVariantValues;
583
500
 
584
501
  const inputPropsKeys = ["class", "className", "style", ...variantKeys];
@@ -589,8 +506,7 @@ export function create({
589
506
  const computedVariantsCfg = config.computedVariants;
590
507
  const computed = config.computed;
591
508
  const baseStyle = config.style;
592
- const baseClass: ClassValue =
593
- config.class === undefined ? null : (config.class as ClassValue);
509
+ const hasBaseStyle = !!baseStyle;
594
510
 
595
511
  // Pre-build variant entries for fast iteration. For each variant key in
596
512
  // `variants`, we have a name and a PrebuiltVariant with normalized values.
@@ -670,22 +586,66 @@ export function create({
670
586
  }
671
587
 
672
588
  // Pre-build extended component info, so we don't have to call
673
- // `getComponentMeta` per render.
674
- const extEntries: AnyComponent[] = extend ? (extend as AnyComponent[]) : [];
589
+ // `getComponentMeta` per render. Extends from a different `create()`
590
+ // factory (different `transformClass` identity) need their contribution
591
+ // transformed by their own `transformClass` before being joined into our
592
+ // class string — otherwise our outer `transformClass(clsx(allClasses))`
593
+ // would be the only transform that runs, and the extend's factory would
594
+ // be silently bypassed for any base coming from `extend: [otherFactoryCv]`.
595
+ const extMetas: ComponentMeta[] = [];
675
596
  const extBaseClassesArr: string[] = [];
676
- const extMetas: (ComponentMeta | undefined)[] = [];
597
+ const extIsolated: boolean[] = [];
598
+ let hasIsolatedExt = false;
677
599
  if (extend) {
678
600
  for (const ext of extend) {
679
601
  const meta = getComponentMeta(ext);
602
+ if (!meta) continue;
680
603
  extMetas.push(meta);
681
- extBaseClassesArr.push(meta?.baseClass ?? "");
604
+ const isolated = meta.transformClass !== transformClass;
605
+ extIsolated.push(isolated);
606
+ if (isolated) {
607
+ hasIsolatedExt = true;
608
+ // Apply the extend's own transformClass to its base class so it
609
+ // survives our outer transform (which still applies on top, matching
610
+ // the original public-component round-trip behavior).
611
+ extBaseClassesArr.push(meta.transformClass(meta.baseClass));
612
+ } else {
613
+ extBaseClassesArr.push(meta.baseClass);
614
+ }
682
615
  }
683
616
  }
684
- const extCount = extEntries.length;
685
-
686
- // Inlined "filter disabled" - mutates `out` adding only allowed entries.
687
- // Most components have no disabled variants, in which case we can skip
688
- // the filter entirely.
617
+ const extCount = extMetas.length;
618
+
619
+ // Filter to only extends whose `resolveDefaults` actually does work
620
+ // (config.computed exists, transitively). Iterating these in
621
+ // `resolveVariantsHot` skips empty work.
622
+ const extMetasWithResolveDefaults: ComponentMeta[] = [];
623
+ for (let i = 0; i < extCount; i++) {
624
+ if (extMetas[i].resolveDefaults) {
625
+ extMetasWithResolveDefaults.push(extMetas[i]);
626
+ }
627
+ }
628
+ const extMetasWithResolveDefaultsCount = extMetasWithResolveDefaults.length;
629
+
630
+ // Pre-compute static skip key/value sets to pass to extends. These never
631
+ // change across calls — when caller passes no skip sets, we reuse the same
632
+ // object and avoid Set allocation.
633
+ let staticExtSkipKeys: Set<string> | null = null;
634
+ if (hasDisabledVariantKeys || computedVariantCount > 0) {
635
+ staticExtSkipKeys = new Set<string>();
636
+ for (const k of disabledVariantKeys) staticExtSkipKeys.add(k);
637
+ for (let i = 0; i < computedVariantCount; i++) {
638
+ staticExtSkipKeys.add(computedVariantNames[i]);
639
+ }
640
+ }
641
+ // Skip values are passed directly to extends. We can reuse the same object
642
+ // when no caller-provided values need merging.
643
+ const staticExtSkipValues: Record<string, Set<string>> | null =
644
+ hasDisabledVariantValues ? disabledVariantValues : null;
645
+
646
+ // Branches on `hasAnyDisabled` so the no-disabled path skips the per-key
647
+ // filter checks entirely — most components have no disabled variants and
648
+ // hit only the plain copy.
689
649
  function filterDisabledInto(
690
650
  input: Record<string, unknown>,
691
651
  out: Record<string, unknown>,
@@ -710,12 +670,82 @@ export function create({
710
670
  }
711
671
  }
712
672
 
713
- // Pre-create default-variants resolver which is referenced during the hot
714
- // path through extended components' meta. The closure captures
715
- // staticDefaults, extend, computed, etc.
716
- const resolveDefaultsFn = createResolveDefaults(config, staticDefaults);
673
+ // Pre-create resolveDefaults function used by parents during their
674
+ // `resolveVariantsHot`. Returns the variants set via setDefaultVariants in
675
+ // the computed function chain.
676
+ //
677
+ // When this component has no `computed` and no `extend` with work, the
678
+ // function is null — callers can skip iterating it entirely.
679
+ const resolveDefaultsFn: ComponentMeta["resolveDefaults"] =
680
+ computed || extMetasWithResolveDefaultsCount > 0
681
+ ? (
682
+ childDefaults: Record<string, unknown>,
683
+ userProps: Record<string, unknown> = EMPTY_DEFAULTS,
684
+ ) => {
685
+ // userProps is contractually variant-only (callers pre-filter
686
+ // when starting from a full props object).
687
+ const resolvedVariants: Record<string, unknown> = {};
688
+ Object.assign(resolvedVariants, staticDefaults);
689
+ for (const key in childDefaults) {
690
+ if (!hasOwn.call(childDefaults, key)) continue;
691
+ const v = childDefaults[key];
692
+ if (v === undefined) continue;
693
+ resolvedVariants[key] = v;
694
+ }
695
+ for (const key in userProps) {
696
+ if (!hasOwn.call(userProps, key)) continue;
697
+ const v = userProps[key];
698
+ if (v === undefined) continue;
699
+ resolvedVariants[key] = v;
700
+ }
701
+
702
+ const computedDefaults: Record<string, unknown> = {};
703
+
704
+ for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
705
+ const extDefaults = extMetasWithResolveDefaults[i]
706
+ .resolveDefaults!(childDefaults, userProps);
707
+ for (const k in extDefaults) {
708
+ if (!hasOwn.call(extDefaults, k)) continue;
709
+ computedDefaults[k] = extDefaults[k];
710
+ }
711
+ }
712
+
713
+ if (computed) {
714
+ // Filter to own variant keys so `computed.ctx.variants` matches
715
+ // `VariantValues<V>` when this component is used as an extend by
716
+ // a parent that adds extra variant keys (those keys would
717
+ // otherwise leak through `userProps`).
718
+ const ownVariants: Record<string, unknown> = {};
719
+ for (let i = 0; i < variantKeysLength; i++) {
720
+ const k = variantKeys[i];
721
+ if (hasOwn.call(resolvedVariants, k)) {
722
+ ownVariants[k] = resolvedVariants[k];
723
+ }
724
+ }
725
+ computed({
726
+ variants: ownVariants as VariantValues<Record<string, unknown>>,
727
+ setVariants: noop,
728
+ setDefaultVariants: (newDefaults) => {
729
+ for (const key in newDefaults) {
730
+ if (!hasOwn.call(newDefaults, key)) continue;
731
+ const value = (newDefaults as Record<string, unknown>)[key];
732
+ if (userProps[key] !== undefined) continue;
733
+ if (isVariantDisabled(config, key)) continue;
734
+ if (isVariantValueDisabled(config, key, value)) continue;
735
+ computedDefaults[key] = value;
736
+ }
737
+ },
738
+ addClass: noop,
739
+ addStyle: noop,
740
+ });
741
+ }
717
742
 
718
- // Resolve variants: defaults -> computed defaults from extended -> props.
743
+ return computedDefaults;
744
+ }
745
+ : null;
746
+
747
+ // Hot path: resolve variants by merging static defaults + extends'
748
+ // computed defaults + user-provided props.
719
749
  function resolveVariantsHot(
720
750
  propsVariants: Record<string, unknown>,
721
751
  ): Record<string, unknown> {
@@ -723,22 +753,20 @@ export function create({
723
753
  const defaults: Record<string, unknown> = {};
724
754
  Object.assign(defaults, staticDefaults);
725
755
 
726
- // Apply computed defaults from extended components
727
- if (hasExtend) {
728
- for (let i = 0; i < extCount; i++) {
729
- const meta = extMetas[i];
730
- if (!meta) continue;
731
- const extComputed = meta.resolveDefaults(defaults, propsVariants);
732
- for (const k in extComputed) {
733
- if (hasOwn.call(extComputed, k)) {
734
- defaults[k] = extComputed[k];
735
- }
736
- }
756
+ // Apply computed defaults from extended components (only those that have
757
+ // actual work to do).
758
+ for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
759
+ const meta = extMetasWithResolveDefaults[i];
760
+ const extComputed = meta.resolveDefaults!(defaults, propsVariants);
761
+ for (const k in extComputed) {
762
+ if (!hasOwn.call(extComputed, k)) continue;
763
+ defaults[k] = extComputed[k];
737
764
  }
738
765
  }
739
766
 
740
- // Now merge: defaults < propsVariants (filter undefined)
741
- // Apply propsVariants on top
767
+ // Apply propsVariants on top (filter undefined). propsVariants is
768
+ // contractually variant-only here — callers building from a full props
769
+ // object filter to variant keys before calling.
742
770
  for (const k in propsVariants) {
743
771
  if (!hasOwn.call(propsVariants, k)) continue;
744
772
  const v = propsVariants[k];
@@ -746,239 +774,231 @@ export function create({
746
774
  defaults[k] = v;
747
775
  }
748
776
 
777
+ if (!hasAnyDisabled) return defaults;
778
+
749
779
  // Filter disabled
750
780
  const result: Record<string, unknown> = {};
751
781
  filterDisabledInto(defaults, result);
752
782
  return result;
753
783
  }
754
784
 
755
- // Hot path: build a fresh result.
756
- const computeResult = (
757
- props: ComponentProps<MergedVariants> = {},
758
- ): { className: string; style: StyleValue } => {
759
- // Extract skip style keys from props (set by child's computedVariants)
760
- const skipStyleKeysIn = (props as Record<symbol, unknown>)[
761
- SKIP_STYLE_KEYS
762
- ] as Set<string> | undefined;
763
- const skipStyleVariantValuesIn = (props as Record<symbol, unknown>)[
764
- SKIP_STYLE_VARIANT_VALUES
765
- ] as Record<string, Set<string>> | undefined;
766
-
767
- // Extract variant props from input. Also remember the propsVariants for
768
- // computed-defaults application.
769
- const variantProps: Record<string, unknown> = {};
770
- for (let i = 0; i < variantKeys.length; i++) {
771
- const key = variantKeys[i];
772
- if (key in props) {
773
- variantProps[key] = (props as Record<string, unknown>)[key];
774
- }
775
- }
776
-
777
- // Resolve variants with defaults
778
- let resolvedVariants = resolveVariantsHot(variantProps);
779
-
780
- // Run computed function (may update variants and emit class/style)
781
- let computedClassesArr: ClassValue[] | null = null;
782
- let computedStyleObj: StyleValue | null = null;
785
+ // Core compute path. Called both for top-level rendering (via
786
+ // `computeResult`) and recursively when this component is used as an
787
+ // `extend` target by another component. Pushes variant classes (excluding
788
+ // base class) into `classesOut` and merges styles into `styleOut`.
789
+ const compute: ComputeFn = (
790
+ resolved,
791
+ userVariantProps,
792
+ skipKeys,
793
+ skipValues,
794
+ classesOut,
795
+ styleOut,
796
+ ) => {
797
+ // Run `computed` (if any). May modify resolved variants and emit classes
798
+ // and styles.
799
+ let workingResolved = resolved;
800
+ let cClasses: ClassValue[] | null = null;
801
+ let cStyle: StyleValue | null = null;
783
802
 
784
803
  if (computed) {
785
- const updatedVariants: Record<string, unknown> = {};
786
- Object.assign(updatedVariants, resolvedVariants);
787
- const cClasses: ClassValue[] = [];
788
- let cStyle: StyleValue | null = null;
804
+ // When this component is being extended, `resolved` is the parent's
805
+ // workingResolved (a superset of our variant keys). Filter to our own
806
+ // keys for `ctx.variants` so the user's `computed` callback sees the
807
+ // shape declared by `VariantValues<V>` and not foreign parent keys.
808
+ const ownVariants: Record<string, unknown> = {};
809
+ for (let i = 0; i < variantKeysLength; i++) {
810
+ const k = variantKeys[i];
811
+ if (hasOwn.call(resolved, k)) ownVariants[k] = resolved[k];
812
+ }
813
+ // Lazy-init updatedVariants — many computeds only inspect `variants`
814
+ // or call setDefaultVariants for keys the user already set, so the
815
+ // copy is unnecessary in the common case.
816
+ let updatedVariants: Record<string, unknown> | null = null;
817
+ const localCClasses: ClassValue[] = [];
818
+ let localCStyle: StyleValue | null = null;
819
+ const ensureUpdated = (): Record<string, unknown> => {
820
+ if (updatedVariants) return updatedVariants;
821
+ const u: Record<string, unknown> = {};
822
+ Object.assign(u, ownVariants);
823
+ updatedVariants = u;
824
+ return u;
825
+ };
789
826
  const ctx = {
790
- variants: resolvedVariants as VariantValues<Record<string, unknown>>,
827
+ variants: ownVariants as VariantValues<Record<string, unknown>>,
791
828
  setVariants: (
792
829
  newVariants: VariantValues<Record<string, unknown>>,
793
830
  ) => {
794
- const filtered: Record<string, unknown> = {};
795
- filterDisabledInto(
796
- newVariants as Record<string, unknown>,
797
- filtered,
798
- );
799
- Object.assign(updatedVariants, filtered);
831
+ if (!hasAnyDisabled) {
832
+ Object.assign(ensureUpdated(), newVariants);
833
+ return;
834
+ }
835
+ for (const key in newVariants) {
836
+ if (!hasOwn.call(newVariants, key)) continue;
837
+ if (disabledVariantKeys.has(key)) continue;
838
+ const value = (newVariants as Record<string, unknown>)[key];
839
+ if (hasDisabledVariantValues) {
840
+ const valueKey = getVariantValueKey(value);
841
+ if (
842
+ valueKey != null &&
843
+ disabledVariantValues[key]?.has(valueKey)
844
+ ) {
845
+ continue;
846
+ }
847
+ }
848
+ ensureUpdated()[key] = value;
849
+ }
800
850
  },
801
851
  setDefaultVariants: (
802
852
  newDefaults: VariantValues<Record<string, unknown>>,
803
853
  ) => {
804
854
  for (const key in newDefaults) {
805
855
  if (!hasOwn.call(newDefaults, key)) continue;
806
- if (variantProps[key] !== undefined) continue;
807
- if (disabledVariantKeys.has(key)) continue;
856
+ if (userVariantProps[key] !== undefined) continue;
808
857
  const value = (newDefaults as Record<string, unknown>)[key];
809
- const valueKey = getVariantValueKey(value);
810
- if (
811
- valueKey != null &&
812
- disabledVariantValues[key]?.has(valueKey)
813
- ) {
814
- continue;
858
+ if (hasAnyDisabled) {
859
+ if (disabledVariantKeys.has(key)) continue;
860
+ const valueKey = getVariantValueKey(value);
861
+ if (
862
+ valueKey != null &&
863
+ disabledVariantValues[key]?.has(valueKey)
864
+ ) {
865
+ continue;
866
+ }
815
867
  }
816
- updatedVariants[key] = value;
868
+ ensureUpdated()[key] = value;
817
869
  }
818
870
  },
819
871
  addClass: (className: ClassValue) => {
820
- cClasses.push(className);
872
+ localCClasses.push(className);
821
873
  },
822
874
  addStyle: (newStyle: StyleValue) => {
823
- if (!cStyle) cStyle = {};
824
- assign(cStyle, newStyle);
875
+ if (!localCStyle) localCStyle = {};
876
+ Object.assign(localCStyle, newStyle);
825
877
  },
826
878
  };
827
879
  const result = computed(ctx);
828
880
  if (result != null) {
829
881
  const r = extractClassAndStylePrebuilt(result);
830
- if (r.class != null) cClasses.push(r.class);
882
+ if (r.class != null) localCClasses.push(r.class);
831
883
  if (r.style) {
832
- if (!cStyle) cStyle = {};
833
- assign(cStyle, r.style);
884
+ if (!localCStyle) localCStyle = {};
885
+ Object.assign(localCStyle, r.style);
834
886
  }
835
887
  }
836
- // Apply filterDisabled to updatedVariants
837
- const filteredUpdated: Record<string, unknown> = {};
838
- filterDisabledInto(updatedVariants, filteredUpdated);
839
- resolvedVariants = filteredUpdated;
840
- computedClassesArr = cClasses;
841
- computedStyleObj = cStyle;
842
- }
843
-
844
- // Compute skip-style sets for the extended components and current
845
- // component. Only allocate when needed.
846
- const hasSkipKeys = !!skipStyleKeysIn || disabledVariantKeys.size > 0;
847
- let currentVariantKeys: Set<string> | null = null;
848
- if (hasSkipKeys) {
849
- currentVariantKeys = new Set<string>();
850
- if (skipStyleKeysIn) {
851
- for (const k of skipStyleKeysIn) currentVariantKeys.add(k);
852
- }
853
- for (const k of disabledVariantKeys) currentVariantKeys.add(k);
854
- }
855
- // computedVariantKeys is currentVariantKeys + computedVariants names
856
- let computedVariantKeysSet: Set<string> | null = null;
857
- if (hasExtend) {
858
- if (currentVariantKeys || computedVariantNames.length > 0) {
859
- computedVariantKeysSet = new Set<string>();
860
- if (currentVariantKeys) {
861
- for (const k of currentVariantKeys) computedVariantKeysSet.add(k);
862
- }
863
- for (let i = 0; i < computedVariantNames.length; i++) {
864
- computedVariantKeysSet.add(computedVariantNames[i]);
888
+ cClasses = localCClasses;
889
+ cStyle = localCStyle;
890
+ if (updatedVariants) {
891
+ if (hasAnyDisabled) {
892
+ const filteredUpdated: Record<string, unknown> = {};
893
+ filterDisabledInto(updatedVariants, filteredUpdated);
894
+ workingResolved = filteredUpdated;
895
+ } else {
896
+ workingResolved = updatedVariants;
865
897
  }
866
898
  }
867
899
  }
868
900
 
869
- // computedVariantValues = mergeDisabledVariantValues(skipIn, disabledValues)
870
- let computedVariantValues: Record<string, Set<string>> | null = null;
871
- const hasInValues = !!skipStyleVariantValuesIn;
872
- const disabledValuesKeys = Object.keys(disabledVariantValues);
873
- const hasDisabledValues = disabledValuesKeys.length > 0;
874
- if (hasExtend && (hasInValues || hasDisabledValues)) {
875
- computedVariantValues = {};
876
- if (hasInValues) {
877
- for (const k in skipStyleVariantValuesIn) {
878
- if (!hasOwn.call(skipStyleVariantValuesIn, k)) continue;
879
- const set = new Set<string>();
880
- for (const v of skipStyleVariantValuesIn[k]) {
881
- set.add(v);
882
- }
883
- computedVariantValues[k] = set;
884
- }
901
+ // Build skip sets to pass to extends. Reuse precomputed values when no
902
+ // caller-provided sets need merging.
903
+ let extSkipKeys: Set<string> | null;
904
+ if (skipKeys === null) {
905
+ extSkipKeys = staticExtSkipKeys;
906
+ } else if (staticExtSkipKeys === null) {
907
+ extSkipKeys = skipKeys;
908
+ } else {
909
+ extSkipKeys = new Set(skipKeys);
910
+ for (const k of staticExtSkipKeys) extSkipKeys.add(k);
911
+ }
912
+
913
+ let extSkipVals: Record<string, Set<string>> | null;
914
+ if (skipValues === null) {
915
+ extSkipVals = staticExtSkipValues;
916
+ } else if (staticExtSkipValues === null) {
917
+ extSkipVals = skipValues;
918
+ } else {
919
+ extSkipVals = {};
920
+ for (const k in skipValues) {
921
+ extSkipVals[k] = skipValues[k];
885
922
  }
886
- for (let i = 0; i < disabledValuesKeys.length; i++) {
887
- const k = disabledValuesKeys[i];
888
- let bucket = computedVariantValues[k];
889
- if (!bucket) {
890
- bucket = new Set<string>();
891
- computedVariantValues[k] = bucket;
923
+ for (const k in staticExtSkipValues) {
924
+ const existing = extSkipVals[k];
925
+ if (existing) {
926
+ const merged = new Set<string>(existing);
927
+ for (const v of staticExtSkipValues[k]) merged.add(v);
928
+ extSkipVals[k] = merged;
929
+ } else {
930
+ extSkipVals[k] = staticExtSkipValues[k];
892
931
  }
893
- for (const v of disabledVariantValues[k]) bucket.add(v);
894
932
  }
895
933
  }
896
934
 
897
- // ----- Build classes/styles in proper order -----
898
- // 1. Extended base classes & their styles (with skip applied)
899
- // 2. Current base class & base style
900
- // 3. Extended variant classes
901
- // 4. Current variants
902
- // 5. computed results
903
- // 6. props.class / props.className
904
- // 7. props.style
905
- const allClasses: ClassValue[] = [];
906
- const allStyle: StyleValue = {};
907
-
908
- // Process extended components
935
+ // Run extends' contributions first (their full classes + styles) so our
936
+ // own base style and variants apply on top, matching the original
937
+ // ext1 ext2 current ordering.
938
+ //
939
+ // `workingResolved` is passed as the extends' `userVariantProps`. This
940
+ // is deliberate — by the time `compute` runs, the resolveDefaults chain
941
+ // and our own `computed`'s `setDefaultVariants` have already produced
942
+ // the most-specific resolution for every key. Treating those values as
943
+ // "user-provided" makes extends' own `setDefaultVariants` skip them, so
944
+ // extends emit variant classes that match what we resolved (rather than
945
+ // re-running their own defaults and emitting a different class).
946
+ // Replacing this with the original `userVariantProps` looks cleaner but
947
+ // breaks "child computed setDefaultVariants overrides parent computed
948
+ // setDefaultVariants" in `tests/computed.test.ts` — extends would then
949
+ // overwrite values the descendant already resolved.
909
950
  if (hasExtend) {
910
- const hasComputedVariantKeysSet =
911
- !!computedVariantKeysSet && computedVariantKeysSet.size > 0;
912
- const hasComputedVariantValues =
913
- !!computedVariantValues &&
914
- Object.keys(computedVariantValues).length > 0;
915
- const hasSkipForExt =
916
- hasComputedVariantKeysSet || hasComputedVariantValues;
917
-
918
- const extVariantClasses: ClassValue[] = [];
919
-
920
951
  for (let i = 0; i < extCount; i++) {
921
- const ext = extEntries[i];
922
- const extBaseClass = extBaseClassesArr[i];
923
- let propsForExt: Record<string | symbol, unknown>;
924
- if (hasSkipForExt) {
925
- propsForExt = {};
926
- // Copy resolvedVariants
927
- for (const k in resolvedVariants) {
928
- if (hasOwn.call(resolvedVariants, k)) {
929
- propsForExt[k] = resolvedVariants[k];
952
+ if (hasIsolatedExt && extIsolated[i]) {
953
+ // Isolated extend (different factory): gather its variant classes
954
+ // into a scratch array, then push the joined string after applying
955
+ // its own transformClass. Our outer transform applies on top.
956
+ const extClasses: ClsxClassValue[] = [];
957
+ extMetas[i].compute(
958
+ workingResolved,
959
+ workingResolved,
960
+ extSkipKeys,
961
+ extSkipVals,
962
+ extClasses,
963
+ styleOut,
964
+ );
965
+ if (extClasses.length > 0) {
966
+ const joined = clsx(extClasses);
967
+ if (joined.length > 0) {
968
+ classesOut.push(
969
+ extMetas[i].transformClass(joined) as ClsxClassValue,
970
+ );
930
971
  }
931
972
  }
932
- if (hasComputedVariantKeysSet) {
933
- propsForExt[SKIP_STYLE_KEYS] = computedVariantKeysSet;
934
- }
935
- if (hasComputedVariantValues) {
936
- propsForExt[SKIP_STYLE_VARIANT_VALUES] = computedVariantValues;
937
- }
938
973
  } else {
939
- propsForExt = resolvedVariants as Record<string | symbol, unknown>;
940
- }
941
-
942
- const extResult = ext(
943
- propsForExt as ComponentProps<Record<string, unknown>>,
944
- );
945
- // ext may be a modal component (.html / .htmlObj), whose style is a
946
- // CSS string or hyphen-keyed object — normalize before merging.
947
- if (extResult.style != null) {
948
- assign(allStyle, normalizeStyle(extResult.style));
974
+ extMetas[i].compute(
975
+ workingResolved,
976
+ workingResolved,
977
+ extSkipKeys,
978
+ extSkipVals,
979
+ classesOut,
980
+ styleOut,
981
+ );
949
982
  }
950
-
951
- allClasses.push(extBaseClass);
952
- const fullClass =
953
- "className" in extResult ? extResult.className : extResult.class;
954
- const variantPortion = extractVariantClasses(fullClass, extBaseClass);
955
- if (variantPortion) extVariantClasses.push(variantPortion);
956
- }
957
-
958
- // 2. Current base class
959
- allClasses.push(baseClass);
960
- if (baseStyle) assign(allStyle, baseStyle);
961
-
962
- // 4. Extended variant classes
963
- for (let i = 0; i < extVariantClasses.length; i++) {
964
- allClasses.push(extVariantClasses[i]);
965
983
  }
966
- } else {
967
- // No extends: just current base
968
- allClasses.push(baseClass);
969
- if (baseStyle) assign(allStyle, baseStyle);
970
984
  }
971
985
 
972
- // 5. Current component's variants (skip keys overridden)
973
- // Walk pre-built variant entries
986
+ // Apply own base style (after extends' styles, matching original order).
987
+ if (hasBaseStyle) Object.assign(styleOut, baseStyle);
988
+
989
+ // Apply own variants. Skip keys/values come from caller (e.g., parent
990
+ // wants its own computedVariants to override this variant).
991
+ // `variantEntryNames` already excludes disabled keys (those with `null`
992
+ // value in config), so we don't re-check `disabledVariantKeys` here.
993
+ const ownSkipKeys = skipKeys;
994
+ const ownSkipValues = skipValues;
974
995
  for (let i = 0; i < variantEntryCount; i++) {
975
996
  const variantName = variantEntryNames[i];
976
- const variant = variantEntryDefs[i];
977
- if (currentVariantKeys && currentVariantKeys.has(variantName)) continue;
978
- const selectedValue = resolvedVariants[variantName];
997
+ if (ownSkipKeys && ownSkipKeys.has(variantName)) continue;
998
+ const selectedValue = workingResolved[variantName];
979
999
  if (selectedValue === undefined) continue;
980
1000
  const selectedKey = getVariantValueKey(selectedValue);
981
- // disabled values from current config:
1001
+ const variant = variantEntryDefs[i];
982
1002
  if (
983
1003
  variant.disabledValues &&
984
1004
  selectedKey != null &&
@@ -986,13 +1006,10 @@ export function create({
986
1006
  ) {
987
1007
  continue;
988
1008
  }
989
- // skipVariantValues comes from skipStyleVariantValuesIn (only relevant
990
- // if this is being called as an extended component). For top-level it
991
- // would be undefined.
992
1009
  if (
993
- skipStyleVariantValuesIn &&
1010
+ ownSkipValues &&
994
1011
  selectedKey != null &&
995
- skipStyleVariantValuesIn[variantName]?.has(selectedKey)
1012
+ ownSkipValues[variantName]?.has(selectedKey)
996
1013
  ) {
997
1014
  continue;
998
1015
  }
@@ -1001,83 +1018,149 @@ export function create({
1001
1018
  if (selectedKey == null) continue;
1002
1019
  const v = variant.values[selectedKey];
1003
1020
  if (!v) continue;
1004
- if (v.class != null) allClasses.push(v.class);
1005
- if (v.style) assign(allStyle, v.style);
1006
- } else if (variant.shorthand) {
1007
- // shorthand: applies when selectedValue === true
1008
- if (selectedValue === true) {
1009
- const v = variant.shorthand;
1010
- if (v.class != null) allClasses.push(v.class);
1011
- if (v.style) assign(allStyle, v.style);
1012
- }
1021
+ if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1022
+ if (v.style) Object.assign(styleOut, v.style);
1023
+ } else if (variant.shorthand && selectedValue === true) {
1024
+ const v = variant.shorthand;
1025
+ if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1026
+ if (v.style) Object.assign(styleOut, v.style);
1013
1027
  }
1014
1028
  }
1015
1029
 
1016
- // computedVariants
1030
+ // Apply computedVariants.
1017
1031
  for (let i = 0; i < computedVariantCount; i++) {
1018
1032
  const variantName = computedVariantNames[i];
1019
- const fn = computedVariantFns[i];
1020
- if (currentVariantKeys && currentVariantKeys.has(variantName)) continue;
1021
- const selectedValue = resolvedVariants[variantName];
1033
+ if (ownSkipKeys && ownSkipKeys.has(variantName)) continue;
1034
+ const selectedValue = workingResolved[variantName];
1022
1035
  if (selectedValue === undefined) continue;
1023
1036
  const selectedKey = getVariantValueKey(selectedValue);
1024
1037
  if (
1025
- skipStyleVariantValuesIn &&
1038
+ ownSkipValues &&
1026
1039
  selectedKey != null &&
1027
- skipStyleVariantValuesIn[variantName]?.has(selectedKey)
1040
+ ownSkipValues[variantName]?.has(selectedKey)
1028
1041
  ) {
1029
1042
  continue;
1030
1043
  }
1044
+ const fn = computedVariantFns[i];
1031
1045
  const computedResult = fn(selectedValue);
1032
1046
  if (computedResult == null) continue;
1033
1047
  const r = extractClassAndStylePrebuilt(computedResult);
1034
- if (r.class != null) allClasses.push(r.class);
1035
- if (r.style) assign(allStyle, r.style);
1048
+ if (r.class != null) classesOut.push(r.class as ClsxClassValue);
1049
+ if (r.style) Object.assign(styleOut, r.style);
1036
1050
  }
1037
1051
 
1038
- // computed function results
1039
- if (computedClassesArr) {
1040
- for (let i = 0; i < computedClassesArr.length; i++) {
1041
- allClasses.push(computedClassesArr[i]);
1052
+ // Apply `computed` results — must come after own variants/computedVariants.
1053
+ if (cClasses) {
1054
+ for (let i = 0; i < cClasses.length; i++) {
1055
+ classesOut.push(cClasses[i] as ClsxClassValue);
1056
+ }
1057
+ }
1058
+ if (cStyle) Object.assign(styleOut, cStyle);
1059
+ };
1060
+
1061
+ // Top-level: resolves variants from user props, calls compute, then
1062
+ // assembles className and style with user-provided class/style overrides.
1063
+ const computeResult = (
1064
+ props: ComponentProps<MergedVariants> = EMPTY_DEFAULTS as ComponentProps<MergedVariants>,
1065
+ ): { className: string; style: StyleValue } => {
1066
+ const propsRecord = props as Record<string, unknown>;
1067
+
1068
+ // Inline resolve: avoids allocating a separate variantProps object for
1069
+ // the common case where no extends need a resolveDefaults pass.
1070
+ // resolveVariantsHot would also work here but assumes its input is
1071
+ // variant-only (it uses for-in for speed).
1072
+ let resolved: Record<string, unknown> = {};
1073
+ Object.assign(resolved, staticDefaults);
1074
+
1075
+ let userVariantProps: Record<string, unknown>;
1076
+ if (extMetasWithResolveDefaultsCount > 0) {
1077
+ // Some extends need a resolveDefaults pass. They expect a variant-only
1078
+ // object as `userProps`, so we extract one.
1079
+ const variantProps: Record<string, unknown> = {};
1080
+ for (let i = 0; i < variantKeysLength; i++) {
1081
+ const key = variantKeys[i];
1082
+ if (hasOwn.call(propsRecord, key)) {
1083
+ variantProps[key] = propsRecord[key];
1084
+ }
1085
+ }
1086
+ for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
1087
+ const meta = extMetasWithResolveDefaults[i];
1088
+ const extComputed = meta.resolveDefaults!(resolved, variantProps);
1089
+ for (const k in extComputed) {
1090
+ if (!hasOwn.call(extComputed, k)) continue;
1091
+ resolved[k] = extComputed[k];
1092
+ }
1093
+ }
1094
+ for (const k in variantProps) {
1095
+ if (!hasOwn.call(variantProps, k)) continue;
1096
+ const v = variantProps[k];
1097
+ if (v === undefined) continue;
1098
+ resolved[k] = v;
1042
1099
  }
1100
+ userVariantProps = variantProps;
1101
+ } else {
1102
+ // Fast path: walk variantKeys directly against propsRecord. Use
1103
+ // hasOwn so a polluted Object.prototype can't introduce variant
1104
+ // values the user didn't pass.
1105
+ for (let i = 0; i < variantKeysLength; i++) {
1106
+ const key = variantKeys[i];
1107
+ if (!hasOwn.call(propsRecord, key)) continue;
1108
+ const v = propsRecord[key];
1109
+ if (v === undefined) continue;
1110
+ resolved[key] = v;
1111
+ }
1112
+ userVariantProps = propsRecord;
1043
1113
  }
1044
- if (computedStyleObj) assign(allStyle, computedStyleObj);
1045
1114
 
1046
- // props.class / props.className
1047
- if ("class" in props)
1048
- allClasses.push((props as { class: ClassValue }).class);
1049
- if ("className" in props)
1050
- allClasses.push((props as { className: ClassValue }).className);
1115
+ if (hasAnyDisabled) {
1116
+ const filtered: Record<string, unknown> = {};
1117
+ filterDisabledInto(resolved, filtered);
1118
+ resolved = filtered;
1119
+ }
1120
+
1121
+ // Build allClasses directly. computedBaseClass already has all extend
1122
+ // bases joined with config.class — `compute` only adds variant classes
1123
+ // on top.
1124
+ const allClasses: ClsxClassValue[] = [computedBaseClass];
1125
+ const style: StyleValue = {};
1126
+ compute(resolved, userVariantProps, null, null, allClasses, style);
1127
+
1128
+ // Apply user-provided class / className.
1129
+ if ("class" in propsRecord) {
1130
+ allClasses.push(propsRecord.class as ClsxClassValue);
1131
+ }
1132
+ if ("className" in propsRecord) {
1133
+ allClasses.push(propsRecord.className as ClsxClassValue);
1134
+ }
1051
1135
 
1052
- // props.style
1053
- const psv = (props as { style?: unknown }).style;
1136
+ // Apply user-provided style.
1137
+ const psv = propsRecord.style;
1054
1138
  if (psv != null) {
1055
- // Fast path: if it's an object with no keys, skip
1056
1139
  if (typeof psv === "string") {
1057
1140
  if (psv.length > 0) {
1058
- assign(allStyle, htmlStyleToStyleValue(psv));
1141
+ Object.assign(style, htmlStyleToStyleValue(psv));
1059
1142
  }
1060
1143
  } else if (typeof psv === "object") {
1061
- // Could be HTMLObj or JSX form. Don't allocate when empty.
1144
+ // Don't allocate when empty.
1062
1145
  let hasAnyKey = false;
1063
1146
  for (const _ in psv) {
1064
1147
  hasAnyKey = true;
1065
1148
  break;
1066
1149
  }
1067
1150
  if (hasAnyKey) {
1068
- assign(allStyle, normalizeStyle(psv));
1151
+ Object.assign(style, normalizeStyle(psv));
1069
1152
  }
1070
1153
  }
1071
1154
  }
1072
1155
 
1073
1156
  return {
1074
- className: cx(...(allClasses as ClsxClassValue[])),
1075
- style: allStyle,
1157
+ className: transformClass(clsx(allClasses)),
1158
+ style,
1076
1159
  };
1077
1160
  };
1078
1161
 
1079
1162
  const getVariants = (variants?: VariantValues<MergedVariants>) => {
1080
- const variantProps = (variants ?? {}) as Record<string, unknown>;
1163
+ const variantProps = variants ?? EMPTY_DEFAULTS;
1081
1164
  let resolvedVariants = resolveVariantsHot(variantProps);
1082
1165
  // Run computed function to get variants set via setVariants and
1083
1166
  // setDefaultVariants
@@ -1089,12 +1172,25 @@ export function create({
1089
1172
  setVariants: (
1090
1173
  newVariants: VariantValues<Record<string, unknown>>,
1091
1174
  ) => {
1092
- const filtered: Record<string, unknown> = {};
1093
- filterDisabledInto(
1094
- newVariants as Record<string, unknown>,
1095
- filtered,
1096
- );
1097
- Object.assign(updatedVariants, filtered);
1175
+ if (!hasAnyDisabled) {
1176
+ Object.assign(updatedVariants, newVariants);
1177
+ return;
1178
+ }
1179
+ for (const key in newVariants) {
1180
+ if (!hasOwn.call(newVariants, key)) continue;
1181
+ if (disabledVariantKeys.has(key)) continue;
1182
+ const value = (newVariants as Record<string, unknown>)[key];
1183
+ if (hasDisabledVariantValues) {
1184
+ const valueKey = getVariantValueKey(value);
1185
+ if (
1186
+ valueKey != null &&
1187
+ disabledVariantValues[key]?.has(valueKey)
1188
+ ) {
1189
+ continue;
1190
+ }
1191
+ }
1192
+ updatedVariants[key] = value;
1193
+ }
1098
1194
  },
1099
1195
  setDefaultVariants: (
1100
1196
  newDefaults: VariantValues<Record<string, unknown>>,
@@ -1102,33 +1198,42 @@ export function create({
1102
1198
  for (const key in newDefaults) {
1103
1199
  if (!hasOwn.call(newDefaults, key)) continue;
1104
1200
  if (variantProps[key] !== undefined) continue;
1105
- if (disabledVariantKeys.has(key)) continue;
1106
1201
  const value = (newDefaults as Record<string, unknown>)[key];
1107
- const valueKey = getVariantValueKey(value);
1108
- if (
1109
- valueKey != null &&
1110
- disabledVariantValues[key]?.has(valueKey)
1111
- ) {
1112
- continue;
1202
+ if (hasAnyDisabled) {
1203
+ if (disabledVariantKeys.has(key)) continue;
1204
+ const valueKey = getVariantValueKey(value);
1205
+ if (
1206
+ valueKey != null &&
1207
+ disabledVariantValues[key]?.has(valueKey)
1208
+ ) {
1209
+ continue;
1210
+ }
1113
1211
  }
1114
1212
  updatedVariants[key] = value;
1115
1213
  }
1116
1214
  },
1117
- addClass: () => {},
1118
- addStyle: () => {},
1215
+ addClass: noop,
1216
+ addStyle: noop,
1119
1217
  };
1120
1218
  computed(ctx);
1121
- const filteredUpdated: Record<string, unknown> = {};
1122
- filterDisabledInto(updatedVariants, filteredUpdated);
1123
- resolvedVariants = filteredUpdated;
1219
+ if (hasAnyDisabled) {
1220
+ const filteredUpdated: Record<string, unknown> = {};
1221
+ filterDisabledInto(updatedVariants, filteredUpdated);
1222
+ resolvedVariants = filteredUpdated;
1223
+ } else {
1224
+ resolvedVariants = updatedVariants;
1225
+ }
1124
1226
  }
1125
1227
  return resolvedVariants as VariantValues<MergedVariants>;
1126
1228
  };
1127
1229
 
1128
- // Compute base class (without variants) - includes extended base classes.
1129
- // Reuses `extBaseClassesArr` (built earlier) so we don't walk `extend` and
1130
- // call `getComponentMeta` a second time.
1131
- const computedBaseClass = cx(
1230
+ // Compute base class (without variants) includes extended base classes.
1231
+ // Plain `clsx` (no `transformClass`): `meta.baseClass` flows back into
1232
+ // parent extends as `clsx` input and then through the single
1233
+ // `transformClass(clsx(allClasses))` at render time, so applying it here
1234
+ // would compound (double for own-render, triple+ for extend chains) and
1235
+ // misbehave for non-idempotent transforms.
1236
+ const computedBaseClass = clsx(
1132
1237
  ...(extBaseClassesArr as ClsxClassValue[]),
1133
1238
  config.class as ClsxClassValue,
1134
1239
  );
@@ -1141,6 +1246,8 @@ export function create({
1141
1246
  baseClass: computedBaseClass,
1142
1247
  staticDefaults,
1143
1248
  resolveDefaults: resolveDefaultsFn,
1249
+ compute,
1250
+ transformClass,
1144
1251
  };
1145
1252
 
1146
1253
  const initComponent = <
@@ -1213,4 +1320,6 @@ export function create({
1213
1320
  return { cv, cx };
1214
1321
  }
1215
1322
 
1323
+ function noop() {}
1324
+
1216
1325
  export const { cv, cx } = create();