clava 0.4.2 → 0.5.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
@@ -13,6 +13,7 @@ import type {
13
13
  ClassValue,
14
14
  ComponentProps,
15
15
  ComponentResult,
16
+ DefaultVariants,
16
17
  ExtendableVariants,
17
18
  HTMLObjProps,
18
19
  HTMLProps,
@@ -53,6 +54,8 @@ type ComputeFn = (
53
54
  protectedVariants?: Record<string, unknown> | null,
54
55
  pendingProtectedVariants?: Record<string, unknown> | null,
55
56
  protectedVariantKeys?: Set<string> | null,
57
+ defaultResolved?: Record<string, unknown>,
58
+ renderOnly?: boolean,
56
59
  ) => Record<string, unknown>;
57
60
 
58
61
  type ResolveRefineFn = (
@@ -63,21 +66,18 @@ type ResolveRefineFn = (
63
66
  protectedVariants?: Record<string, unknown> | null,
64
67
  pendingProtectedVariants?: Record<string, unknown> | null,
65
68
  protectedVariantKeys?: Set<string> | null,
69
+ defaultResolved?: Record<string, unknown>,
66
70
  ) => Record<string, unknown>;
67
71
 
72
+ type ComputedDefaultVariantFn = (context: {
73
+ defaultValue: unknown;
74
+ variants: Readonly<Record<string, unknown>>;
75
+ }) => unknown;
76
+
68
77
  // Internal metadata stored on components but hidden from public types.
69
78
  interface ComponentMeta {
70
79
  baseClass: string;
71
80
  staticDefaults: Record<string, unknown>;
72
- // Returns variants set via setDefaultVariants in the refine function chain.
73
- // null when this component has no resolveDefaults work to do (no `refine`
74
- // and no extends with work).
75
- resolveDefaults:
76
- | ((
77
- childDefaults: Record<string, unknown>,
78
- userProps?: Record<string, unknown>,
79
- ) => Record<string, unknown>)
80
- | null;
81
81
  // Returns variant classes + style for this component, used by extending
82
82
  // components. Top-level rendering also routes through this.
83
83
  compute: ComputeFn;
@@ -93,6 +93,10 @@ interface ComponentMeta {
93
93
  // type-level "function variant is replaced by anything in the child" rule).
94
94
  // Empty when no key in this chain is a function variant.
95
95
  functionVariantKeys: Set<string>;
96
+ // Variant keys with computed defaults anywhere in this component's chain.
97
+ // Child components use this to preserve inherited computed defaults through
98
+ // `defaultValue` without preserving their own prior computed result.
99
+ computedDefaultKeys: Set<string>;
96
100
  }
97
101
 
98
102
  const META_KEY = "__meta";
@@ -168,6 +172,31 @@ function mergeVariants(
168
172
  return changed;
169
173
  }
170
174
 
175
+ function mergeProtectedIntoBase(
176
+ baseResolved: Record<string, unknown>,
177
+ protectedVariants: Record<string, unknown> | null | undefined,
178
+ ): Record<string, unknown> {
179
+ if (!protectedVariants) {
180
+ return baseResolved;
181
+ }
182
+ let hasProtected = false;
183
+ for (const key in protectedVariants) {
184
+ if (!Object.hasOwn(protectedVariants, key)) continue;
185
+ hasProtected = true;
186
+ break;
187
+ }
188
+ if (!hasProtected) {
189
+ return baseResolved;
190
+ }
191
+ const resolved: Record<string, unknown> = {};
192
+ Object.assign(resolved, baseResolved);
193
+ for (const key in protectedVariants) {
194
+ if (!Object.hasOwn(protectedVariants, key)) continue;
195
+ resolved[key] = protectedVariants[key];
196
+ }
197
+ return resolved;
198
+ }
199
+
171
200
  // Components carry internal metadata on a non-public property so user-facing
172
201
  // component types stay clean.
173
202
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
@@ -211,7 +240,7 @@ export interface CVConfig<
211
240
  class?: ClassValue;
212
241
  style?: StyleValue;
213
242
  variants?: ExtendableVariants<V, E>;
214
- defaultVariants?: VariantValues<MergeVariants<V, E>>;
243
+ defaultVariants?: DefaultVariants<MergeVariants<V, E>>;
215
244
  refine?: Refine<MergeVariants<V, E>>;
216
245
  }
217
246
 
@@ -219,6 +248,11 @@ interface CreateParams {
219
248
  transformClass?: (className: string) => string;
220
249
  }
221
250
 
251
+ interface VariantConfigLike {
252
+ extend?: AnyComponent[];
253
+ variants?: Record<string, unknown>;
254
+ }
255
+
222
256
  function isRecordObject(value: unknown): value is Record<string, unknown> {
223
257
  if (typeof value !== "object") return false;
224
258
  if (value == null) return false;
@@ -282,9 +316,7 @@ function extractClassAndStylePrebuilt(value: unknown): PrebuiltValue {
282
316
  * Gets all variant keys from a component's config, including extended
283
317
  * components.
284
318
  */
285
- function collectVariantKeys(
286
- config: CVConfig<Variants, AnyComponent[]>,
287
- ): string[] {
319
+ function collectVariantKeys(config: VariantConfigLike): string[] {
288
320
  const keys = new Set<string>();
289
321
 
290
322
  if (config.extend) {
@@ -299,7 +331,7 @@ function collectVariantKeys(
299
331
  if (config.variants) {
300
332
  for (const key in config.variants) {
301
333
  if (!Object.hasOwn(config.variants, key)) continue;
302
- const variant = (config.variants as Record<string, unknown>)[key];
334
+ const variant = config.variants[key];
303
335
  if (variant === null) {
304
336
  keys.delete(key);
305
337
  continue;
@@ -311,13 +343,6 @@ function collectVariantKeys(
311
343
  return Array.from(keys);
312
344
  }
313
345
 
314
- function isVariantDisabled(
315
- config: CVConfig<Variants, AnyComponent[]>,
316
- key: string,
317
- ): boolean {
318
- return config.variants?.[key] === null;
319
- }
320
-
321
346
  function getVariantValueKey(value: unknown): string | undefined {
322
347
  if (typeof value === "string") {
323
348
  return value;
@@ -331,28 +356,14 @@ function getVariantValueKey(value: unknown): string | undefined {
331
356
  return undefined;
332
357
  }
333
358
 
334
- function isVariantValueDisabled(
335
- config: CVConfig<Variants, AnyComponent[]>,
336
- key: string,
337
- value: unknown,
338
- ): boolean {
339
- const valueKey = getVariantValueKey(value);
340
- if (valueKey == null) return false;
341
- const variant = config.variants?.[key];
342
- if (!isRecordObject(variant)) return false;
343
- return variant[valueKey] === null;
344
- }
345
-
346
- function collectDisabledVariantKeys(
347
- config: CVConfig<Variants, AnyComponent[]>,
348
- ): Set<string> {
359
+ function collectDisabledVariantKeys(config: VariantConfigLike): Set<string> {
349
360
  const keys = new Set<string>();
350
361
  if (!config.variants) {
351
362
  return keys;
352
363
  }
353
364
  for (const key in config.variants) {
354
365
  if (!Object.hasOwn(config.variants, key)) continue;
355
- if ((config.variants as Record<string, unknown>)[key] === null) {
366
+ if (config.variants[key] === null) {
356
367
  keys.add(key);
357
368
  }
358
369
  }
@@ -360,7 +371,7 @@ function collectDisabledVariantKeys(
360
371
  }
361
372
 
362
373
  function collectDisabledVariantValues(
363
- config: CVConfig<Variants, AnyComponent[]>,
374
+ config: VariantConfigLike,
364
375
  ): Record<string, Set<string>> {
365
376
  const values: Record<string, Set<string>> = {};
366
377
  if (!config.variants) {
@@ -368,7 +379,7 @@ function collectDisabledVariantValues(
368
379
  }
369
380
  for (const key in config.variants) {
370
381
  if (!Object.hasOwn(config.variants, key)) continue;
371
- const variant = (config.variants as Record<string, unknown>)[key];
382
+ const variant = config.variants[key];
372
383
  if (!isRecordObject(variant)) continue;
373
384
  let bucket: Set<string> | undefined;
374
385
  for (const variantValue in variant) {
@@ -628,10 +639,19 @@ export function create({
628
639
  const variantEntryCount = variantEntryNames.length;
629
640
  const functionVariantCount = functionVariantNames.length;
630
641
 
642
+ const computedDefaultNames: string[] = [];
643
+ const computedDefaultFns: ComputedDefaultVariantFn[] = [];
644
+ const defaultVariants = config.defaultVariants as
645
+ | Record<string, unknown>
646
+ | undefined;
647
+
631
648
  // Pre-compute static defaults. Includes:
632
649
  // - extended components' static defaults
633
650
  // - implicit boolean defaults (variants with a `false` key default to false)
634
- // - this config's defaultVariants (overriding the above)
651
+ // - this config's literal defaultVariants (overriding the above)
652
+ //
653
+ // Function entries in defaultVariants are computed defaults. They run in
654
+ // the refine loop so they can react to setVariants updates.
635
655
  // Then filtered through disabled-variants.
636
656
  const staticDefaults: Record<string, unknown> = {};
637
657
  if (extend) {
@@ -655,9 +675,23 @@ export function create({
655
675
  }
656
676
  }
657
677
  }
658
- if (config.defaultVariants) {
659
- Object.assign(staticDefaults, config.defaultVariants);
678
+ if (defaultVariants) {
679
+ for (const name in defaultVariants) {
680
+ if (!Object.hasOwn(defaultVariants, name)) continue;
681
+ const value = defaultVariants[name];
682
+ if (typeof value === "function") {
683
+ computedDefaultNames.push(name);
684
+ computedDefaultFns.push(value as ComputedDefaultVariantFn);
685
+ continue;
686
+ }
687
+ if (value === undefined) {
688
+ Reflect.deleteProperty(staticDefaults, name);
689
+ continue;
690
+ }
691
+ staticDefaults[name] = value;
692
+ }
660
693
  }
694
+ const computedDefaultCount = computedDefaultNames.length;
661
695
  if (hasAnyDisabled) {
662
696
  // Filter disabled variants in-place
663
697
  for (const key in staticDefaults) {
@@ -707,13 +741,21 @@ export function create({
707
741
  }
708
742
  const extCount = extMetas.length;
709
743
 
710
- // Filter to only extends with refine work in their chain. `resolveDefaults`
711
- // and `resolveRefine` are populated from the same transitive condition,
712
- // so one bucket is enough for both resolver paths.
744
+ const inheritedComputedDefaultKeys = new Set<string>();
745
+ for (let i = 0; i < extCount; i++) {
746
+ const keys = extMetas[i].computedDefaultKeys;
747
+ for (const key of keys) {
748
+ inheritedComputedDefaultKeys.add(key);
749
+ }
750
+ }
751
+
752
+ // Filter to only extends with computed default or refine work in their
753
+ // chain. Those are the components that can change resolved variants across
754
+ // fixed-point iterations.
713
755
  const extMetasWithRefine: ComponentMeta[] = [];
714
756
  for (let i = 0; i < extCount; i++) {
715
757
  const meta = extMetas[i];
716
- if (meta.resolveDefaults) {
758
+ if (meta.resolveRefine) {
717
759
  extMetasWithRefine.push(meta);
718
760
  }
719
761
  }
@@ -727,7 +769,8 @@ export function create({
727
769
  // frame is captured at creation time but the underlying `.stack` string is
728
770
  // formatted lazily on first access, so component creation stays cheap
729
771
  // unless the warning actually fires.
730
- const canTriggerRefineWarning = !!refine || extMetasWithRefineCount > 0;
772
+ const canTriggerRefineWarning =
773
+ !!refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0;
731
774
  const creationFrame = canTriggerRefineWarning
732
775
  ? captureCreationFrame(cv)
733
776
  : undefined;
@@ -756,6 +799,11 @@ export function create({
756
799
  functionVariantKeys.delete(variantEntryNames[i]);
757
800
  }
758
801
 
802
+ const computedDefaultKeys = new Set(inheritedComputedDefaultKeys);
803
+ for (let i = 0; i < computedDefaultCount; i++) {
804
+ computedDefaultKeys.add(computedDefaultNames[i]);
805
+ }
806
+
759
807
  // Static-variant keys in this component that override an inherited
760
808
  // function variant. Type-level merge says child fully replaces, so the
761
809
  // ancestor's function must not run with the child's (object-typed) value.
@@ -831,84 +879,62 @@ export function create({
831
879
  }
832
880
  }
833
881
 
834
- // Pre-create resolveDefaults function used by parents during their
835
- // `resolveVariantsHot`. Returns the variants set via setDefaultVariants in
836
- // the refine function chain.
837
- //
838
- // When this component has no `refine` and no `extend` with work, the
839
- // function is null — callers can skip iterating it entirely.
840
- const resolveDefaultsFn: ComponentMeta["resolveDefaults"] =
841
- refine || extMetasWithRefineCount > 0
842
- ? (
843
- childDefaults: Record<string, unknown>,
844
- userProps: Record<string, unknown> = EMPTY_DEFAULTS,
845
- ) => {
846
- // userProps is contractually variant-only (callers pre-filter
847
- // when starting from a full props object).
848
- const resolvedVariants: Record<string, unknown> = {};
849
- Object.assign(resolvedVariants, staticDefaults);
850
- for (const key in childDefaults) {
851
- if (!Object.hasOwn(childDefaults, key)) continue;
852
- const v = childDefaults[key];
853
- if (v === undefined) continue;
854
- resolvedVariants[key] = v;
855
- }
856
- for (const key in userProps) {
857
- if (!Object.hasOwn(userProps, key)) continue;
858
- const v = userProps[key];
859
- if (v === undefined) continue;
860
- resolvedVariants[key] = v;
861
- }
882
+ const isOwnDisabledValue = (key: string, value: unknown): boolean => {
883
+ if (disabledVariantKeys.has(key)) {
884
+ return true;
885
+ }
886
+ if (hasDisabledVariantValues) {
887
+ const valueKey = getVariantValueKey(value);
888
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
889
+ return true;
890
+ }
891
+ }
892
+ return false;
893
+ };
862
894
 
863
- const refineDefaults: Record<string, unknown> = {};
895
+ const filterOwnDisabledVariants = (
896
+ input: Record<string, unknown>,
897
+ fallback: Record<string, unknown>,
898
+ ): Record<string, unknown> => {
899
+ if (!hasAnyDisabled) {
900
+ return input;
901
+ }
864
902
 
865
- for (let i = 0; i < extMetasWithRefineCount; i++) {
866
- const extDefaults = extMetasWithRefine[i].resolveDefaults!(
867
- childDefaults,
868
- userProps,
869
- );
870
- for (const k in extDefaults) {
871
- if (!Object.hasOwn(extDefaults, k)) continue;
872
- refineDefaults[k] = extDefaults[k];
873
- }
874
- }
903
+ let hasOwnDisabledValue = false;
904
+ for (const key in input) {
905
+ if (!Object.hasOwn(input, key)) continue;
906
+ const value = input[key];
907
+ if (isOwnDisabledValue(key, value)) {
908
+ hasOwnDisabledValue = true;
909
+ break;
910
+ }
911
+ }
912
+ if (!hasOwnDisabledValue) {
913
+ return input;
914
+ }
875
915
 
876
- if (refine) {
877
- // Filter to own variant keys so `ctx.variants` matches
878
- // `VariantValues<V>` when this component is used as an extend by
879
- // a parent that adds extra variant keys (those keys would
880
- // otherwise leak through `userProps`).
881
- const ownVariants: Record<string, unknown> = {};
882
- for (let i = 0; i < variantKeysLength; i++) {
883
- const k = variantKeys[i];
884
- if (Object.hasOwn(resolvedVariants, k)) {
885
- ownVariants[k] = resolvedVariants[k];
886
- }
887
- }
888
- refine({
889
- variants: ownVariants as VariantValues<Record<string, unknown>>,
890
- setVariants: noop,
891
- setDefaultVariants: (newDefaults) => {
892
- for (const key in newDefaults) {
893
- if (!Object.hasOwn(newDefaults, key)) continue;
894
- const value = (newDefaults as Record<string, unknown>)[key];
895
- if (userProps[key] !== undefined) continue;
896
- if (isVariantDisabled(config, key)) continue;
897
- if (isVariantValueDisabled(config, key, value)) continue;
898
- refineDefaults[key] = value;
899
- }
900
- },
901
- addClass: noop,
902
- addStyle: noop,
903
- });
904
- }
916
+ const filtered: Record<string, unknown> = {};
917
+ for (const key in input) {
918
+ if (!Object.hasOwn(input, key)) continue;
919
+ const value = input[key];
920
+ if (!isOwnDisabledValue(key, value)) {
921
+ filtered[key] = value;
922
+ continue;
923
+ }
924
+ const fallbackValue = fallback[key];
925
+ if (
926
+ fallbackValue !== undefined &&
927
+ !isOwnDisabledValue(key, fallbackValue)
928
+ ) {
929
+ filtered[key] = fallbackValue;
930
+ }
931
+ }
905
932
 
906
- return refineDefaults;
907
- }
908
- : null;
933
+ return filtered;
934
+ };
909
935
 
910
936
  // Hot path: resolve variants by merging static defaults + extends'
911
- // refine defaults + user-provided props.
937
+ // static defaults + user-provided props.
912
938
  function resolveVariantsHot(
913
939
  propsVariants: Record<string, unknown>,
914
940
  ): Record<string, unknown> {
@@ -916,17 +942,6 @@ export function create({
916
942
  const defaults: Record<string, unknown> = {};
917
943
  Object.assign(defaults, staticDefaults);
918
944
 
919
- // Apply refine defaults from extended components (only those that have
920
- // actual work to do).
921
- for (let i = 0; i < extMetasWithRefineCount; i++) {
922
- const meta = extMetasWithRefine[i];
923
- const extDefaults = meta.resolveDefaults!(defaults, propsVariants);
924
- for (const k in extDefaults) {
925
- if (!Object.hasOwn(extDefaults, k)) continue;
926
- defaults[k] = extDefaults[k];
927
- }
928
- }
929
-
930
945
  // Apply propsVariants on top (filter undefined). propsVariants is
931
946
  // contractually variant-only here — callers building from a full props
932
947
  // object filter to variant keys before calling.
@@ -947,11 +962,100 @@ export function create({
947
962
  return result;
948
963
  }
949
964
 
965
+ const runComputedDefaults = (
966
+ resolved: Record<string, unknown>,
967
+ defaultResolved: Record<string, unknown>,
968
+ userVariantProps: Record<string, unknown>,
969
+ filterOwnVariants: boolean,
970
+ protectedVariantKeys: Set<string> | null | undefined,
971
+ ): {
972
+ workingResolved: Record<string, unknown>;
973
+ changedVariants: Record<string, unknown> | null;
974
+ } => {
975
+ if (computedDefaultCount === 0) {
976
+ return { workingResolved: resolved, changedVariants: null };
977
+ }
978
+
979
+ let ownVariants = filterOwnVariants ? null : resolved;
980
+ const getOwnVariants = (): Record<string, unknown> => {
981
+ if (ownVariants) {
982
+ return ownVariants;
983
+ }
984
+ const filteredVariants: Record<string, unknown> = {};
985
+ for (let i = 0; i < variantKeysLength; i++) {
986
+ const key = variantKeys[i];
987
+ if (Object.hasOwn(resolved, key)) {
988
+ filteredVariants[key] = resolved[key];
989
+ }
990
+ }
991
+ ownVariants = filteredVariants;
992
+ return filteredVariants;
993
+ };
994
+
995
+ let updatedVariants: Record<string, unknown> | null = null;
996
+ let changedVariants: Record<string, unknown> | null = null;
997
+ const ensureUpdated = (): Record<string, unknown> => {
998
+ if (updatedVariants) {
999
+ return updatedVariants;
1000
+ }
1001
+ const updated: Record<string, unknown> = {};
1002
+ Object.assign(updated, resolved);
1003
+ updatedVariants = updated;
1004
+ return updated;
1005
+ };
1006
+
1007
+ for (let i = 0; i < computedDefaultCount; i++) {
1008
+ const key = computedDefaultNames[i];
1009
+ if (Object.hasOwn(userVariantProps, key)) {
1010
+ if (userVariantProps[key] !== undefined) continue;
1011
+ }
1012
+ if (protectedVariantKeys?.has(key)) continue;
1013
+
1014
+ const variantSnapshot = getOwnVariants();
1015
+ const defaultValue = inheritedComputedDefaultKeys.has(key)
1016
+ ? variantSnapshot[key]
1017
+ : defaultResolved[key];
1018
+ const value = computedDefaultFns[i]({
1019
+ defaultValue,
1020
+ variants: variantSnapshot,
1021
+ });
1022
+ if (hasAnyDisabled) {
1023
+ if (disabledVariantKeys.has(key)) continue;
1024
+ const valueKey = getVariantValueKey(value);
1025
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
1026
+ continue;
1027
+ }
1028
+ }
1029
+
1030
+ if (value === undefined) {
1031
+ if (!Object.hasOwn(variantSnapshot, key)) continue;
1032
+ if (shouldCollectChangedVariants) {
1033
+ changedVariants ??= {};
1034
+ changedVariants[key] = value;
1035
+ }
1036
+ Reflect.deleteProperty(ensureUpdated(), key);
1037
+ continue;
1038
+ }
1039
+ if (Object.is(variantSnapshot[key], value)) continue;
1040
+ if (shouldCollectChangedVariants) {
1041
+ changedVariants ??= {};
1042
+ changedVariants[key] = value;
1043
+ }
1044
+ ensureUpdated()[key] = value;
1045
+ }
1046
+
1047
+ return {
1048
+ workingResolved: updatedVariants ?? resolved,
1049
+ changedVariants,
1050
+ };
1051
+ };
1052
+
950
1053
  const runRefineContext = (
951
1054
  resolved: Record<string, unknown>,
952
1055
  userVariantProps: Record<string, unknown>,
953
1056
  filterOwnVariants: boolean,
954
1057
  collectOutput: boolean,
1058
+ applyVariantUpdates: boolean,
955
1059
  protectedVariants: Record<string, unknown> | null | undefined,
956
1060
  pendingProtectedVariants: Record<string, unknown> | null | undefined,
957
1061
  protectedVariantKeys: Set<string> | null | undefined,
@@ -983,8 +1087,7 @@ export function create({
983
1087
  ownVariants = filteredVariants;
984
1088
  }
985
1089
  // Lazy-init updatedVariants — many refine callbacks only inspect
986
- // `variants` or call setDefaultVariants for keys the user already set,
987
- // so the copy is unnecessary in the common case.
1090
+ // `variants`, so the copy is unnecessary in the common case.
988
1091
  let updatedVariants: Record<string, unknown> | null = null;
989
1092
  const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
990
1093
  let localCStyle: StyleValue | null = null;
@@ -1021,6 +1124,9 @@ export function create({
1021
1124
  setVariants: (
1022
1125
  newVariants: VariantValues<Record<string, unknown>>,
1023
1126
  ) => {
1127
+ if (!applyVariantUpdates) {
1128
+ return;
1129
+ }
1024
1130
  if (!hasAnyDisabled) {
1025
1131
  for (const key in newVariants) {
1026
1132
  if (!Object.hasOwn(newVariants, key)) continue;
@@ -1049,32 +1155,6 @@ export function create({
1049
1155
  ensureUpdated()[key] = value;
1050
1156
  }
1051
1157
  },
1052
- setDefaultVariants: (
1053
- newDefaults: VariantValues<Record<string, unknown>>,
1054
- ) => {
1055
- for (const key in newDefaults) {
1056
- if (!Object.hasOwn(newDefaults, key)) continue;
1057
- if (userVariantProps[key] !== undefined) continue;
1058
- if (protectedVariantKeys?.has(key)) continue;
1059
- const value = (newDefaults as Record<string, unknown>)[key];
1060
- if (hasAnyDisabled) {
1061
- if (disabledVariantKeys.has(key)) continue;
1062
- const valueKey = getVariantValueKey(value);
1063
- if (
1064
- valueKey != null &&
1065
- disabledVariantValues[key]?.has(valueKey)
1066
- ) {
1067
- continue;
1068
- }
1069
- }
1070
- if (Object.is(getCurrentVariantValue(key), value)) continue;
1071
- setChangedVariant(key, value);
1072
- if (pendingProtectedVariants) {
1073
- pendingProtectedVariants[key] = value;
1074
- }
1075
- ensureUpdated()[key] = value;
1076
- }
1077
- },
1078
1158
  addClass: (className: ClassValue) => {
1079
1159
  localCClasses?.push(className);
1080
1160
  },
@@ -1138,37 +1218,17 @@ export function create({
1138
1218
  protectedVariants,
1139
1219
  pendingProtectedVariants,
1140
1220
  protectedVariantKeys,
1221
+ defaultResolved = resolved,
1222
+ renderOnly = false,
1141
1223
  ) => {
1142
- // Run `refine` (if any). May modify resolved variants and emit classes
1143
- // and styles.
1144
1224
  let workingResolved = resolved;
1145
1225
  let cClasses: ClassValue[] | null = null;
1146
1226
  let cStyle: StyleValue | null = null;
1147
1227
  let changedVariants: Record<string, unknown> | null = null;
1148
- if (refine) {
1149
- const refineResult = runRefineContext(
1150
- resolved,
1151
- userVariantProps,
1152
- true,
1153
- true,
1154
- protectedVariants,
1155
- pendingProtectedVariants,
1156
- protectedVariantKeys,
1157
- );
1158
- workingResolved = refineResult.workingResolved;
1159
- cClasses = refineResult.classes;
1160
- cStyle = refineResult.style;
1161
- changedVariants = refineResult.changedVariants;
1162
- }
1163
1228
 
1164
1229
  // Run extends' contributions first (their full classes + styles) so our
1165
1230
  // own base style and variants apply on top, matching the original
1166
1231
  // ext1 → ext2 → … → current ordering.
1167
- //
1168
- // Pass explicit user values plus refine changes as the extends'
1169
- // `userVariantProps`. This lets more-specific refine decisions stick
1170
- // across re-runs while inherited static defaults can still be refined by
1171
- // the extended component's own refine chain.
1172
1232
  if (hasExtend) {
1173
1233
  // Build skip sets to pass to extends. Reuse precomputed values when no
1174
1234
  // caller-provided sets need merging.
@@ -1233,6 +1293,8 @@ export function create({
1233
1293
  protectedVariants,
1234
1294
  pendingProtectedVariants,
1235
1295
  protectedVariantKeys,
1296
+ defaultResolved,
1297
+ renderOnly,
1236
1298
  );
1237
1299
  if (extClasses.length > 0) {
1238
1300
  const joined = clsx(extClasses);
@@ -1254,8 +1316,14 @@ export function create({
1254
1316
  protectedVariants,
1255
1317
  pendingProtectedVariants,
1256
1318
  protectedVariantKeys,
1319
+ defaultResolved,
1320
+ renderOnly,
1257
1321
  );
1258
1322
  }
1323
+ workingResolved = filterOwnDisabledVariants(
1324
+ workingResolved,
1325
+ defaultResolved,
1326
+ );
1259
1327
  // Only sync protected variants when a child refine resolver can
1260
1328
  // observe them. Otherwise extUserVariantProps may alias caller props.
1261
1329
  if (protectedVariants && extMetasWithRefineCount > 0) {
@@ -1264,6 +1332,42 @@ export function create({
1264
1332
  }
1265
1333
  }
1266
1334
 
1335
+ // Run own computed defaults after extended components so defaults resolve
1336
+ // from base to child. They still run before this component's `refine`.
1337
+ if (!renderOnly && computedDefaultCount > 0) {
1338
+ const computedResult = runComputedDefaults(
1339
+ workingResolved,
1340
+ defaultResolved,
1341
+ userVariantProps,
1342
+ true,
1343
+ protectedVariantKeys,
1344
+ );
1345
+ workingResolved = computedResult.workingResolved;
1346
+ changedVariants = computedResult.changedVariants;
1347
+ }
1348
+
1349
+ // Run own `refine` (if any). May modify resolved variants and emit
1350
+ // classes and styles that are applied after this component's variants.
1351
+ if (refine) {
1352
+ const refineResult = runRefineContext(
1353
+ workingResolved,
1354
+ userVariantProps,
1355
+ true,
1356
+ true,
1357
+ !renderOnly,
1358
+ protectedVariants,
1359
+ pendingProtectedVariants,
1360
+ protectedVariantKeys,
1361
+ );
1362
+ workingResolved = refineResult.workingResolved;
1363
+ cClasses = refineResult.classes;
1364
+ cStyle = refineResult.style;
1365
+ if (refineResult.changedVariants) {
1366
+ changedVariants ??= {};
1367
+ Object.assign(changedVariants, refineResult.changedVariants);
1368
+ }
1369
+ }
1370
+
1267
1371
  // Apply own base style (after extends' styles, matching original order).
1268
1372
  if (hasBaseStyle) {
1269
1373
  Object.assign(styleOut, baseStyle);
@@ -1362,7 +1466,7 @@ export function create({
1362
1466
  };
1363
1467
 
1364
1468
  const compute: ComputeFn =
1365
- !refine && extMetasWithRefineCount === 0
1469
+ !refine && computedDefaultCount === 0 && extMetasWithRefineCount === 0
1366
1470
  ? computeOnce
1367
1471
  : (
1368
1472
  resolved,
@@ -1375,7 +1479,25 @@ export function create({
1375
1479
  protectedVariants,
1376
1480
  pendingProtectedVariants,
1377
1481
  protectedVariantKeys,
1482
+ incomingDefaultResolved = resolved,
1483
+ renderOnly = false,
1378
1484
  ) => {
1485
+ if (renderOnly) {
1486
+ return computeOnce(
1487
+ resolved,
1488
+ userVariantProps,
1489
+ skipKeys,
1490
+ skipValues,
1491
+ classesOut,
1492
+ styleOut,
1493
+ runState,
1494
+ protectedVariants,
1495
+ pendingProtectedVariants,
1496
+ protectedVariantKeys,
1497
+ incomingDefaultResolved,
1498
+ true,
1499
+ );
1500
+ }
1379
1501
  runState ??= { remaining: MAX_REFINE_RUNS };
1380
1502
  protectedVariants ??= {};
1381
1503
  protectedVariantKeys ??= new Set<string>();
@@ -1404,6 +1526,10 @@ export function create({
1404
1526
  ? classesOut
1405
1527
  : [];
1406
1528
  const nextStyle: StyleValue = useDirectOutput ? styleOut : {};
1529
+ const defaultResolved = mergeProtectedIntoBase(
1530
+ incomingDefaultResolved,
1531
+ protectedVariants,
1532
+ );
1407
1533
  const nextResolved = computeOnce(
1408
1534
  workingResolved,
1409
1535
  userVariantProps,
@@ -1415,6 +1541,7 @@ export function create({
1415
1541
  protectedVariants,
1416
1542
  nextPendingProtectedVariants,
1417
1543
  protectedVariantKeys,
1544
+ defaultResolved,
1418
1545
  );
1419
1546
 
1420
1547
  let protectedChanged: boolean;
@@ -1437,7 +1564,30 @@ export function create({
1437
1564
  (nextResolved === workingResolved ||
1438
1565
  areVariantsEqual(workingResolved, nextResolved))
1439
1566
  ) {
1440
- if (!useDirectOutput) {
1567
+ if (nextResolved !== workingResolved) {
1568
+ if (useDirectOutput) {
1569
+ classesOut.length = classCount;
1570
+ for (const key in styleOut) {
1571
+ if (Object.hasOwn(styleOut, key)) {
1572
+ Reflect.deleteProperty(styleOut, key);
1573
+ }
1574
+ }
1575
+ }
1576
+ computeOnce(
1577
+ nextResolved,
1578
+ userVariantProps,
1579
+ skipKeys,
1580
+ skipValues,
1581
+ classesOut,
1582
+ styleOut,
1583
+ runState,
1584
+ protectedVariants,
1585
+ null,
1586
+ protectedVariantKeys,
1587
+ defaultResolved,
1588
+ true,
1589
+ );
1590
+ } else if (!useDirectOutput) {
1441
1591
  for (let i = 0; i < nextClasses.length; i++) {
1442
1592
  classesOut.push(nextClasses[i]);
1443
1593
  }
@@ -1508,22 +1658,10 @@ export function create({
1508
1658
  protectedVariants,
1509
1659
  pendingProtectedVariants,
1510
1660
  protectedVariantKeys,
1661
+ defaultResolved = resolved,
1511
1662
  ) => {
1512
1663
  let workingResolved = resolved;
1513
1664
  let changedVariants: Record<string, unknown> | null = null;
1514
- if (refine) {
1515
- const refineResult = runRefineContext(
1516
- resolved,
1517
- userVariantProps,
1518
- filterOwnVariants,
1519
- false,
1520
- protectedVariants,
1521
- pendingProtectedVariants,
1522
- protectedVariantKeys,
1523
- );
1524
- workingResolved = refineResult.workingResolved;
1525
- changedVariants = refineResult.changedVariants;
1526
- }
1527
1665
 
1528
1666
  if (extMetasWithRefineCount > 0) {
1529
1667
  const extUserVariantProps = getExtUserVariantProps(
@@ -1543,6 +1681,11 @@ export function create({
1543
1681
  protectedVariants,
1544
1682
  pendingProtectedVariants,
1545
1683
  protectedVariantKeys,
1684
+ defaultResolved,
1685
+ );
1686
+ workingResolved = filterOwnDisabledVariants(
1687
+ workingResolved,
1688
+ defaultResolved,
1546
1689
  );
1547
1690
  if (protectedVariants) {
1548
1691
  Object.assign(extUserVariantProps, protectedVariants);
@@ -1550,11 +1693,40 @@ export function create({
1550
1693
  }
1551
1694
  }
1552
1695
 
1696
+ if (computedDefaultCount > 0) {
1697
+ const computedResult = runComputedDefaults(
1698
+ workingResolved,
1699
+ defaultResolved,
1700
+ userVariantProps,
1701
+ filterOwnVariants,
1702
+ protectedVariantKeys,
1703
+ );
1704
+ workingResolved = computedResult.workingResolved;
1705
+ changedVariants = computedResult.changedVariants;
1706
+ }
1707
+ if (refine) {
1708
+ const refineResult = runRefineContext(
1709
+ workingResolved,
1710
+ userVariantProps,
1711
+ filterOwnVariants,
1712
+ false,
1713
+ true,
1714
+ protectedVariants,
1715
+ pendingProtectedVariants,
1716
+ protectedVariantKeys,
1717
+ );
1718
+ workingResolved = refineResult.workingResolved;
1719
+ if (refineResult.changedVariants) {
1720
+ changedVariants ??= {};
1721
+ Object.assign(changedVariants, refineResult.changedVariants);
1722
+ }
1723
+ }
1724
+
1553
1725
  return workingResolved;
1554
1726
  };
1555
1727
 
1556
1728
  const resolveRefine: ResolveRefineFn | null =
1557
- refine || extMetasWithRefineCount > 0
1729
+ refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0
1558
1730
  ? (
1559
1731
  resolved,
1560
1732
  userVariantProps,
@@ -1563,6 +1735,7 @@ export function create({
1563
1735
  protectedVariants,
1564
1736
  pendingProtectedVariants,
1565
1737
  protectedVariantKeys,
1738
+ incomingDefaultResolved = resolved,
1566
1739
  ) => {
1567
1740
  runState ??= { remaining: MAX_REFINE_RUNS };
1568
1741
  protectedVariants ??= {};
@@ -1577,6 +1750,10 @@ export function create({
1577
1750
  while (runState.remaining > 0) {
1578
1751
  runState.remaining -= 1;
1579
1752
  const nextPendingProtectedVariants: Record<string, unknown> = {};
1753
+ const defaultResolved = mergeProtectedIntoBase(
1754
+ incomingDefaultResolved,
1755
+ protectedVariants,
1756
+ );
1580
1757
  const nextResolved = resolveRefineOnce(
1581
1758
  workingResolved,
1582
1759
  userVariantProps,
@@ -1585,6 +1762,7 @@ export function create({
1585
1762
  protectedVariants,
1586
1763
  nextPendingProtectedVariants,
1587
1764
  protectedVariantKeys,
1765
+ defaultResolved,
1588
1766
  );
1589
1767
  let protectedChanged: boolean;
1590
1768
  if (pendingProtectedVariants) {
@@ -1646,17 +1824,11 @@ export function create({
1646
1824
  ): { className: string; style: StyleValue } => {
1647
1825
  const propsRecord = props as Record<string, unknown>;
1648
1826
 
1649
- // Inline resolve: avoids allocating a separate variantProps object for
1650
- // the common case where no extends need a resolveDefaults pass.
1651
- // resolveVariantsHot would also work here but assumes its input is
1652
- // variant-only (it uses for-in for speed).
1653
1827
  let resolved: Record<string, unknown> = {};
1654
1828
  Object.assign(resolved, staticDefaults);
1655
1829
 
1656
1830
  let userVariantProps: Record<string, unknown>;
1657
- if (extMetasWithRefineCount > 0) {
1658
- // Some extends need a resolveDefaults pass. They expect a variant-only
1659
- // object as `userProps`, so we extract one.
1831
+ if (refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0) {
1660
1832
  const variantProps: Record<string, unknown> = {};
1661
1833
  for (let i = 0; i < variantKeysLength; i++) {
1662
1834
  const key = variantKeys[i];
@@ -1664,14 +1836,6 @@ export function create({
1664
1836
  variantProps[key] = propsRecord[key];
1665
1837
  }
1666
1838
  }
1667
- for (let i = 0; i < extMetasWithRefineCount; i++) {
1668
- const meta = extMetasWithRefine[i];
1669
- const extDefaults = meta.resolveDefaults!(resolved, variantProps);
1670
- for (const k in extDefaults) {
1671
- if (!Object.hasOwn(extDefaults, k)) continue;
1672
- resolved[k] = extDefaults[k];
1673
- }
1674
- }
1675
1839
  for (const k in variantProps) {
1676
1840
  if (!Object.hasOwn(variantProps, k)) continue;
1677
1841
  const v = variantProps[k];
@@ -1769,11 +1933,11 @@ export function create({
1769
1933
  const meta: ComponentMeta = {
1770
1934
  baseClass: computedBaseClass,
1771
1935
  staticDefaults,
1772
- resolveDefaults: resolveDefaultsFn,
1773
1936
  compute,
1774
1937
  resolveRefine,
1775
1938
  transformClass,
1776
1939
  functionVariantKeys,
1940
+ computedDefaultKeys,
1777
1941
  };
1778
1942
 
1779
1943
  const initComponent = <
@@ -1845,6 +2009,4 @@ export function create({
1845
2009
  return { cv, cx };
1846
2010
  }
1847
2011
 
1848
- function noop() {}
1849
-
1850
2012
  export const { cv, cx } = create();