clava 0.2.4 → 0.3.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/src/index.ts CHANGED
@@ -42,7 +42,26 @@ type ComputeFn = (
42
42
  skipValues: Record<string, Set<string>> | null,
43
43
  classesOut: ClsxClassValue[],
44
44
  styleOut: StyleValue,
45
- ) => void;
45
+ runState?: ComputedRunState,
46
+ protectedVariants?: Record<string, unknown> | null,
47
+ pendingProtectedVariants?: Record<string, unknown> | null,
48
+ protectedVariantKeys?: Set<string> | null,
49
+ ) => Record<string, unknown>;
50
+
51
+ type ResolveComputedFn = (
52
+ resolved: Record<string, unknown>,
53
+ userVariantProps: Record<string, unknown>,
54
+ filterOwnVariants?: boolean,
55
+ runState?: ComputedRunState,
56
+ protectedVariants?: Record<string, unknown> | null,
57
+ pendingProtectedVariants?: Record<string, unknown> | null,
58
+ protectedVariantKeys?: Set<string> | null,
59
+ ) => Record<string, unknown>;
60
+
61
+ interface ComputedRunState {
62
+ remaining: number;
63
+ warned: boolean;
64
+ }
46
65
 
47
66
  // Internal metadata stored on components but hidden from public types.
