clava 0.4.2 → 0.6.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 = (
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 {
@@ -189,12 +218,64 @@ export type {
189
218
  CVComponent,
190
219
  };
191
220
 
221
+ /**
222
+ * Extracts the variant props inferred for a Clava component. Use it to add a
223
+ * component's variant props to framework component props.
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * import { type VariantProps, cv } from "clava";
228
+ * import type { ComponentProps } from "react";
229
+ *
230
+ * const button = cv({
231
+ * variants: {
232
+ * size: { sm: "button-sm", lg: "button-lg" },
233
+ * disabled: { true: "button-disabled", false: "" },
234
+ * },
235
+ * });
236
+ *
237
+ * interface ButtonProps
238
+ * extends ComponentProps<"button">,
239
+ * VariantProps<typeof button> {}
240
+ *
241
+ * const props: ButtonProps = {
242
+ * size: "lg",
243
+ * disabled: true,
244
+ * };
245
+ * ```
246
+ */
192
247
  export type VariantProps<T extends Pick<AnyComponent, "getVariants">> =
193
248
  ReturnType<T["getVariants"]>;
194
249
 
195
250
  // Variant props expose booleans, but variant object keys are always strings.
196
251
  type VariantKey<T> = T extends boolean ? "true" | "false" : Extract<T, string>;
197
252
 
253
+ /**
254
+ * Constrains a variant map to the same value keys as a variant on another
255
+ * component. Boolean variants are represented with `"true"` and `"false"`
256
+ * object keys.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * import { type Variant, cv } from "clava";
261
+ *
262
+ * const button = cv({
263
+ * variants: {
264
+ * size: { sm: "button-sm", lg: "button-lg" },
265
+ * },
266
+ * });
267
+ *
268
+ * const icon = cv({
269
+ * extend: [button],
270
+ * variants: {
271
+ * size: {
272
+ * sm: "icon-sm",
273
+ * lg: "icon-lg",
274
+ * } satisfies Variant<typeof button, "size">,
275
+ * },
276
+ * });
277
+ * ```
278
+ */
198
279
  export type Variant<
199
280
  T extends Pick<AnyComponent, "getVariants">,
200
281
  K extends keyof VariantProps<T>,
@@ -203,6 +284,32 @@ export type Variant<
203
284
  ClassValue | StyleClassValue
204
285
  >;
205
286
 
287
+ /**
288
+ * The configuration object accepted by `cv()`. It defines base class/style
289
+ * output, variants, default variants, component extensions, and refinement
290
+ * logic.
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * import { type CVConfig, cv } from "clava";
295
+ *
296
+ * const config: CVConfig<{
297
+ * tone: { info: string; danger: string };
298
+ * }> = {
299
+ * variants: {
300
+ * tone: {
301
+ * info: "alert-info",
302
+ * danger: "alert-danger",
303
+ * },
304
+ * },
305
+ * defaultVariants: {
306
+ * tone: "info",
307
+ * },
308
+ * };
309
+ *
310
+ * const alert = cv(config);
311
+ * ```
312
+ */
206
313
  export interface CVConfig<
207
314
  V extends Variants = {},
208
315
  E extends AnyComponent[] = [],
@@ -211,7 +318,7 @@ export interface CVConfig<
211
318
  class?: ClassValue;
212
319
  style?: StyleValue;
213
320
  variants?: ExtendableVariants<V, E>;
214
- defaultVariants?: VariantValues<MergeVariants<V, E>>;
321
+ defaultVariants?: DefaultVariants<MergeVariants<V, E>>;
215
322
  refine?: Refine<MergeVariants<V, E>>;
216
323
  }
217
324
 
@@ -219,6 +326,11 @@ interface CreateParams {
219
326
  transformClass?: (className: string) => string;
220
327
  }
221
328
 
329
+ interface VariantConfigLike {
330
+ extend?: AnyComponent[];
331
+ variants?: Record<string, unknown>;
332
+ }
333
+
222
334
  function isRecordObject(value: unknown): value is Record<string, unknown> {
223
335
  if (typeof value !== "object") return false;
224
336
  if (value == null) return false;
@@ -282,9 +394,7 @@ function extractClassAndStylePrebuilt(value: unknown): PrebuiltValue {
282
394
  * Gets all variant keys from a component's config, including extended
283
395
  * components.
284
396
  */
285
- function collectVariantKeys(
286
- config: CVConfig<Variants, AnyComponent[]>,
287
- ): string[] {
397
+ function collectVariantKeys(config: VariantConfigLike): string[] {
288
398
  const keys = new Set<string>();
289
399
 
290
400
  if (config.extend) {
@@ -299,7 +409,7 @@ function collectVariantKeys(
299
409
  if (config.variants) {
300
410
  for (const key in config.variants) {
301
411
  if (!Object.hasOwn(config.variants, key)) continue;
302
- const variant = (config.variants as Record<string, unknown>)[key];
412
+ const variant = config.variants[key];
303
413
  if (variant === null) {
304
414
  keys.delete(key);
305
415
  continue;
@@ -311,13 +421,6 @@ function collectVariantKeys(
311
421
  return Array.from(keys);
312
422
  }
313
423
 
314
- function isVariantDisabled(
315
- config: CVConfig<Variants, AnyComponent[]>,
316
- key: string,
317
- ): boolean {
318
- return config.variants?.[key] === null;
319
- }
320
-
321
424
  function getVariantValueKey(value: unknown): string | undefined {
322
425
  if (typeof value === "string") {
323
426
  return value;
@@ -331,28 +434,14 @@ function getVariantValueKey(value: unknown): string | undefined {
331
434
  return undefined;
332
435
  }
333
436
 
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> {
437
+ function collectDisabledVariantKeys(config: VariantConfigLike): Set<string> {
349
438
  const keys = new Set<string>();
350
439
  if (!config.variants) {
351
440
  return keys;
352
441
  }
353
442
  for (const key in config.variants) {
354
443
  if (!Object.hasOwn(config.variants, key)) continue;
355
- if ((config.variants as Record<string, unknown>)[key] === null) {
444
+ if (config.variants[key] === null) {
356
445
  keys.add(key);
357
446
  }
358
447
  }
@@ -360,7 +449,7 @@ function collectDisabledVariantKeys(
360
449
  }
361
450
 
362
451
  function collectDisabledVariantValues(
363
- config: CVConfig<Variants, AnyComponent[]>,
452
+ config: VariantConfigLike,
364
453
  ): Record<string, Set<string>> {
365
454
  const values: Record<string, Set<string>> = {};
366
455
  if (!config.variants) {
@@ -368,7 +457,7 @@ function collectDisabledVariantValues(
368
457
  }
369
458
  for (const key in config.variants) {
370
459
  if (!Object.hasOwn(config.variants, key)) continue;
371
- const variant = (config.variants as Record<string, unknown>)[key];
460
+ const variant = config.variants[key];
372
461
  if (!isRecordObject(variant)) continue;
373
462
  let bucket: Set<string> | undefined;
374
463
  for (const variantValue in variant) {
@@ -628,10 +717,19 @@ export function create({
628
717
  const variantEntryCount = variantEntryNames.length;
629
718
  const functionVariantCount = functionVariantNames.length;
630
719
 
720
+ const computedDefaultNames: string[] = [];
721
+ const computedDefaultFns: ComputedDefaultVariantFn[] = [];
722
+ const defaultVariants = config.defaultVariants as
723
+ | Record<string, unknown>
724
+ | undefined;
725
+
631
726
  // Pre-compute static defaults. Includes:
632
727
  // - extended components' static defaults
633
728
  // - implicit boolean defaults (variants with a `false` key default to false)
634
- // - this config's defaultVariants (overriding the above)
729
+ // - this config's literal defaultVariants (overriding the above)
730
+ //
731
+ // Function entries in defaultVariants are computed defaults. They run in
732
+ // the refine loop so they can react to setVariants updates.
635
733
  // Then filtered through disabled-variants.
636
734
  const staticDefaults: Record<string, unknown> = {};
637
735
  if (extend) {
@@ -655,9 +753,23 @@ export function create({
655
753
  }
656
754
  }
657
755
  }
658
- if (config.defaultVariants) {
659
- Object.assign(staticDefaults, config.defaultVariants);
756
+ if (defaultVariants) {
757
+ for (const name in defaultVariants) {
758
+ if (!Object.hasOwn(defaultVariants, name)) continue;
759
+ const value = defaultVariants[name];
760
+ if (typeof value === "function") {
761
+ computedDefaultNames.push(name);
762
+ computedDefaultFns.push(value as ComputedDefaultVariantFn);
763
+ continue;
764
+ }
765
+ if (value === undefined) {
766
+ Reflect.deleteProperty(staticDefaults, name);
767
+ continue;
768
+ }
769
+ staticDefaults[name] = value;
770
+ }
660
771
  }
772
+ const computedDefaultCount = computedDefaultNames.length;
661
773
  if (hasAnyDisabled) {
662
774
  // Filter disabled variants in-place
663
775
  for (const key in staticDefaults) {
@@ -707,13 +819,21 @@ export function create({
707
819
  }
708
820
  const extCount = extMetas.length;
709
821
 
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.
822
+ const inheritedComputedDefaultKeys = new Set<string>();
823
+ for (let i = 0; i < extCount; i++) {
824
+ const keys = extMetas[i].computedDefaultKeys;
825
+ for (const key of keys) {
826
+ inheritedComputedDefaultKeys.add(key);
827
+ }
828
+ }
829
+
830
+ // Filter to only extends with computed default or refine work in their
831
+ // chain. Those are the components that can change resolved variants across
832
+ // fixed-point iterations.
713
833
  const extMetasWithRefine: ComponentMeta[] = [];
714
834
  for (let i = 0; i < extCount; i++) {
715
835
  const meta = extMetas[i];
716
- if (meta.resolveDefaults) {
836
+ if (meta.resolveRefine) {
717
837
  extMetasWithRefine.push(meta);
718
838
  }
719
839
  }
@@ -727,7 +847,8 @@ export function create({
727
847
  // frame is captured at creation time but the underlying `.stack` string is
728
848
  // formatted lazily on first access, so component creation stays cheap
729
849
  // unless the warning actually fires.
730
- const canTriggerRefineWarning = !!refine || extMetasWithRefineCount > 0;
850
+ const canTriggerRefineWarning =
851
+ !!refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0;
731
852
  const creationFrame = canTriggerRefineWarning
732
853
  ? captureCreationFrame(cv)
733
854
  : undefined;
@@ -756,6 +877,11 @@ export function create({
756
877
  functionVariantKeys.delete(variantEntryNames[i]);
757
878
  }
758
879
 
880
+ const computedDefaultKeys = new Set(inheritedComputedDefaultKeys);
881
+ for (let i = 0; i < computedDefaultCount; i++) {
882
+ computedDefaultKeys.add(computedDefaultNames[i]);
883
+ }
884
+
759
885
  // Static-variant keys in this component that override an inherited
760
886
  // function variant. Type-level merge says child fully replaces, so the
761
887
  // ancestor's function must not run with the child's (object-typed) value.
@@ -831,84 +957,62 @@ export function create({
831
957
  }
832
958
  }
833
959
 
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
- }
960
+ const isOwnDisabledValue = (key: string, value: unknown): boolean => {
961
+ if (disabledVariantKeys.has(key)) {
962
+ return true;
963
+ }
964
+ if (hasDisabledVariantValues) {
965
+ const valueKey = getVariantValueKey(value);
966
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
967
+ return true;
968
+ }
969
+ }
970
+ return false;
971
+ };
862
972
 
863
- const refineDefaults: Record<string, unknown> = {};
973
+ const filterOwnDisabledVariants = (
974
+ input: Record<string, unknown>,
975
+ fallback: Record<string, unknown>,
976
+ ): Record<string, unknown> => {
977
+ if (!hasAnyDisabled) {
978
+ return input;
979
+ }
864
980
 
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
- }
981
+ let hasOwnDisabledValue = false;
982
+ for (const key in input) {
983
+ if (!Object.hasOwn(input, key)) continue;
984
+ const value = input[key];
985
+ if (isOwnDisabledValue(key, value)) {
986
+ hasOwnDisabledValue = true;
987
+ break;
988
+ }
989
+ }
990
+ if (!hasOwnDisabledValue) {
991
+ return input;
992
+ }
875
993
 
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
- }
994
+ const filtered: Record<string, unknown> = {};
995
+ for (const key in input) {
996
+ if (!Object.hasOwn(input, key)) continue;
997
+ const value = input[key];
998
+ if (!isOwnDisabledValue(key, value)) {
999
+ filtered[key] = value;
1000
+ continue;
1001
+ }
1002
+ const fallbackValue = fallback[key];
1003
+ if (
1004
+ fallbackValue !== undefined &&
1005
+ !isOwnDisabledValue(key, fallbackValue)
1006
+ ) {
1007
+ filtered[key] = fallbackValue;
1008
+ }
1009
+ }
905
1010
 
906
- return refineDefaults;
907
- }
908
- : null;
1011
+ return filtered;
1012
+ };
909
1013
 
910
1014
  // Hot path: resolve variants by merging static defaults + extends'
911
- // refine defaults + user-provided props.
1015
+ // static defaults + user-provided props.
912
1016
  function resolveVariantsHot(
913
1017
  propsVariants: Record<string, unknown>,
914
1018
  ): Record<string, unknown> {
@@ -916,17 +1020,6 @@ export function create({
916
1020
  const defaults: Record<string, unknown> = {};
917
1021
  Object.assign(defaults, staticDefaults);
918
1022
 
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
1023
  // Apply propsVariants on top (filter undefined). propsVariants is
931
1024
  // contractually variant-only here — callers building from a full props
932
1025
  // object filter to variant keys before calling.
@@ -947,11 +1040,97 @@ export function create({
947
1040
  return result;
948
1041
  }
949
1042
 
1043
+ const runComputedDefaults = (
1044
+ resolved: Record<string, unknown>,
1045
+ defaultResolved: Record<string, unknown>,
1046
+ userVariantProps: Record<string, unknown>,
1047
+ filterOwnVariants: boolean,
1048
+ protectedVariantKeys: Set<string> | null | undefined,
1049
+ ): {
1050
+ workingResolved: Record<string, unknown>;
1051
+ changedVariants: Record<string, unknown> | null;
1052
+ } => {
1053
+ if (computedDefaultCount === 0) {
1054
+ return { workingResolved: resolved, changedVariants: null };
1055
+ }
1056
+
1057
+ let ownVariants = filterOwnVariants ? null : resolved;
1058
+ const getOwnVariants = (): Record<string, unknown> => {
1059
+ if (ownVariants) {
1060
+ return ownVariants;
1061
+ }
1062
+ const filteredVariants: Record<string, unknown> = {};
1063
+ for (let i = 0; i < variantKeysLength; i++) {
1064
+ const key = variantKeys[i];
1065
+ if (Object.hasOwn(resolved, key)) {
1066
+ filteredVariants[key] = resolved[key];
1067
+ }
1068
+ }
1069
+ ownVariants = filteredVariants;
1070
+ return filteredVariants;
1071
+ };
1072
+
1073
+ let updatedVariants: Record<string, unknown> | null = null;
1074
+ let changedVariants: Record<string, unknown> | null = null;
1075
+ const ensureUpdated = (): Record<string, unknown> => {
1076
+ if (updatedVariants) {
1077
+ return updatedVariants;
1078
+ }
1079
+ const updated: Record<string, unknown> = {};
1080
+ Object.assign(updated, resolved);
1081
+ updatedVariants = updated;
1082
+ return updated;
1083
+ };
1084
+
1085
+ for (let i = 0; i < computedDefaultCount; i++) {
1086
+ const key = computedDefaultNames[i];
1087
+ if (Object.hasOwn(userVariantProps, key)) {
1088
+ if (userVariantProps[key] !== undefined) continue;
1089
+ }
1090
+ if (protectedVariantKeys?.has(key)) continue;
1091
+
1092
+ const variantSnapshot = getOwnVariants();
1093
+ const defaultValue = inheritedComputedDefaultKeys.has(key)
1094
+ ? variantSnapshot[key]
1095
+ : defaultResolved[key];
1096
+ const value = computedDefaultFns[i](defaultValue, variantSnapshot);
1097
+ if (hasAnyDisabled) {
1098
+ if (disabledVariantKeys.has(key)) continue;
1099
+ const valueKey = getVariantValueKey(value);
1100
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
1101
+ continue;
1102
+ }
1103
+ }
1104
+
1105
+ if (value === undefined) {
1106
+ if (!Object.hasOwn(variantSnapshot, key)) continue;
1107
+ if (shouldCollectChangedVariants) {
1108
+ changedVariants ??= {};
1109
+ changedVariants[key] = value;
1110
+ }
1111
+ Reflect.deleteProperty(ensureUpdated(), key);
1112
+ continue;
1113
+ }
1114
+ if (Object.is(variantSnapshot[key], value)) continue;
1115
+ if (shouldCollectChangedVariants) {
1116
+ changedVariants ??= {};
1117
+ changedVariants[key] = value;
1118
+ }
1119
+ ensureUpdated()[key] = value;
1120
+ }
1121
+
1122
+ return {
1123
+ workingResolved: updatedVariants ?? resolved,
1124
+ changedVariants,
1125
+ };
1126
+ };
1127
+
950
1128
  const runRefineContext = (
951
1129
  resolved: Record<string, unknown>,
952
1130
  userVariantProps: Record<string, unknown>,
953
1131
  filterOwnVariants: boolean,
954
1132
  collectOutput: boolean,
1133
+ applyVariantUpdates: boolean,
955
1134
  protectedVariants: Record<string, unknown> | null | undefined,
956
1135
  pendingProtectedVariants: Record<string, unknown> | null | undefined,
957
1136
  protectedVariantKeys: Set<string> | null | undefined,
@@ -983,8 +1162,7 @@ export function create({
983
1162
  ownVariants = filteredVariants;
984
1163
  }
985
1164
  // 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.
1165
+ // `variants`, so the copy is unnecessary in the common case.
988
1166
  let updatedVariants: Record<string, unknown> | null = null;
989
1167
  const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
990
1168
  let localCStyle: StyleValue | null = null;
@@ -1021,6 +1199,9 @@ export function create({
1021
1199
  setVariants: (
1022
1200
  newVariants: VariantValues<Record<string, unknown>>,
1023
1201
  ) => {
1202
+ if (!applyVariantUpdates) {
1203
+ return;
1204
+ }
1024
1205
  if (!hasAnyDisabled) {
1025
1206
  for (const key in newVariants) {
1026
1207
  if (!Object.hasOwn(newVariants, key)) continue;
@@ -1049,32 +1230,6 @@ export function create({
1049
1230
  ensureUpdated()[key] = value;
1050
1231
  }
1051
1232
  },
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
1233
  addClass: (className: ClassValue) => {
1079
1234
  localCClasses?.push(className);
1080
1235
  },
@@ -1138,37 +1293,17 @@ export function create({
1138
1293
  protectedVariants,
1139
1294
  pendingProtectedVariants,
1140
1295
  protectedVariantKeys,
1296
+ defaultResolved = resolved,
1297
+ renderOnly = false,
1141
1298
  ) => {
1142
- // Run `refine` (if any). May modify resolved variants and emit classes
1143
- // and styles.
1144
1299
  let workingResolved = resolved;
1145
1300
  let cClasses: ClassValue[] | null = null;
1146
1301
  let cStyle: StyleValue | null = null;
1147
1302
  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
1303
 
1164
1304
  // Run extends' contributions first (their full classes + styles) so our
1165
1305
  // own base style and variants apply on top, matching the original
1166
1306
  // 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
1307
  if (hasExtend) {
1173
1308
  // Build skip sets to pass to extends. Reuse precomputed values when no
1174
1309
  // caller-provided sets need merging.
@@ -1233,6 +1368,8 @@ export function create({
1233
1368
  protectedVariants,
1234
1369
  pendingProtectedVariants,
1235
1370
  protectedVariantKeys,
1371
+ defaultResolved,
1372
+ renderOnly,
1236
1373
  );
1237
1374
  if (extClasses.length > 0) {
1238
1375
  const joined = clsx(extClasses);
@@ -1254,8 +1391,14 @@ export function create({
1254
1391
  protectedVariants,
1255
1392
  pendingProtectedVariants,
1256
1393
  protectedVariantKeys,
1394
+ defaultResolved,
1395
+ renderOnly,
1257
1396
  );
1258
1397
  }
1398
+ workingResolved = filterOwnDisabledVariants(
1399
+ workingResolved,
1400
+ defaultResolved,
1401
+ );
1259
1402
  // Only sync protected variants when a child refine resolver can
1260
1403
  // observe them. Otherwise extUserVariantProps may alias caller props.
1261
1404
  if (protectedVariants && extMetasWithRefineCount > 0) {
@@ -1264,6 +1407,42 @@ export function create({
1264
1407
  }
1265
1408
  }
1266
1409
 
1410
+ // Run own computed defaults after extended components so defaults resolve
1411
+ // from base to child. They still run before this component's `refine`.
1412
+ if (!renderOnly && computedDefaultCount > 0) {
1413
+ const computedResult = runComputedDefaults(
1414
+ workingResolved,
1415
+ defaultResolved,
1416
+ userVariantProps,
1417
+ true,
1418
+ protectedVariantKeys,
1419
+ );
1420
+ workingResolved = computedResult.workingResolved;
1421
+ changedVariants = computedResult.changedVariants;
1422
+ }
1423
+
1424
+ // Run own `refine` (if any). May modify resolved variants and emit
1425
+ // classes and styles that are applied after this component's variants.
1426
+ if (refine) {
1427
+ const refineResult = runRefineContext(
1428
+ workingResolved,
1429
+ userVariantProps,
1430
+ true,
1431
+ true,
1432
+ !renderOnly,
1433
+ protectedVariants,
1434
+ pendingProtectedVariants,
1435
+ protectedVariantKeys,
1436
+ );
1437
+ workingResolved = refineResult.workingResolved;
1438
+ cClasses = refineResult.classes;
1439
+ cStyle = refineResult.style;
1440
+ if (refineResult.changedVariants) {
1441
+ changedVariants ??= {};
1442
+ Object.assign(changedVariants, refineResult.changedVariants);
1443
+ }
1444
+ }
1445
+
1267
1446
  // Apply own base style (after extends' styles, matching original order).
1268
1447
  if (hasBaseStyle) {
1269
1448
  Object.assign(styleOut, baseStyle);
@@ -1362,7 +1541,7 @@ export function create({
1362
1541
  };
1363
1542
 
1364
1543
  const compute: ComputeFn =
1365
- !refine && extMetasWithRefineCount === 0
1544
+ !refine && computedDefaultCount === 0 && extMetasWithRefineCount === 0
1366
1545
  ? computeOnce
1367
1546
  : (
1368
1547
  resolved,
@@ -1375,7 +1554,25 @@ export function create({
1375
1554
  protectedVariants,
1376
1555
  pendingProtectedVariants,
1377
1556
  protectedVariantKeys,
1557
+ incomingDefaultResolved = resolved,
1558
+ renderOnly = false,
1378
1559
  ) => {
1560
+ if (renderOnly) {
1561
+ return computeOnce(
1562
+ resolved,
1563
+ userVariantProps,
1564
+ skipKeys,
1565
+ skipValues,
1566
+ classesOut,
1567
+ styleOut,
1568
+ runState,
1569
+ protectedVariants,
1570
+ pendingProtectedVariants,
1571
+ protectedVariantKeys,
1572
+ incomingDefaultResolved,
1573
+ true,
1574
+ );
1575
+ }
1379
1576
  runState ??= { remaining: MAX_REFINE_RUNS };
1380
1577
  protectedVariants ??= {};
1381
1578
  protectedVariantKeys ??= new Set<string>();
@@ -1404,6 +1601,10 @@ export function create({
1404
1601
  ? classesOut
1405
1602
  : [];
1406
1603
  const nextStyle: StyleValue = useDirectOutput ? styleOut : {};
1604
+ const defaultResolved = mergeProtectedIntoBase(
1605
+ incomingDefaultResolved,
1606
+ protectedVariants,
1607
+ );
1407
1608
  const nextResolved = computeOnce(
1408
1609
  workingResolved,
1409
1610
  userVariantProps,
@@ -1415,6 +1616,7 @@ export function create({
1415
1616
  protectedVariants,
1416
1617
  nextPendingProtectedVariants,
1417
1618
  protectedVariantKeys,
1619
+ defaultResolved,
1418
1620
  );
1419
1621
 
1420
1622
  let protectedChanged: boolean;
@@ -1437,7 +1639,30 @@ export function create({
1437
1639
  (nextResolved === workingResolved ||
1438
1640
  areVariantsEqual(workingResolved, nextResolved))
1439
1641
  ) {
1440
- if (!useDirectOutput) {
1642
+ if (nextResolved !== workingResolved) {
1643
+ if (useDirectOutput) {
1644
+ classesOut.length = classCount;
1645
+ for (const key in styleOut) {
1646
+ if (Object.hasOwn(styleOut, key)) {
1647
+ Reflect.deleteProperty(styleOut, key);
1648
+ }
1649
+ }
1650
+ }
1651
+ computeOnce(
1652
+ nextResolved,
1653
+ userVariantProps,
1654
+ skipKeys,
1655
+ skipValues,
1656
+ classesOut,
1657
+ styleOut,
1658
+ runState,
1659
+ protectedVariants,
1660
+ null,
1661
+ protectedVariantKeys,
1662
+ defaultResolved,
1663
+ true,
1664
+ );
1665
+ } else if (!useDirectOutput) {
1441
1666
  for (let i = 0; i < nextClasses.length; i++) {
1442
1667
  classesOut.push(nextClasses[i]);
1443
1668
  }
@@ -1508,22 +1733,10 @@ export function create({
1508
1733
  protectedVariants,
1509
1734
  pendingProtectedVariants,
1510
1735
  protectedVariantKeys,
1736
+ defaultResolved = resolved,
1511
1737
  ) => {
1512
1738
  let workingResolved = resolved;
1513
1739
  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
1740
 
1528
1741
  if (extMetasWithRefineCount > 0) {
1529
1742
  const extUserVariantProps = getExtUserVariantProps(
@@ -1543,6 +1756,11 @@ export function create({
1543
1756
  protectedVariants,
1544
1757
  pendingProtectedVariants,
1545
1758
  protectedVariantKeys,
1759
+ defaultResolved,
1760
+ );
1761
+ workingResolved = filterOwnDisabledVariants(
1762
+ workingResolved,
1763
+ defaultResolved,
1546
1764
  );
1547
1765
  if (protectedVariants) {
1548
1766
  Object.assign(extUserVariantProps, protectedVariants);
@@ -1550,11 +1768,40 @@ export function create({
1550
1768
  }
1551
1769
  }
1552
1770
 
1771
+ if (computedDefaultCount > 0) {
1772
+ const computedResult = runComputedDefaults(
1773
+ workingResolved,
1774
+ defaultResolved,
1775
+ userVariantProps,
1776
+ filterOwnVariants,
1777
+ protectedVariantKeys,
1778
+ );
1779
+ workingResolved = computedResult.workingResolved;
1780
+ changedVariants = computedResult.changedVariants;
1781
+ }
1782
+ if (refine) {
1783
+ const refineResult = runRefineContext(
1784
+ workingResolved,
1785
+ userVariantProps,
1786
+ filterOwnVariants,
1787
+ false,
1788
+ true,
1789
+ protectedVariants,
1790
+ pendingProtectedVariants,
1791
+ protectedVariantKeys,
1792
+ );
1793
+ workingResolved = refineResult.workingResolved;
1794
+ if (refineResult.changedVariants) {
1795
+ changedVariants ??= {};
1796
+ Object.assign(changedVariants, refineResult.changedVariants);
1797
+ }
1798
+ }
1799
+
1553
1800
  return workingResolved;
1554
1801
  };
1555
1802
 
1556
1803
  const resolveRefine: ResolveRefineFn | null =
1557
- refine || extMetasWithRefineCount > 0
1804
+ refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0
1558
1805
  ? (
1559
1806
  resolved,
1560
1807
  userVariantProps,
@@ -1563,6 +1810,7 @@ export function create({
1563
1810
  protectedVariants,
1564
1811
  pendingProtectedVariants,
1565
1812
  protectedVariantKeys,
1813
+ incomingDefaultResolved = resolved,
1566
1814
  ) => {
1567
1815
  runState ??= { remaining: MAX_REFINE_RUNS };
1568
1816
  protectedVariants ??= {};
@@ -1577,6 +1825,10 @@ export function create({
1577
1825
  while (runState.remaining > 0) {
1578
1826
  runState.remaining -= 1;
1579
1827
  const nextPendingProtectedVariants: Record<string, unknown> = {};
1828
+ const defaultResolved = mergeProtectedIntoBase(
1829
+ incomingDefaultResolved,
1830
+ protectedVariants,
1831
+ );
1580
1832
  const nextResolved = resolveRefineOnce(
1581
1833
  workingResolved,
1582
1834
  userVariantProps,
@@ -1585,6 +1837,7 @@ export function create({
1585
1837
  protectedVariants,
1586
1838
  nextPendingProtectedVariants,
1587
1839
  protectedVariantKeys,
1840
+ defaultResolved,
1588
1841
  );
1589
1842
  let protectedChanged: boolean;
1590
1843
  if (pendingProtectedVariants) {
@@ -1646,17 +1899,11 @@ export function create({
1646
1899
  ): { className: string; style: StyleValue } => {
1647
1900
  const propsRecord = props as Record<string, unknown>;
1648
1901
 
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
1902
  let resolved: Record<string, unknown> = {};
1654
1903
  Object.assign(resolved, staticDefaults);
1655
1904
 
1656
1905
  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.
1906
+ if (refine || computedDefaultCount > 0 || extMetasWithRefineCount > 0) {
1660
1907
  const variantProps: Record<string, unknown> = {};
1661
1908
  for (let i = 0; i < variantKeysLength; i++) {
1662
1909
  const key = variantKeys[i];
@@ -1664,14 +1911,6 @@ export function create({
1664
1911
  variantProps[key] = propsRecord[key];
1665
1912
  }
1666
1913
  }
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
1914
  for (const k in variantProps) {
1676
1915
  if (!Object.hasOwn(variantProps, k)) continue;
1677
1916
  const v = variantProps[k];
@@ -1769,11 +2008,11 @@ export function create({
1769
2008
  const meta: ComponentMeta = {
1770
2009
  baseClass: computedBaseClass,
1771
2010
  staticDefaults,
1772
- resolveDefaults: resolveDefaultsFn,
1773
2011
  compute,
1774
2012
  resolveRefine,
1775
2013
  transformClass,
1776
2014
  functionVariantKeys,
2015
+ computedDefaultKeys,
1777
2016
  };
1778
2017
 
1779
2018
  const initComponent = <
@@ -1845,6 +2084,4 @@ export function create({
1845
2084
  return { cv, cx };
1846
2085
  }
1847
2086
 
1848
- function noop() {}
1849
-
1850
2087
  export const { cv, cx } = create();