48
67
  interface ComponentMeta {
@@ -60,6 +79,7 @@ interface ComponentMeta {
60
79
  // Returns variant classes + style for this component, used by extending
61
80
  // components. Top-level rendering also routes through this.
62
81
  compute: ComputeFn;
82
+ resolveComputed: ResolveComputedFn | null;
63
83
  // Reference identity is used to detect mixed-factory `extend`. When a
64
84
  // component is extended by a parent from a different `create()` call, the
65
85
  // parent applies this transform to the extend's contribution before joining,
@@ -69,14 +89,81 @@ interface ComponentMeta {
69
89
 
70
90
  const META_KEY = "__meta";
71
91
 
72
- // eslint-disable-next-line @typescript-eslint/unbound-method
73
- const hasOwn = Object.prototype.hasOwnProperty;
74
-
75
92
  const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
76
93
  string,
77
94
  unknown
78
95
  >;
79
96
 
97
+ const MAX_COMPUTED_RUNS = 50;
98
+
99
+ function areVariantsEqual(
100
+ a: Record<string, unknown>,
101
+ b: Record<string, unknown>,
102
+ ): boolean {
103
+ for (const key in a) {
104
+ if (!Object.hasOwn(a, key)) continue;
105
+ if (!Object.is(a[key], b[key])) return false;
106
+ }
107
+ for (const key in b) {
108
+ if (!Object.hasOwn(b, key)) continue;
109
+ if (!Object.hasOwn(a, key)) return false;
110
+ }
111
+ return true;
112
+ }
113
+
114
+ function warnComputedLimit(runState: ComputedRunState): void {
115
+ if (runState.warned) return;
116
+ runState.warned = true;
117
+ if (process.env.NODE_ENV !== "production") {
118
+ console.warn(
119
+ "Clava: Maximum computed update iterations exceeded. This can happen " +
120
+ "when a computed callback calls setVariants or setDefaultVariants, " +
121
+ "but one of the variants changes on every run.",
122
+ );
123
+ }
124
+ }
125
+
126
+ function getExtUserVariantProps(
127
+ userVariantProps: Record<string, unknown>,
128
+ protectedVariants: Record<string, unknown> | null,
129
+ changedVariants: Record<string, unknown> | null,
130
+ ): Record<string, unknown> {
131
+ const extUserVariantProps: Record<string, unknown> = {};
132
+ Object.assign(extUserVariantProps, userVariantProps);
133
+ if (protectedVariants) {
134
+ Object.assign(extUserVariantProps, protectedVariants);
135
+ }
136
+ if (changedVariants) {
137
+ Object.assign(extUserVariantProps, changedVariants);
138
+ }
139
+ return extUserVariantProps;
140
+ }
141
+
142
+ function mergeVariants(
143
+ target: Record<string, unknown>,
144
+ source: Record<string, unknown>,
145
+ skipKeys?: Set<string> | null,
146
+ ): boolean {
147
+ let changed = false;
148
+ if (!skipKeys || skipKeys.size === 0) {
149
+ for (const key in source) {
150
+ if (!Object.hasOwn(source, key)) continue;
151
+ const value = source[key];
152
+ if (!Object.is(target[key], value)) changed = true;
153
+ target[key] = value;
154
+ }
155
+ return changed;
156
+ }
157
+ for (const key in source) {
158
+ if (!Object.hasOwn(source, key)) continue;
159
+ if (skipKeys.has(key)) continue;
160
+ const value = source[key];
161
+ if (!Object.is(target[key], value)) changed = true;
162
+ target[key] = value;
163
+ }
164
+ return changed;
165
+ }
166
+
80
167
  // Dynamic property access on function requires cast through unknown.
81
168
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
82
169
  return (component as unknown as Record<string, unknown>)[META_KEY] as
@@ -210,7 +297,7 @@ function collectVariantKeys(
210
297
 
211
298
  if (config.variants) {
212
299
  for (const key in config.variants) {
213
- if (!hasOwn.call(config.variants, key)) continue;
300
+ if (!Object.hasOwn(config.variants, key)) continue;
214
301
  const variant = (config.variants as Record<string, unknown>)[key];
215
302
  if (variant === null) {
216
303
  keys.delete(key);
@@ -222,7 +309,7 @@ function collectVariantKeys(
222
309
 
223
310
  if (config.computedVariants) {
224
311
  for (const key in config.computedVariants) {
225
- if (!hasOwn.call(config.computedVariants, key)) continue;
312
+ if (!Object.hasOwn(config.computedVariants, key)) continue;
226
313
  keys.add(key);
227
314
  }
228
315
  }
@@ -262,7 +349,7 @@ function collectDisabledVariantKeys(
262
349
  const keys = new Set<string>();
263
350
  if (!config.variants) return keys;
264
351
  for (const key in config.variants) {
265
- if (!hasOwn.call(config.variants, key)) continue;
352
+ if (!Object.hasOwn(config.variants, key)) continue;
266
353
  if ((config.variants as Record<string, unknown>)[key] === null) {
267
354
  keys.add(key);
268
355
  }
@@ -276,12 +363,12 @@ function collectDisabledVariantValues(
276
363
  const values: Record<string, Set<string>> = {};
277
364
  if (!config.variants) return values;
278
365
  for (const key in config.variants) {
279
- if (!hasOwn.call(config.variants, key)) continue;
366
+ if (!Object.hasOwn(config.variants, key)) continue;
280
367
  const variant = (config.variants as Record<string, unknown>)[key];
281
368
  if (!isRecordObject(variant)) continue;
282
369
  let bucket: Set<string> | undefined;
283
370
  for (const variantValue in variant) {
284
- if (!hasOwn.call(variant, variantValue)) continue;
371
+ if (!Object.hasOwn(variant, variantValue)) continue;
285
372
  if (variant[variantValue] !== null) continue;
286
373
  if (!bucket) {
287
374
  bucket = new Set<string>();
@@ -294,13 +381,13 @@ function collectDisabledVariantValues(
294
381
  }
295
382
 
296
383
  interface NormalizedSource {
297
- keys: string[];
384
+ propKeys: string[];
298
385
  variantKeys: string[];
299
386
  isComponent: boolean;
300
387
  }
301
388
 
302
389
  const EMPTY_SOURCE: NormalizedSource = {
303
- keys: [],
390
+ propKeys: [],
304
391
  variantKeys: [],
305
392
  isComponent: false,
306
393
  };
@@ -308,7 +395,7 @@ const EMPTY_SOURCE: NormalizedSource = {
308
395
  function normalizeKeySource(source: unknown): NormalizedSource {
309
396
  if (Array.isArray(source)) {
310
397
  return {
311
- keys: source as string[],
398
+ propKeys: source as string[],
312
399
  variantKeys: source as string[],
313
400
  isComponent: false,
314
401
  };
@@ -318,17 +405,14 @@ function normalizeKeySource(source: unknown): NormalizedSource {
318
405
  if (typeof source !== "object" && typeof source !== "function") {
319
406
  return EMPTY_SOURCE;
320
407
  }
321
- if (!("keys" in source)) return EMPTY_SOURCE;
322
- if (!("variantKeys" in source)) return EMPTY_SOURCE;
408
+ const typed = source as Record<string, unknown>;
409
+ if (typeof typed.getVariants !== "function") return EMPTY_SOURCE;
410
+ if (!Array.isArray(typed.propKeys)) return EMPTY_SOURCE;
411
+ if (!Array.isArray(typed.variantKeys)) return EMPTY_SOURCE;
323
412
 
324
- // Component-provided arrays are immutable metadata — reference directly.
325
- const typed = source as {
326
- keys: string[];
327
- variantKeys: string[];
328
- };
329
413
  return {
330
- keys: typed.keys,
331
- variantKeys: typed.variantKeys,
414
+ propKeys: typed.propKeys as string[],
415
+ variantKeys: typed.variantKeys as string[],
332
416
  isComponent: true,
333
417
  };
334
418
  }
@@ -368,7 +452,9 @@ function splitPropsImpl(
368
452
  const sourceResult: Record<string, unknown> = {};
369
453
 
370
454
  const effectiveKeys =
371
- source.isComponent && stylingClaimed ? source.variantKeys : source.keys;
455
+ source.isComponent && stylingClaimed
456
+ ? source.variantKeys
457
+ : source.propKeys;
372
458
 
373
459
  const effectiveKeysLength = effectiveKeys.length;
374
460
  for (let i = 0; i < effectiveKeysLength; i++) {
@@ -422,7 +508,7 @@ export const splitProps: SplitPropsFunction = ((
422
508
  ) => {
423
509
  const normalizedSource1 = normalizeKeySource(source1);
424
510
  return splitPropsImpl(
425
- normalizedSource1.keys,
511
+ normalizedSource1.propKeys,
426
512
  normalizedSource1.isComponent,
427
513
  props,
428
514
  sources,
@@ -455,7 +541,7 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
455
541
  const values: Record<string, PrebuiltValue> = {};
456
542
  let disabledValues: Set<string> | null = null;
457
543
  for (const key in variantDef) {
458
- if (!hasOwn.call(variantDef, key)) continue;
544
+ if (!Object.hasOwn(variantDef, key)) continue;
459
545
  const value = variantDef[key];
460
546
  if (value === null) {
461
547
  if (!disabledValues) disabledValues = new Set<string>();
@@ -514,7 +600,7 @@ export function create({
514
600
  const variantEntryDefs: PrebuiltVariant[] = [];
515
601
  if (variants) {
516
602
  for (const name in variants) {
517
- if (!hasOwn.call(variants, name)) continue;
603
+ if (!Object.hasOwn(variants, name)) continue;
518
604
  const variant = (variants as Record<string, unknown>)[name];
519
605
  if (variant === null) continue;
520
606
  variantEntryNames.push(name);
@@ -528,7 +614,7 @@ export function create({
528
614
  const computedVariantFns: Array<(value: unknown) => unknown> = [];
529
615
  if (computedVariantsCfg) {
530
616
  for (const name in computedVariantsCfg) {
531
- if (!hasOwn.call(computedVariantsCfg, name)) continue;
617
+ if (!Object.hasOwn(computedVariantsCfg, name)) continue;
532
618
  computedVariantNames.push(name);
533
619
  computedVariantFns.push(
534
620
  (computedVariantsCfg as Record<string, (value: unknown) => unknown>)[
@@ -553,11 +639,11 @@ export function create({
553
639
  }
554
640
  if (variants) {
555
641
  for (const name in variants) {
556
- if (!hasOwn.call(variants, name)) continue;
642
+ if (!Object.hasOwn(variants, name)) continue;
557
643
  const variantDef = (variants as Record<string, unknown>)[name];
558
644
  if (!isRecordObject(variantDef)) continue;
559
645
  if (
560
- hasOwn.call(variantDef, "false") &&
646
+ Object.hasOwn(variantDef, "false") &&
561
647
  staticDefaults[name] === undefined
562
648
  ) {
563
649
  staticDefaults[name] = false;
@@ -570,7 +656,7 @@ export function create({
570
656
  if (hasAnyDisabled) {
571
657
  // Filter disabled variants in-place
572
658
  for (const key in staticDefaults) {
573
- if (!hasOwn.call(staticDefaults, key)) continue;
659
+ if (!Object.hasOwn(staticDefaults, key)) continue;
574
660
  if (disabledVariantKeys.has(key)) {
575
661
  delete staticDefaults[key];
576
662
  continue;
@@ -596,7 +682,7 @@ export function create({
596
682
  const extBaseClassesArr: string[] = [];
597
683
  const extIsolated: boolean[] = [];
598
684
  let hasIsolatedExt = false;
599
- if (extend) {
685
+ if (hasExtend) {
600
686
  for (const ext of extend) {
601
687
  const meta = getComponentMeta(ext);
602
688
  if (!meta) continue;
@@ -616,16 +702,18 @@ export function create({
616
702
  }
617
703
  const extCount = extMetas.length;
618
704
 
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[] = [];
705
+ // Filter to only extends with computed work in their chain. `resolveDefaults`
706
+ // and `resolveComputed` are populated from the same transitive condition,
707
+ // so one bucket is enough for both resolver paths.
708
+ const extMetasWithComputed: ComponentMeta[] = [];
623
709
  for (let i = 0; i < extCount; i++) {
624
- if (extMetas[i].resolveDefaults) {
625
- extMetasWithResolveDefaults.push(extMetas[i]);
710
+ const meta = extMetas[i];
711
+ if (meta.resolveDefaults) {
712
+ extMetasWithComputed.push(meta);
626
713
  }
627
714
  }
628
- const extMetasWithResolveDefaultsCount = extMetasWithResolveDefaults.length;
715
+ const extMetasWithComputedCount = extMetasWithComputed.length;
716
+ const shouldCollectChangedVariants = extMetasWithComputedCount > 0;
629
717
 
630
718
  // Pre-compute static skip key/value sets to pass to extends. These never
631
719
  // change across calls — when caller passes no skip sets, we reuse the same
@@ -652,12 +740,12 @@ export function create({
652
740
  ): void {
653
741
  if (!hasAnyDisabled) {
654
742
  for (const key in input) {
655
- if (hasOwn.call(input, key)) out[key] = input[key];
743
+ if (Object.hasOwn(input, key)) out[key] = input[key];
656
744
  }
657
745
  return;
658
746
  }
659
747
  for (const key in input) {
660
- if (!hasOwn.call(input, key)) continue;
748
+ if (!Object.hasOwn(input, key)) continue;
661
749
  if (disabledVariantKeys.has(key)) continue;
662
750
  const value = input[key];
663
751
  if (hasDisabledVariantValues) {
@@ -677,7 +765,7 @@ export function create({
677
765
  // When this component has no `computed` and no `extend` with work, the
678
766
  // function is null — callers can skip iterating it entirely.
679
767
  const resolveDefaultsFn: ComponentMeta["resolveDefaults"] =
680
- computed || extMetasWithResolveDefaultsCount > 0
768
+ computed || extMetasWithComputedCount > 0
681
769
  ? (
682
770
  childDefaults: Record<string, unknown>,
683
771
  userProps: Record<string, unknown> = EMPTY_DEFAULTS,
@@ -687,13 +775,13 @@ export function create({
687
775
  const resolvedVariants: Record<string, unknown> = {};
688
776
  Object.assign(resolvedVariants, staticDefaults);
689
777
  for (const key in childDefaults) {
690
- if (!hasOwn.call(childDefaults, key)) continue;
778
+ if (!Object.hasOwn(childDefaults, key)) continue;
691
779
  const v = childDefaults[key];
692
780
  if (v === undefined) continue;
693
781
  resolvedVariants[key] = v;
694
782
  }
695
783
  for (const key in userProps) {
696
- if (!hasOwn.call(userProps, key)) continue;
784
+ if (!Object.hasOwn(userProps, key)) continue;
697
785
  const v = userProps[key];
698
786
  if (v === undefined) continue;
699
787
  resolvedVariants[key] = v;
@@ -701,11 +789,13 @@ export function create({
701
789
 
702
790
  const computedDefaults: Record<string, unknown> = {};
703
791
 
704
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
705
- const extDefaults = extMetasWithResolveDefaults[i]
706
- .resolveDefaults!(childDefaults, userProps);
792
+ for (let i = 0; i < extMetasWithComputedCount; i++) {
793
+ const extDefaults = extMetasWithComputed[i].resolveDefaults!(
794
+ childDefaults,
795
+ userProps,
796
+ );
707
797
  for (const k in extDefaults) {
708
- if (!hasOwn.call(extDefaults, k)) continue;
798
+ if (!Object.hasOwn(extDefaults, k)) continue;
709
799
  computedDefaults[k] = extDefaults[k];
710
800
  }
711
801
  }
@@ -718,7 +808,7 @@ export function create({
718
808
  const ownVariants: Record<string, unknown> = {};
719
809
  for (let i = 0; i < variantKeysLength; i++) {
720
810
  const k = variantKeys[i];
721
- if (hasOwn.call(resolvedVariants, k)) {
811
+ if (Object.hasOwn(resolvedVariants, k)) {
722
812
  ownVariants[k] = resolvedVariants[k];
723
813
  }
724
814
  }
@@ -727,7 +817,7 @@ export function create({
727
817
  setVariants: noop,
728
818
  setDefaultVariants: (newDefaults) => {
729
819
  for (const key in newDefaults) {
730
- if (!hasOwn.call(newDefaults, key)) continue;
820
+ if (!Object.hasOwn(newDefaults, key)) continue;
731
821
  const value = (newDefaults as Record<string, unknown>)[key];
732
822
  if (userProps[key] !== undefined) continue;
733
823
  if (isVariantDisabled(config, key)) continue;
@@ -755,11 +845,11 @@ export function create({
755
845
 
756
846
  // Apply computed defaults from extended components (only those that have
757
847
  // actual work to do).
758
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
759
- const meta = extMetasWithResolveDefaults[i];
848
+ for (let i = 0; i < extMetasWithComputedCount; i++) {
849
+ const meta = extMetasWithComputed[i];
760
850
  const extComputed = meta.resolveDefaults!(defaults, propsVariants);
761
851
  for (const k in extComputed) {
762
- if (!hasOwn.call(extComputed, k)) continue;
852
+ if (!Object.hasOwn(extComputed, k)) continue;
763
853
  defaults[k] = extComputed[k];
764
854
  }
765
855
  }
@@ -768,7 +858,7 @@ export function create({
768
858
  // contractually variant-only here — callers building from a full props
769
859
  // object filter to variant keys before calling.
770
860
  for (const k in propsVariants) {
771
- if (!hasOwn.call(propsVariants, k)) continue;
861
+ if (!Object.hasOwn(propsVariants, k)) continue;
772
862
  const v = propsVariants[k];
773
863
  if (v === undefined) continue;
774
864
  defaults[k] = v;
@@ -782,39 +872,44 @@ export function create({
782
872
  return result;
783
873
  }
784
874
 
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.
875
+ const runComputedContext = (
876
+ resolved: Record<string, unknown>,
877
+ userVariantProps: Record<string, unknown>,
878
+ filterOwnVariants: boolean,
879
+ collectOutput: boolean,
880
+ protectedVariants: Record<string, unknown> | null | undefined,
881
+ pendingProtectedVariants: Record<string, unknown> | null | undefined,
882
+ protectedVariantKeys: Set<string> | null | undefined,
883
+ ): {
884
+ workingResolved: Record<string, unknown>;
885
+ changedVariants: Record<string, unknown> | null;
886
+ classes: ClassValue[] | null;
887
+ style: StyleValue | null;
888
+ } => {
799
889
  let workingResolved = resolved;
800
890
  let cClasses: ClassValue[] | null = null;
801
891
  let cStyle: StyleValue | null = null;
892
+ let changedVariants: Record<string, unknown> | null = null;
802
893
 
803
894
  if (computed) {
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];
895
+ let ownVariants = resolved;
896
+ if (filterOwnVariants) {
897
+ // When this component is being extended, `resolved` is the parent's
898
+ // workingResolved (a superset of our variant keys). Filter to our own
899
+ // keys for `ctx.variants` so the user's `computed` callback sees the
900
+ // shape declared by `VariantValues<V>` and not foreign parent keys.
901
+ const filteredVariants: Record<string, unknown> = {};
902
+ for (let i = 0; i < variantKeysLength; i++) {
903
+ const k = variantKeys[i];
904
+ if (Object.hasOwn(resolved, k)) filteredVariants[k] = resolved[k];
905
+ }
906
+ ownVariants = filteredVariants;
812
907
  }
813
908
  // Lazy-init updatedVariants — many computeds only inspect `variants`
814
909
  // or call setDefaultVariants for keys the user already set, so the
815
910
  // copy is unnecessary in the common case.
816
911
  let updatedVariants: Record<string, unknown> | null = null;
817
- const localCClasses: ClassValue[] = [];
912
+ const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
818
913
  let localCStyle: StyleValue | null = null;
819
914
  const ensureUpdated = (): Record<string, unknown> => {
820
915
  if (updatedVariants) return updatedVariants;
@@ -823,17 +918,40 @@ export function create({
823
918
  updatedVariants = u;
824
919
  return u;
825
920
  };
921
+ const setChangedVariant = (
922
+ key: string,
923
+ value: unknown,
924
+ protect = false,
925
+ ) => {
926
+ if (shouldCollectChangedVariants) {
927
+ if (!changedVariants) changedVariants = {};
928
+ changedVariants[key] = value;
929
+ }
930
+ if (protect && protectedVariants) {
931
+ protectedVariants[key] = value;
932
+ protectedVariantKeys?.add(key);
933
+ }
934
+ };
935
+ const getCurrentVariantValue = (key: string) => {
936
+ return updatedVariants ? updatedVariants[key] : ownVariants[key];
937
+ };
826
938
  const ctx = {
827
939
  variants: ownVariants as VariantValues<Record<string, unknown>>,
828
940
  setVariants: (
829
941
  newVariants: VariantValues<Record<string, unknown>>,
830
942
  ) => {
831
943
  if (!hasAnyDisabled) {
832
- Object.assign(ensureUpdated(), newVariants);
944
+ for (const key in newVariants) {
945
+ if (!Object.hasOwn(newVariants, key)) continue;
946
+ const value = (newVariants as Record<string, unknown>)[key];
947
+ setChangedVariant(key, value, true);
948
+ if (getCurrentVariantValue(key) === value) continue;
949
+ ensureUpdated()[key] = value;
950
+ }
833
951
  return;
834
952
  }
835
953
  for (const key in newVariants) {
836
- if (!hasOwn.call(newVariants, key)) continue;
954
+ if (!Object.hasOwn(newVariants, key)) continue;
837
955
  if (disabledVariantKeys.has(key)) continue;
838
956
  const value = (newVariants as Record<string, unknown>)[key];
839
957
  if (hasDisabledVariantValues) {
@@ -845,6 +963,8 @@ export function create({
845
963
  continue;
846
964
  }
847
965
  }
966
+ setChangedVariant(key, value, true);
967
+ if (getCurrentVariantValue(key) === value) continue;
848
968
  ensureUpdated()[key] = value;
849
969
  }
850
970
  },
@@ -852,8 +972,9 @@ export function create({
852
972
  newDefaults: VariantValues<Record<string, unknown>>,
853
973
  ) => {
854
974
  for (const key in newDefaults) {
855
- if (!hasOwn.call(newDefaults, key)) continue;
975
+ if (!Object.hasOwn(newDefaults, key)) continue;
856
976
  if (userVariantProps[key] !== undefined) continue;
977
+ if (protectedVariantKeys?.has(key)) continue;
857
978
  const value = (newDefaults as Record<string, unknown>)[key];
858
979
  if (hasAnyDisabled) {
859
980
  if (disabledVariantKeys.has(key)) continue;
@@ -865,21 +986,27 @@ export function create({
865
986
  continue;
866
987
  }
867
988
  }
989
+ setChangedVariant(key, value);
990
+ if (pendingProtectedVariants) {
991
+ pendingProtectedVariants[key] = value;
992
+ }
993
+ if (getCurrentVariantValue(key) === value) continue;
868
994
  ensureUpdated()[key] = value;
869
995
  }
870
996
  },
871
997
  addClass: (className: ClassValue) => {
872
- localCClasses.push(className);
998
+ localCClasses?.push(className);
873
999
  },
874
1000
  addStyle: (newStyle: StyleValue) => {
1001
+ if (!collectOutput) return;
875
1002
  if (!localCStyle) localCStyle = {};
876
1003
  Object.assign(localCStyle, newStyle);
877
1004
  },
878
1005
  };
879
1006
  const result = computed(ctx);
880
- if (result != null) {
1007
+ if (collectOutput && result != null) {
881
1008
  const r = extractClassAndStylePrebuilt(result);
882
- if (r.class != null) localCClasses.push(r.class);
1009
+ if (r.class != null) localCClasses?.push(r.class);
883
1010
  if (r.style) {
884
1011
  if (!localCStyle) localCStyle = {};
885
1012
  Object.assign(localCStyle, r.style);
@@ -888,79 +1015,133 @@ export function create({
888
1015
  cClasses = localCClasses;
889
1016
  cStyle = localCStyle;
890
1017
  if (updatedVariants) {
1018
+ const nextResolved: Record<string, unknown> = {};
1019
+ Object.assign(nextResolved, workingResolved);
891
1020
  if (hasAnyDisabled) {
892
1021
  const filteredUpdated: Record<string, unknown> = {};
893
1022
  filterDisabledInto(updatedVariants, filteredUpdated);
894
- workingResolved = filteredUpdated;
1023
+ Object.assign(nextResolved, filteredUpdated);
895
1024
  } else {
896
- workingResolved = updatedVariants;
1025
+ Object.assign(nextResolved, updatedVariants);
897
1026
  }
1027
+ workingResolved = nextResolved;
898
1028
  }
899
1029
  }
900
1030
 
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
- }
1031
+ return {
1032
+ workingResolved,
1033
+ changedVariants,
1034
+ classes: cClasses,
1035
+ style: cStyle,
1036
+ };
1037
+ };
912
1038
 
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];
922
- }
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];
931
- }
932
- }
1039
+ // Core compute path. Called both for top-level rendering (via
1040
+ // `computeResult`) and recursively when this component is used as an
1041
+ // `extend` target by another component. Pushes variant classes (excluding
1042
+ // base class) into `classesOut` and merges styles into `styleOut`.
1043
+ const computeOnce: ComputeFn = (
1044
+ resolved,
1045
+ userVariantProps,
1046
+ skipKeys,
1047
+ skipValues,
1048
+ classesOut,
1049
+ styleOut,
1050
+ runState,
1051
+ protectedVariants,
1052
+ pendingProtectedVariants,
1053
+ protectedVariantKeys,
1054
+ ) => {
1055
+ // Run `computed` (if any). May modify resolved variants and emit classes
1056
+ // and styles.
1057
+ let workingResolved = resolved;
1058
+ let cClasses: ClassValue[] | null = null;
1059
+ let cStyle: StyleValue | null = null;
1060
+ let changedVariants: Record<string, unknown> | null = null;
1061
+ if (computed) {
1062
+ const computedResult = runComputedContext(
1063
+ resolved,
1064
+ userVariantProps,
1065
+ true,
1066
+ true,
1067
+ protectedVariants,
1068
+ pendingProtectedVariants,
1069
+ protectedVariantKeys,
1070
+ );
1071
+ workingResolved = computedResult.workingResolved;
1072
+ cClasses = computedResult.classes;
1073
+ cStyle = computedResult.style;
1074
+ changedVariants = computedResult.changedVariants;
933
1075
  }
934
1076
 
935
1077
  // Run extends' contributions first (their full classes + styles) so our
936
1078
  // own base style and variants apply on top, matching the original
937
1079
  // ext1 → ext2 → … → current ordering.
938
1080
  //
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.
1081
+ // Pass explicit user values plus computed changes as the extends'
1082
+ // `userVariantProps`. This lets more-specific computed decisions stick
1083
+ // across re-runs while inherited static defaults can still be refined by
1084
+ // the extended component's own computed chain.
950
1085
  if (hasExtend) {
1086
+ // Build skip sets to pass to extends. Reuse precomputed values when no
1087
+ // caller-provided sets need merging.
1088
+ let extSkipKeys: Set<string> | null;
1089
+ if (skipKeys === null) {
1090
+ extSkipKeys = staticExtSkipKeys;
1091
+ } else if (staticExtSkipKeys === null) {
1092
+ extSkipKeys = skipKeys;
1093
+ } else {
1094
+ extSkipKeys = new Set(skipKeys);
1095
+ for (const k of staticExtSkipKeys) extSkipKeys.add(k);
1096
+ }
1097
+
1098
+ let extSkipVals: Record<string, Set<string>> | null;
1099
+ if (skipValues === null) {
1100
+ extSkipVals = staticExtSkipValues;
1101
+ } else if (staticExtSkipValues === null) {
1102
+ extSkipVals = skipValues;
1103
+ } else {
1104
+ extSkipVals = {};
1105
+ for (const k in skipValues) {
1106
+ extSkipVals[k] = skipValues[k];
1107
+ }
1108
+ for (const k in staticExtSkipValues) {
1109
+ const existing = extSkipVals[k];
1110
+ if (existing) {
1111
+ const merged = new Set<string>(existing);
1112
+ for (const v of staticExtSkipValues[k]) merged.add(v);
1113
+ extSkipVals[k] = merged;
1114
+ } else {
1115
+ extSkipVals[k] = staticExtSkipValues[k];
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ const extUserVariantProps =
1121
+ extMetasWithComputedCount > 0
1122
+ ? getExtUserVariantProps(
1123
+ userVariantProps,
1124
+ protectedVariants ?? null,
1125
+ changedVariants,
1126
+ )
1127
+ : userVariantProps;
951
1128
  for (let i = 0; i < extCount; i++) {
952
1129
  if (hasIsolatedExt && extIsolated[i]) {
953
1130
  // Isolated extend (different factory): gather its variant classes
954
1131
  // into a scratch array, then push the joined string after applying
955
1132
  // its own transformClass. Our outer transform applies on top.
956
1133
  const extClasses: ClsxClassValue[] = [];
957
- extMetas[i].compute(
958
- workingResolved,
1134
+ workingResolved = extMetas[i].compute(
959
1135
  workingResolved,
1136
+ extUserVariantProps,
960
1137
  extSkipKeys,
961
1138
  extSkipVals,
962
1139
  extClasses,
963
1140
  styleOut,
1141
+ runState,
1142
+ protectedVariants,
1143
+ pendingProtectedVariants,
1144
+ protectedVariantKeys,
964
1145
  );
965
1146
  if (extClasses.length > 0) {
966
1147
  const joined = clsx(extClasses);
@@ -971,15 +1152,24 @@ export function create({
971
1152
  }
972
1153
  }
973
1154
  } else {
974
- extMetas[i].compute(
975
- workingResolved,
1155
+ workingResolved = extMetas[i].compute(
976
1156
  workingResolved,
1157
+ extUserVariantProps,
977
1158
  extSkipKeys,
978
1159
  extSkipVals,
979
1160
  classesOut,
980
1161
  styleOut,
1162
+ runState,
1163
+ protectedVariants,
1164
+ pendingProtectedVariants,
1165
+ protectedVariantKeys,
981
1166
  );
982
1167
  }
1168
+ // Only sync protected variants when a child computed resolver can
1169
+ // observe them. Otherwise extUserVariantProps may alias caller props.
1170
+ if (protectedVariants && extMetasWithComputedCount > 0) {
1171
+ Object.assign(extUserVariantProps, protectedVariants);
1172
+ }
983
1173
  }
984
1174
  }
985
1175
 
@@ -1056,8 +1246,266 @@ export function create({
1056
1246
  }
1057
1247
  }
1058
1248
  if (cStyle) Object.assign(styleOut, cStyle);
1249
+
1250
+ return workingResolved;
1251
+ };
1252
+
1253
+ const compute: ComputeFn =
1254
+ !computed && extMetasWithComputedCount === 0
1255
+ ? (
1256
+ resolved,
1257
+ userVariantProps,
1258
+ skipKeys,
1259
+ skipValues,
1260
+ classesOut,
1261
+ styleOut,
1262
+ runState,
1263
+ protectedVariants,
1264
+ pendingProtectedVariants,
1265
+ protectedVariantKeys,
1266
+ ) => {
1267
+ return computeOnce(
1268
+ resolved,
1269
+ userVariantProps,
1270
+ skipKeys,
1271
+ skipValues,
1272
+ classesOut,
1273
+ styleOut,
1274
+ runState,
1275
+ protectedVariants,
1276
+ pendingProtectedVariants,
1277
+ protectedVariantKeys,
1278
+ );
1279
+ }
1280
+ : (
1281
+ resolved,
1282
+ userVariantProps,
1283
+ skipKeys,
1284
+ skipValues,
1285
+ classesOut,
1286
+ styleOut,
1287
+ runState,
1288
+ protectedVariants,
1289
+ pendingProtectedVariants,
1290
+ protectedVariantKeys,
1291
+ ) => {
1292
+ runState ??= { remaining: MAX_COMPUTED_RUNS, warned: false };
1293
+ protectedVariants ??= {};
1294
+ protectedVariantKeys ??= new Set<string>();
1295
+ let workingResolved = resolved;
1296
+ let lastClasses: ClsxClassValue[] = [];
1297
+ let lastStyle: StyleValue = {};
1298
+ let isFirstRun = true;
1299
+
1300
+ while (runState.remaining > 0) {
1301
+ runState.remaining -= 1;
1302
+ let useDirectOutput = isFirstRun;
1303
+ if (useDirectOutput) {
1304
+ for (const key in styleOut) {
1305
+ if (Object.hasOwn(styleOut, key)) {
1306
+ useDirectOutput = false;
1307
+ break;
1308
+ }
1309
+ }
1310
+ }
1311
+ const classCount = classesOut.length;
1312
+ const nextPendingProtectedVariants: Record<string, unknown> = {};
1313
+ const nextClasses: ClsxClassValue[] = useDirectOutput
1314
+ ? classesOut
1315
+ : [];
1316
+ const nextStyle: StyleValue = useDirectOutput ? styleOut : {};
1317
+ const nextResolved = computeOnce(
1318
+ workingResolved,
1319
+ userVariantProps,
1320
+ skipKeys,
1321
+ skipValues,
1322
+ nextClasses,
1323
+ nextStyle,
1324
+ runState,
1325
+ protectedVariants,
1326
+ nextPendingProtectedVariants,
1327
+ protectedVariantKeys,
1328
+ );
1329
+
1330
+ let protectedChanged: boolean;
1331
+ if (pendingProtectedVariants) {
1332
+ protectedChanged = mergeVariants(
1333
+ pendingProtectedVariants,
1334
+ nextPendingProtectedVariants,
1335
+ protectedVariantKeys,
1336
+ );
1337
+ } else {
1338
+ protectedChanged = mergeVariants(
1339
+ protectedVariants,
1340
+ nextPendingProtectedVariants,
1341
+ protectedVariantKeys,
1342
+ );
1343
+ }
1344
+
1345
+ if (
1346
+ !protectedChanged &&
1347
+ (nextResolved === workingResolved ||
1348
+ areVariantsEqual(workingResolved, nextResolved))
1349
+ ) {
1350
+ if (!useDirectOutput) {
1351
+ for (let i = 0; i < nextClasses.length; i++) {
1352
+ classesOut.push(nextClasses[i]);
1353
+ }
1354
+ Object.assign(styleOut, nextStyle);
1355
+ }
1356
+ return nextResolved;
1357
+ }
1358
+
1359
+ if (useDirectOutput && runState.remaining === 0) {
1360
+ // Keep the direct output from the last allowed run. Rolling
1361
+ // back here would drop it before the fallback copy below.
1362
+ warnComputedLimit(runState);
1363
+ return nextResolved;
1364
+ }
1365
+
1366
+ if (useDirectOutput) {
1367
+ classesOut.length = classCount;
1368
+ for (const key in styleOut) {
1369
+ if (Object.hasOwn(styleOut, key)) {
1370
+ Reflect.deleteProperty(styleOut, key);
1371
+ }
1372
+ }
1373
+ } else {
1374
+ lastClasses = nextClasses;
1375
+ lastStyle = nextStyle;
1376
+ }
1377
+
1378
+ workingResolved = nextResolved;
1379
+ isFirstRun = false;
1380
+ }
1381
+
1382
+ warnComputedLimit(runState);
1383
+
1384
+ for (let i = 0; i < lastClasses.length; i++) {
1385
+ classesOut.push(lastClasses[i]);
1386
+ }
1387
+ Object.assign(styleOut, lastStyle);
1388
+ return workingResolved;
1389
+ };
1390
+
1391
+ const resolveComputedOnce: ResolveComputedFn = (
1392
+ resolved,
1393
+ userVariantProps,
1394
+ filterOwnVariants = true,
1395
+ runState,
1396
+ protectedVariants,
1397
+ pendingProtectedVariants,
1398
+ protectedVariantKeys,
1399
+ ) => {
1400
+ let workingResolved = resolved;
1401
+ let changedVariants: Record<string, unknown> | null = null;
1402
+ if (computed) {
1403
+ const computedResult = runComputedContext(
1404
+ resolved,
1405
+ userVariantProps,
1406
+ filterOwnVariants,
1407
+ false,
1408
+ protectedVariants,
1409
+ pendingProtectedVariants,
1410
+ protectedVariantKeys,
1411
+ );
1412
+ workingResolved = computedResult.workingResolved;
1413
+ changedVariants = computedResult.changedVariants;
1414
+ }
1415
+
1416
+ if (extMetasWithComputedCount > 0) {
1417
+ const extUserVariantProps = getExtUserVariantProps(
1418
+ userVariantProps,
1419
+ protectedVariants ?? null,
1420
+ changedVariants,
1421
+ );
1422
+ for (let i = 0; i < extMetasWithComputedCount; i++) {
1423
+ const meta = extMetasWithComputed[i];
1424
+ const resolveComputed = meta.resolveComputed;
1425
+ if (!resolveComputed) continue;
1426
+ workingResolved = resolveComputed(
1427
+ workingResolved,
1428
+ extUserVariantProps,
1429
+ true,
1430
+ runState,
1431
+ protectedVariants,
1432
+ pendingProtectedVariants,
1433
+ protectedVariantKeys,
1434
+ );
1435
+ if (protectedVariants) {
1436
+ Object.assign(extUserVariantProps, protectedVariants);
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ return workingResolved;
1059
1442
  };
1060
1443
 
1444
+ const resolveComputed: ResolveComputedFn | null =
1445
+ computed || extMetasWithComputedCount > 0
1446
+ ? (
1447
+ resolved,
1448
+ userVariantProps,
1449
+ filterOwnVariants = true,
1450
+ runState,
1451
+ protectedVariants,
1452
+ pendingProtectedVariants,
1453
+ protectedVariantKeys,
1454
+ ) => {
1455
+ runState ??= { remaining: MAX_COMPUTED_RUNS, warned: false };
1456
+ protectedVariants ??= {};
1457
+ protectedVariantKeys ??= new Set<string>();
1458
+ let workingResolved = resolved;
1459
+ let reachedLimit = true;
1460
+
1461
+ while (runState.remaining > 0) {
1462
+ runState.remaining -= 1;
1463
+ const nextPendingProtectedVariants: Record<string, unknown> = {};
1464
+ const nextResolved = resolveComputedOnce(
1465
+ workingResolved,
1466
+ userVariantProps,
1467
+ filterOwnVariants,
1468
+ runState,
1469
+ protectedVariants,
1470
+ nextPendingProtectedVariants,
1471
+ protectedVariantKeys,
1472
+ );
1473
+ let protectedChanged: boolean;
1474
+ if (pendingProtectedVariants) {
1475
+ protectedChanged = mergeVariants(
1476
+ pendingProtectedVariants,
1477
+ nextPendingProtectedVariants,
1478
+ protectedVariantKeys,
1479
+ );
1480
+ } else {
1481
+ protectedChanged = mergeVariants(
1482
+ protectedVariants,
1483
+ nextPendingProtectedVariants,
1484
+ protectedVariantKeys,
1485
+ );
1486
+ }
1487
+
1488
+ if (
1489
+ !protectedChanged &&
1490
+ (nextResolved === workingResolved ||
1491
+ areVariantsEqual(workingResolved, nextResolved))
1492
+ ) {
1493
+ workingResolved = nextResolved;
1494
+ reachedLimit = false;
1495
+ break;
1496
+ }
1497
+
1498
+ workingResolved = nextResolved;
1499
+ }
1500
+
1501
+ if (reachedLimit) {
1502
+ warnComputedLimit(runState);
1503
+ }
1504
+
1505
+ return workingResolved;
1506
+ }
1507
+ : null;
1508
+
1061
1509
  // Top-level: resolves variants from user props, calls compute, then
1062
1510
  // assembles className and style with user-provided class/style overrides.
1063
1511
  const computeResult = (
@@ -1073,26 +1521,26 @@ export function create({
1073
1521
  Object.assign(resolved, staticDefaults);
1074
1522
 
1075
1523
  let userVariantProps: Record<string, unknown>;
1076
- if (extMetasWithResolveDefaultsCount > 0) {
1524
+ if (extMetasWithComputedCount > 0) {
1077
1525
  // Some extends need a resolveDefaults pass. They expect a variant-only
1078
1526
  // object as `userProps`, so we extract one.
1079
1527
  const variantProps: Record<string, unknown> = {};
1080
1528
  for (let i = 0; i < variantKeysLength; i++) {
1081
1529
  const key = variantKeys[i];
1082
- if (hasOwn.call(propsRecord, key)) {
1530
+ if (Object.hasOwn(propsRecord, key)) {
1083
1531
  variantProps[key] = propsRecord[key];
1084
1532
  }
1085
1533
  }
1086
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
1087
- const meta = extMetasWithResolveDefaults[i];
1534
+ for (let i = 0; i < extMetasWithComputedCount; i++) {
1535
+ const meta = extMetasWithComputed[i];
1088
1536
  const extComputed = meta.resolveDefaults!(resolved, variantProps);
1089
1537
  for (const k in extComputed) {
1090
- if (!hasOwn.call(extComputed, k)) continue;
1538
+ if (!Object.hasOwn(extComputed, k)) continue;
1091
1539
  resolved[k] = extComputed[k];
1092
1540
  }
1093
1541
  }
1094
1542
  for (const k in variantProps) {
1095
- if (!hasOwn.call(variantProps, k)) continue;
1543
+ if (!Object.hasOwn(variantProps, k)) continue;
1096
1544
  const v = variantProps[k];
1097
1545
  if (v === undefined) continue;
1098
1546
  resolved[k] = v;
@@ -1100,11 +1548,11 @@ export function create({
1100
1548
  userVariantProps = variantProps;
1101
1549
  } else {
1102
1550
  // Fast path: walk variantKeys directly against propsRecord. Use
1103
- // hasOwn so a polluted Object.prototype can't introduce variant
1551
+ // Object.hasOwn so a polluted Object.prototype can't introduce variant
1104
1552
  // values the user didn't pass.
1105
1553
  for (let i = 0; i < variantKeysLength; i++) {
1106
1554
  const key = variantKeys[i];
1107
- if (!hasOwn.call(propsRecord, key)) continue;
1555
+ if (!Object.hasOwn(propsRecord, key)) continue;
1108
1556
  const v = propsRecord[key];
1109
1557
  if (v === undefined) continue;
1110
1558
  resolved[key] = v;
@@ -1162,67 +1610,12 @@ export function create({
1162
1610
  const getVariants = (variants?: VariantValues<MergedVariants>) => {
1163
1611
  const variantProps = variants ?? EMPTY_DEFAULTS;
1164
1612
  let resolvedVariants = resolveVariantsHot(variantProps);
1165
- // Run computed function to get variants set via setVariants and
1166
- // setDefaultVariants
1167
- if (computed) {
1168
- const updatedVariants: Record<string, unknown> = {};
1169
- Object.assign(updatedVariants, resolvedVariants);
1170
- const ctx = {
1171
- variants: resolvedVariants as VariantValues<Record<string, unknown>>,
1172
- setVariants: (
1173
- newVariants: VariantValues<Record<string, unknown>>,
1174
- ) => {
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
- }
1194
- },
1195
- setDefaultVariants: (
1196
- newDefaults: VariantValues<Record<string, unknown>>,
1197
- ) => {
1198
- for (const key in newDefaults) {
1199
- if (!hasOwn.call(newDefaults, key)) continue;
1200
- if (variantProps[key] !== undefined) continue;
1201
- const value = (newDefaults as Record<string, unknown>)[key];
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
- }
1211
- }
1212
- updatedVariants[key] = value;
1213
- }
1214
- },
1215
- addClass: noop,
1216
- addStyle: noop,
1217
- };
1218
- computed(ctx);
1219
- if (hasAnyDisabled) {
1220
- const filteredUpdated: Record<string, unknown> = {};
1221
- filterDisabledInto(updatedVariants, filteredUpdated);
1222
- resolvedVariants = filteredUpdated;
1223
- } else {
1224
- resolvedVariants = updatedVariants;
1225
- }
1613
+ if (resolveComputed) {
1614
+ resolvedVariants = resolveComputed(
1615
+ resolvedVariants,
1616
+ variantProps,
1617
+ false,
1618
+ );
1226
1619
  }
1227
1620
  return resolvedVariants as VariantValues<MergedVariants>;
1228
1621
  };
@@ -1233,10 +1626,12 @@ export function create({
1233
1626
  // `transformClass(clsx(allClasses))` at render time, so applying it here
1234
1627
  // would compound (double for own-render, triple+ for extend chains) and
1235
1628
  // misbehave for non-idempotent transforms.
1236
- const computedBaseClass = clsx(
1237
- ...(extBaseClassesArr as ClsxClassValue[]),
1238
- config.class as ClsxClassValue,
1239
- );
1629
+ const computedBaseClass = hasExtend
1630
+ ? clsx(
1631
+ ...(extBaseClassesArr as ClsxClassValue[]),
1632
+ config.class as ClsxClassValue,
1633
+ )
1634
+ : clsx(config.class as ClsxClassValue);
1240
1635
 
1241
1636
  // Shared closures across the default and modal components.
1242
1637
  const classFn = (props: ComponentProps<MergedVariants> = {}) => {
@@ -1247,6 +1642,7 @@ export function create({
1247
1642
  staticDefaults,
1248
1643
  resolveDefaults: resolveDefaultsFn,
1249
1644
  compute,
1645
+ resolveComputed,
1250
1646
  transformClass,
1251
1647
  };
1252
1648
 
@@ -1255,15 +1651,14 @@ export function create({
1255
1651
  T extends ModalComponent<MergedVariants, R>,
1256
1652
  >(
1257
1653
  c: T,
1258
- keys: string[],
1654
+ propKeys: string[],
1259
1655
  style: T["style"],
1260
1656
  ): T => {
1261
1657
  c.class = classFn;
1262
1658
  c.style = style;
1263
1659
  c.getVariants = getVariants;
1264
- c.keys = keys;
1265
1660
  c.variantKeys = variantKeys;
1266
- c.propKeys = keys;
1661
+ c.propKeys = propKeys;
1267
1662
  setComponentMeta(c, meta);
1268
1663
  return c;
1269
1664
  };