clava 0.3.0 → 0.4.1

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
@@ -5,14 +5,13 @@ import type {
5
5
  ClassValue,
6
6
  ComponentProps,
7
7
  ComponentResult,
8
- Computed,
9
- ComputedVariants,
10
8
  ExtendableVariants,
11
9
  HTMLObjProps,
12
10
  HTMLProps,
13
11
  JSXProps,
14
12
  MergeVariants,
15
13
  ModalComponent,
14
+ Refine,
16
15
  SplitPropsFunction,
17
16
  StyleClassProps,
18
17
  StyleClassValue,
@@ -42,33 +41,33 @@ type ComputeFn = (
42
41
  skipValues: Record<string, Set<string>> | null,
43
42
  classesOut: ClsxClassValue[],
44
43
  styleOut: StyleValue,
45
- runState?: ComputedRunState,
44
+ runState?: RefineRunState,
46
45
  protectedVariants?: Record<string, unknown> | null,
47
46
  pendingProtectedVariants?: Record<string, unknown> | null,
48
47
  protectedVariantKeys?: Set<string> | null,
49
48
  ) => Record<string, unknown>;
50
49
 
51
- type ResolveComputedFn = (
50
+ type ResolveRefineFn = (
52
51
  resolved: Record<string, unknown>,
53
52
  userVariantProps: Record<string, unknown>,
54
53
  filterOwnVariants?: boolean,
55
- runState?: ComputedRunState,
54
+ runState?: RefineRunState,
56
55
  protectedVariants?: Record<string, unknown> | null,
57
56
  pendingProtectedVariants?: Record<string, unknown> | null,
58
57
  protectedVariantKeys?: Set<string> | null,
59
58
  ) => Record<string, unknown>;
60
59
 
61
- interface ComputedRunState {
60
+ interface RefineRunState {
62
61
  remaining: number;
63
- warned: boolean;
62
+ warned?: boolean;
64
63
  }
65
64
 
66
65
  // Internal metadata stored on components but hidden from public types.
67
66
  interface ComponentMeta {
68
67
  baseClass: string;
69
68
  staticDefaults: Record<string, unknown>;
70
- // Returns variants set via setDefaultVariants in the computed function chain.
71
- // null when this component has no resolveDefaults work to do (no `computed`
69
+ // Returns variants set via setDefaultVariants in the refine function chain.
70
+ // null when this component has no resolveDefaults work to do (no `refine`
72
71
  // and no extends with work).
73
72
  resolveDefaults:
74
73
  | ((
@@ -79,22 +78,39 @@ interface ComponentMeta {
79
78
  // Returns variant classes + style for this component, used by extending
80
79
  // components. Top-level rendering also routes through this.
81
80
  compute: ComputeFn;
82
- resolveComputed: ResolveComputedFn | null;
81
+ resolveRefine: ResolveRefineFn | null;
83
82
  // Reference identity is used to detect mixed-factory `extend`. When a
84
83
  // component is extended by a parent from a different `create()` call, the
85
84
  // parent applies this transform to the extend's contribution before joining,
86
85
  // preserving each factory's transform boundary.
87
86
  transformClass: (className: string) => string;
87
+ // Variant keys whose effective definition in this component's chain is a
88
+ // function. An extending component that supplies a non-function variant for
89
+ // the same key uses this to tell us to skip that key (matching the
90
+ // type-level "function variant is replaced by anything in the child" rule).
91
+ // Empty when no key in this chain is a function variant.
92
+ functionVariantKeys: Set<string>;
88
93
  }
89
94
 
90
95
  const META_KEY = "__meta";
91
96
 
97
+ interface ComponentWithMeta {
98
+ [META_KEY]?: ComponentMeta;
99
+ }
100
+
92
101
  const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
93
102
  string,
94
103
  unknown
95
104
  >;
96
105
 
97
- const MAX_COMPUTED_RUNS = 50;
106
+ const MAX_REFINE_RUNS = 50;
107
+
108
+ // Once a refine loop is within this many iterations of the cap, start tracking
109
+ // every variant key that changes between iterations so the warning can report
110
+ // every key that contributed to the oscillation, not just the keys that
111
+ // happened to flip on the final step. Convergent loops (the common case) exit
112
+ // well before this threshold and pay no per-iteration tracking cost.
113
+ const REFINE_UNSTABLE_TRACKING_WINDOW = 10;
98
114
 
99
115
  function areVariantsEqual(
100
116
  a: Record<string, unknown>,
@@ -111,16 +127,94 @@ function areVariantsEqual(
111
127
  return true;
112
128
  }
113
129
 
114
- function warnComputedLimit(runState: ComputedRunState): void {
130
+ interface CreationFrame {
131
+ stack?: string;
132
+ }
133
+
134
+ // Captures the call site of the function passed in `skipFn` so refine-limit
135
+ // warnings can point developers at the originating `cv()` call. Returns
136
+ // `undefined` in production so bundlers that replace `process.env.NODE_ENV` at
137
+ // build time can drop the entire warning machinery. The underlying `.stack`
138
+ // string is formatted lazily on first access in every major engine (V8,
139
+ // SpiderMonkey, JavaScriptCore), so holding the captured frame for the
140
+ // lifetime of the component is cheap when no warning fires.
141
+ function captureCreationFrame(skipFn: Function): CreationFrame | undefined {
142
+ if (process.env.NODE_ENV === "production") return undefined;
143
+ if (typeof Error.captureStackTrace === "function") {
144
+ const holder: CreationFrame = {};
145
+ Error.captureStackTrace(holder, skipFn);
146
+ return holder;
147
+ }
148
+ // Engines without `Error.captureStackTrace` (SpiderMonkey, JavaScriptCore)
149
+ // can't strip internal frames, but their `Error.stack` getter is still
150
+ // lazy, so returning the Error instance defers the format cost. The
151
+ // resulting trace includes 1–2 extra frames at the top from this helper and
152
+ // `cv` itself.
153
+ return new Error();
154
+ }
155
+
156
+ function formatCreationStack(frame: CreationFrame): string | undefined {
157
+ let stack = frame.stack;
158
+ if (!stack) return undefined;
159
+ // V8 prefixes the stack with a leading "Error" / "Error: message" line that
160
+ // isn't meaningful for a captured location — drop it.
161
+ const newlineIdx = stack.indexOf("\n");
162
+ if (newlineIdx > 0) {
163
+ const firstLine = stack.slice(0, newlineIdx);
164
+ if (firstLine === "Error" || firstLine.startsWith("Error:")) {
165
+ stack = stack.slice(newlineIdx + 1);
166
+ }
167
+ }
168
+ return stack;
169
+ }
170
+
171
+ // Accumulates the union of variant keys that differ between `prev` and `next`
172
+ // into `into`. Called on every non-converging iteration of the refine loop so
173
+ // the refine-limit warning can report any key that ever changed across runs,
174
+ // not just the keys that changed on the final iteration (two keys flipping at
175
+ // different cadences could otherwise hide each other on the last step).
176
+ function accumulateUnstableVariantKeys(
177
+ into: Set<string>,
178
+ prev: Record<string, unknown>,
179
+ next: Record<string, unknown>,
180
+ ): void {
181
+ for (const key in next) {
182
+ if (!Object.hasOwn(next, key)) continue;
183
+ if (!Object.is(prev[key], next[key])) {
184
+ into.add(key);
185
+ }
186
+ }
187
+ for (const key in prev) {
188
+ if (!Object.hasOwn(prev, key)) continue;
189
+ if (Object.hasOwn(next, key)) continue;
190
+ into.add(key);
191
+ }
192
+ }
193
+
194
+ function warnRefineLimit(
195
+ runState: RefineRunState,
196
+ creationFrame: CreationFrame | undefined,
197
+ unstableKeys: Set<string> | null,
198
+ ): void {
199
+ // Bundlers are expected to replace this branch with a production literal,
200
+ // allowing warning-only code below to be removed from consumer bundles.
201
+ if (process.env.NODE_ENV === "production") return;
115
202
  if (runState.warned) return;
116
203
  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
- );
204
+ let message =
205
+ "Clava: Maximum refine iterations exceeded. This can happen when a " +
206
+ "refine callback calls setVariants or setDefaultVariants, but one " +
207
+ "of the variants changes on every run.";
208
+ if (unstableKeys && unstableKeys.size > 0) {
209
+ message += `\nVariant(s) that did not stabilize: ${Array.from(unstableKeys).join(", ")}.`;
123
210
  }
211
+ if (creationFrame) {
212
+ const creationStack = formatCreationStack(creationFrame);
213
+ if (creationStack) {
214
+ message += `\nComponent created at:\n${creationStack}`;
215
+ }
216
+ }
217
+ console.warn(message);
124
218
  }
125
219
 
126
220
  function getExtUserVariantProps(
@@ -149,7 +243,9 @@ function mergeVariants(
149
243
  for (const key in source) {
150
244
  if (!Object.hasOwn(source, key)) continue;
151
245
  const value = source[key];
152
- if (!Object.is(target[key], value)) changed = true;
246
+ if (!Object.is(target[key], value)) {
247
+ changed = true;
248
+ }
153
249
  target[key] = value;
154
250
  }
155
251
  return changed;
@@ -158,21 +254,22 @@ function mergeVariants(
158
254
  if (!Object.hasOwn(source, key)) continue;
159
255
  if (skipKeys.has(key)) continue;
160
256
  const value = source[key];
161
- if (!Object.is(target[key], value)) changed = true;
257
+ if (!Object.is(target[key], value)) {
258
+ changed = true;
259
+ }
162
260
  target[key] = value;
163
261
  }
164
262
  return changed;
165
263
  }
166
264
 
167
- // Dynamic property access on function requires cast through unknown.
265
+ // Components carry internal metadata on a non-public property so user-facing
266
+ // component types stay clean.
168
267
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
169
- return (component as unknown as Record<string, unknown>)[META_KEY] as
170
- | ComponentMeta
171
- | undefined;
268
+ return (component as AnyComponent & ComponentWithMeta)[META_KEY];
172
269
  }
173
270
 
174
271
  function setComponentMeta(component: AnyComponent, meta: ComponentMeta): void {
175
- (component as unknown as Record<string, unknown>)[META_KEY] = meta;
272
+ (component as AnyComponent & ComponentWithMeta)[META_KEY] = meta;
176
273
  }
177
274
 
178
275
  export type {
@@ -202,16 +299,14 @@ export type Variant<
202
299
 
203
300
  export interface CVConfig<
204
301
  V extends Variants = {},
205
- CV extends ComputedVariants = {},
206
302
  E extends AnyComponent[] = [],
207
303
  > {
208
304
  extend?: E;
209
305
  class?: ClassValue;
210
306
  style?: StyleValue;
211
307
  variants?: ExtendableVariants<V, E>;
212
- computedVariants?: CV;
213
- defaultVariants?: VariantValues<MergeVariants<V, CV, E>>;
214
- computed?: Computed<MergeVariants<V, CV, E>>;
308
+ defaultVariants?: VariantValues<MergeVariants<V, E>>;
309
+ refine?: Refine<MergeVariants<V, E>>;
215
310
  }
216
311
 
217
312
  interface CreateParams {
@@ -282,7 +377,7 @@ function extractClassAndStylePrebuilt(value: unknown): PrebuiltValue {
282
377
  * components.
283
378
  */
284
379
  function collectVariantKeys(
285
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
380
+ config: CVConfig<Variants, AnyComponent[]>,
286
381
  ): string[] {
287
382
  const keys = new Set<string>();
288
383
 
@@ -307,32 +402,31 @@ function collectVariantKeys(
307
402
  }
308
403
  }
309
404
 
310
- if (config.computedVariants) {
311
- for (const key in config.computedVariants) {
312
- if (!Object.hasOwn(config.computedVariants, key)) continue;
313
- keys.add(key);
314
- }
315
- }
316
-
317
405
  return Array.from(keys);
318
406
  }
319
407
 
320
408
  function isVariantDisabled(
321
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
409
+ config: CVConfig<Variants, AnyComponent[]>,
322
410
  key: string,
323
411
  ): boolean {
324
412
  return config.variants?.[key] === null;
325
413
  }
326
414
 
327
415
  function getVariantValueKey(value: unknown): string | undefined {
328
- if (typeof value === "string") return value;
329
- if (typeof value === "number") return String(value);
330
- if (typeof value === "boolean") return String(value);
416
+ if (typeof value === "string") {
417
+ return value;
418
+ }
419
+ if (typeof value === "number") {
420
+ return String(value);
421
+ }
422
+ if (typeof value === "boolean") {
423
+ return String(value);
424
+ }
331
425
  return undefined;
332
426
  }
333
427
 
334
428
  function isVariantValueDisabled(
335
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
429
+ config: CVConfig<Variants, AnyComponent[]>,
336
430
  key: string,
337
431
  value: unknown,
338
432
  ): boolean {
@@ -344,10 +438,12 @@ function isVariantValueDisabled(
344
438
  }
345
439
 
346
440
  function collectDisabledVariantKeys(
347
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
441
+ config: CVConfig<Variants, AnyComponent[]>,
348
442
  ): Set<string> {
349
443
  const keys = new Set<string>();
350
- if (!config.variants) return keys;
444
+ if (!config.variants) {
445
+ return keys;
446
+ }
351
447
  for (const key in config.variants) {
352
448
  if (!Object.hasOwn(config.variants, key)) continue;
353
449
  if ((config.variants as Record<string, unknown>)[key] === null) {
@@ -358,10 +454,12 @@ function collectDisabledVariantKeys(
358
454
  }
359
455
 
360
456
  function collectDisabledVariantValues(
361
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
457
+ config: CVConfig<Variants, AnyComponent[]>,
362
458
  ): Record<string, Set<string>> {
363
459
  const values: Record<string, Set<string>> = {};
364
- if (!config.variants) return values;
460
+ if (!config.variants) {
461
+ return values;
462
+ }
365
463
  for (const key in config.variants) {
366
464
  if (!Object.hasOwn(config.variants, key)) continue;
367
465
  const variant = (config.variants as Record<string, unknown>)[key];
@@ -401,14 +499,22 @@ function normalizeKeySource(source: unknown): NormalizedSource {
401
499
  };
402
500
  }
403
501
 
404
- if (!source) return EMPTY_SOURCE;
502
+ if (!source) {
503
+ return EMPTY_SOURCE;
504
+ }
405
505
  if (typeof source !== "object" && typeof source !== "function") {
406
506
  return EMPTY_SOURCE;
407
507
  }
408
508
  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;
509
+ if (typeof typed.getVariants !== "function") {
510
+ return EMPTY_SOURCE;
511
+ }
512
+ if (!Array.isArray(typed.propKeys)) {
513
+ return EMPTY_SOURCE;
514
+ }
515
+ if (!Array.isArray(typed.variantKeys)) {
516
+ return EMPTY_SOURCE;
517
+ }
412
518
 
413
519
  return {
414
520
  propKeys: typed.propKeys as string[],
@@ -544,7 +650,9 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
544
650
  if (!Object.hasOwn(variantDef, key)) continue;
545
651
  const value = variantDef[key];
546
652
  if (value === null) {
547
- if (!disabledValues) disabledValues = new Set<string>();
653
+ if (!disabledValues) {
654
+ disabledValues = new Set<string>();
655
+ }
548
656
  disabledValues.add(key);
549
657
  continue;
550
658
  }
@@ -565,14 +673,10 @@ export function create({
565
673
  }: CreateParams = {}) {
566
674
  const cx = (...classes: ClsxClassValue[]) => transformClass(clsx(...classes));
567
675
 
568
- const cv = <
569
- V extends Variants = {},
570
- CV extends ComputedVariants = {},
571
- const E extends AnyComponent[] = [],
572
- >(
573
- config: CVConfig<V, CV, E> = {},
574
- ): CVComponent<V, CV, E> => {
575
- type MergedVariants = MergeVariants<V, CV, E>;
676
+ const cv = <V extends Variants = {}, const E extends AnyComponent[] = []>(
677
+ config: CVConfig<V, E> = {},
678
+ ): CVComponent<V, E> => {
679
+ type MergedVariants = MergeVariants<V, E>;
576
680
 
577
681
  // ----- Pre-computed at creation time -----
578
682
  const variantKeys = collectVariantKeys(config);
@@ -589,41 +693,34 @@ export function create({
589
693
  const extend = config.extend;
590
694
  const hasExtend = !!extend && extend.length > 0;
591
695
  const variants = config.variants;
592
- const computedVariantsCfg = config.computedVariants;
593
- const computed = config.computed;
696
+ const refine = config.refine;
594
697
  const baseStyle = config.style;
595
698
  const hasBaseStyle = !!baseStyle;
596
699
 
597
- // Pre-build variant entries for fast iteration. For each variant key in
598
- // `variants`, we have a name and a PrebuiltVariant with normalized values.
700
+ // Split `variants` entries into static entries (object/shorthand) and
701
+ // function-variant entries. Static entries are pre-built into
702
+ // PrebuiltVariant for fast iteration. Function-variant entries override
703
+ // any same-key inherited variant (see `staticExtSkipKeys`).
599
704
  const variantEntryNames: string[] = [];
600
705
  const variantEntryDefs: PrebuiltVariant[] = [];
706
+ const functionVariantNames: string[] = [];
707
+ const functionVariantFns: Array<(value: unknown) => unknown> = [];
601
708
  if (variants) {
602
709
  for (const name in variants) {
603
710
  if (!Object.hasOwn(variants, name)) continue;
604
711
  const variant = (variants as Record<string, unknown>)[name];
605
712
  if (variant === null) continue;
713
+ if (typeof variant === "function") {
714
+ functionVariantNames.push(name);
715
+ functionVariantFns.push(variant as (value: unknown) => unknown);
716
+ continue;
717
+ }
606
718
  variantEntryNames.push(name);
607
719
  variantEntryDefs.push(buildPrebuiltVariant(variant));
608
720
  }
609
721
  }
610
722
  const variantEntryCount = variantEntryNames.length;
611
-
612
- // Pre-built computed-variants entries.
613
- const computedVariantNames: string[] = [];
614
- const computedVariantFns: Array<(value: unknown) => unknown> = [];
615
- if (computedVariantsCfg) {
616
- for (const name in computedVariantsCfg) {
617
- if (!Object.hasOwn(computedVariantsCfg, name)) continue;
618
- computedVariantNames.push(name);
619
- computedVariantFns.push(
620
- (computedVariantsCfg as Record<string, (value: unknown) => unknown>)[
621
- name
622
- ] as (value: unknown) => unknown,
623
- );
624
- }
625
- }
626
- const computedVariantCount = computedVariantNames.length;
723
+ const functionVariantCount = functionVariantNames.length;
627
724
 
628
725
  // Pre-compute static defaults. Includes:
629
726
  // - extended components' static defaults
@@ -634,7 +731,9 @@ export function create({
634
731
  if (extend) {
635
732
  for (const ext of extend) {
636
733
  const meta = getComponentMeta(ext);
637
- if (meta) Object.assign(staticDefaults, meta.staticDefaults);
734
+ if (meta) {
735
+ Object.assign(staticDefaults, meta.staticDefaults);
736
+ }
638
737
  }
639
738
  }
640
739
  if (variants) {
@@ -702,28 +801,94 @@ export function create({
702
801
  }
703
802
  const extCount = extMetas.length;
704
803
 
705
- // Filter to only extends with computed work in their chain. `resolveDefaults`
706
- // and `resolveComputed` are populated from the same transitive condition,
804
+ // Filter to only extends with refine work in their chain. `resolveDefaults`
805
+ // and `resolveRefine` are populated from the same transitive condition,
707
806
  // so one bucket is enough for both resolver paths.
708
- const extMetasWithComputed: ComponentMeta[] = [];
807
+ const extMetasWithRefine: ComponentMeta[] = [];
709
808
  for (let i = 0; i < extCount; i++) {
710
809
  const meta = extMetas[i];
711
810
  if (meta.resolveDefaults) {
712
- extMetasWithComputed.push(meta);
811
+ extMetasWithRefine.push(meta);
812
+ }
813
+ }
814
+ const extMetasWithRefineCount = extMetasWithRefine.length;
815
+ const shouldCollectChangedVariants = extMetasWithRefineCount > 0;
816
+
817
+ // Call-site frame captured at the `cv()` call site so refine-limit warnings
818
+ // can point developers at the component definition. Skipped entirely for
819
+ // components that can never enter the refine loop, and stripped in
820
+ // production via the NODE_ENV guard inside `captureCreationFrame`. The
821
+ // frame is captured at creation time but the underlying `.stack` string is
822
+ // formatted lazily on first access, so component creation stays cheap
823
+ // unless the warning actually fires.
824
+ const canTriggerRefineWarning = !!refine || extMetasWithRefineCount > 0;
825
+ const creationFrame = canTriggerRefineWarning
826
+ ? captureCreationFrame(cv)
827
+ : undefined;
828
+
829
+ // Function variant keys inherited from extends, filtered through this
830
+ // component's own variants: a static (object/shorthand) variant in this
831
+ // component replaces an inherited function variant for the same key.
832
+ // The closure is exposed on `ComponentMeta` so any further extending
833
+ // component can detect "ancestor's effective variant for K is a function"
834
+ // and skip it when overriding K with a non-function.
835
+ const functionVariantKeys = new Set<string>();
836
+ for (let i = 0; i < extCount; i++) {
837
+ const fnKeys = extMetas[i].functionVariantKeys;
838
+ for (const k of fnKeys) {
839
+ if (disabledVariantKeys.has(k)) continue;
840
+ functionVariantKeys.add(k);
841
+ }
842
+ }
843
+ for (let i = 0; i < functionVariantCount; i++) {
844
+ functionVariantKeys.add(functionVariantNames[i]);
845
+ }
846
+ for (let i = 0; i < variantEntryCount; i++) {
847
+ // A static variant in this component replaces an inherited function
848
+ // variant for the same key; from this component onward, the key is no
849
+ // longer a function variant.
850
+ functionVariantKeys.delete(variantEntryNames[i]);
851
+ }
852
+
853
+ // Static-variant keys in this component that override an inherited
854
+ // function variant. Type-level merge says child fully replaces, so the
855
+ // ancestor's function must not run with the child's (object-typed) value.
856
+ let staticVariantsOverridingExtFn: string[] | null = null;
857
+ if (variantEntryCount > 0 && extCount > 0) {
858
+ for (let i = 0; i < variantEntryCount; i++) {
859
+ const name = variantEntryNames[i];
860
+ for (let j = 0; j < extCount; j++) {
861
+ if (extMetas[j].functionVariantKeys.has(name)) {
862
+ if (!staticVariantsOverridingExtFn) {
863
+ staticVariantsOverridingExtFn = [];
864
+ }
865
+ staticVariantsOverridingExtFn.push(name);
866
+ break;
867
+ }
868
+ }
713
869
  }
714
870
  }
715
- const extMetasWithComputedCount = extMetasWithComputed.length;
716
- const shouldCollectChangedVariants = extMetasWithComputedCount > 0;
717
871
 
718
872
  // Pre-compute static skip key/value sets to pass to extends. These never
719
873
  // change across calls — when caller passes no skip sets, we reuse the same
720
874
  // object and avoid Set allocation.
721
875
  let staticExtSkipKeys: Set<string> | null = null;
722
- if (hasDisabledVariantKeys || computedVariantCount > 0) {
876
+ if (
877
+ hasDisabledVariantKeys ||
878
+ functionVariantCount > 0 ||
879
+ staticVariantsOverridingExtFn !== null
880
+ ) {
723
881
  staticExtSkipKeys = new Set<string>();
724
- for (const k of disabledVariantKeys) staticExtSkipKeys.add(k);
725
- for (let i = 0; i < computedVariantCount; i++) {
726
- staticExtSkipKeys.add(computedVariantNames[i]);
882
+ for (const k of disabledVariantKeys) {
883
+ staticExtSkipKeys.add(k);
884
+ }
885
+ for (let i = 0; i < functionVariantCount; i++) {
886
+ staticExtSkipKeys.add(functionVariantNames[i]);
887
+ }
888
+ if (staticVariantsOverridingExtFn) {
889
+ for (const k of staticVariantsOverridingExtFn) {
890
+ staticExtSkipKeys.add(k);
891
+ }
727
892
  }
728
893
  }
729
894
  // Skip values are passed directly to extends. We can reuse the same object
@@ -740,7 +905,9 @@ export function create({
740
905
  ): void {
741
906
  if (!hasAnyDisabled) {
742
907
  for (const key in input) {
743
- if (Object.hasOwn(input, key)) out[key] = input[key];
908
+ if (Object.hasOwn(input, key)) {
909
+ out[key] = input[key];
910
+ }
744
911
  }
745
912
  return;
746
913
  }
@@ -760,12 +927,12 @@ export function create({
760
927
 
761
928
  // Pre-create resolveDefaults function — used by parents during their
762
929
  // `resolveVariantsHot`. Returns the variants set via setDefaultVariants in
763
- // the computed function chain.
930
+ // the refine function chain.
764
931
  //
765
- // When this component has no `computed` and no `extend` with work, the
932
+ // When this component has no `refine` and no `extend` with work, the
766
933
  // function is null — callers can skip iterating it entirely.
767
934
  const resolveDefaultsFn: ComponentMeta["resolveDefaults"] =
768
- computed || extMetasWithComputedCount > 0
935
+ refine || extMetasWithRefineCount > 0
769
936
  ? (
770
937
  childDefaults: Record<string, unknown>,
771
938
  userProps: Record<string, unknown> = EMPTY_DEFAULTS,
@@ -787,21 +954,21 @@ export function create({
787
954
  resolvedVariants[key] = v;
788
955
  }
789
956
 
790
- const computedDefaults: Record<string, unknown> = {};
957
+ const refineDefaults: Record<string, unknown> = {};
791
958
 
792
- for (let i = 0; i < extMetasWithComputedCount; i++) {
793
- const extDefaults = extMetasWithComputed[i].resolveDefaults!(
959
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
960
+ const extDefaults = extMetasWithRefine[i].resolveDefaults!(
794
961
  childDefaults,
795
962
  userProps,
796
963
  );
797
964
  for (const k in extDefaults) {
798
965
  if (!Object.hasOwn(extDefaults, k)) continue;
799
- computedDefaults[k] = extDefaults[k];
966
+ refineDefaults[k] = extDefaults[k];
800
967
  }
801
968
  }
802
969
 
803
- if (computed) {
804
- // Filter to own variant keys so `computed.ctx.variants` matches
970
+ if (refine) {
971
+ // Filter to own variant keys so `ctx.variants` matches
805
972
  // `VariantValues<V>` when this component is used as an extend by
806
973
  // a parent that adds extra variant keys (those keys would
807
974
  // otherwise leak through `userProps`).
@@ -812,7 +979,7 @@ export function create({
812
979
  ownVariants[k] = resolvedVariants[k];
813
980
  }
814
981
  }
815
- computed({
982
+ refine({
816
983
  variants: ownVariants as VariantValues<Record<string, unknown>>,
817
984
  setVariants: noop,
818
985
  setDefaultVariants: (newDefaults) => {
@@ -822,7 +989,7 @@ export function create({
822
989
  if (userProps[key] !== undefined) continue;
823
990
  if (isVariantDisabled(config, key)) continue;
824
991
  if (isVariantValueDisabled(config, key, value)) continue;
825
- computedDefaults[key] = value;
992
+ refineDefaults[key] = value;
826
993
  }
827
994
  },
828
995
  addClass: noop,
@@ -830,12 +997,12 @@ export function create({
830
997
  });
831
998
  }
832
999
 
833
- return computedDefaults;
1000
+ return refineDefaults;
834
1001
  }
835
1002
  : null;
836
1003
 
837
1004
  // Hot path: resolve variants by merging static defaults + extends'
838
- // computed defaults + user-provided props.
1005
+ // refine defaults + user-provided props.
839
1006
  function resolveVariantsHot(
840
1007
  propsVariants: Record<string, unknown>,
841
1008
  ): Record<string, unknown> {
@@ -843,14 +1010,14 @@ export function create({
843
1010
  const defaults: Record<string, unknown> = {};
844
1011
  Object.assign(defaults, staticDefaults);
845
1012
 
846
- // Apply computed defaults from extended components (only those that have
1013
+ // Apply refine defaults from extended components (only those that have
847
1014
  // actual work to do).
848
- for (let i = 0; i < extMetasWithComputedCount; i++) {
849
- const meta = extMetasWithComputed[i];
850
- const extComputed = meta.resolveDefaults!(defaults, propsVariants);
851
- for (const k in extComputed) {
852
- if (!Object.hasOwn(extComputed, k)) continue;
853
- defaults[k] = extComputed[k];
1015
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
1016
+ const meta = extMetasWithRefine[i];
1017
+ const extDefaults = meta.resolveDefaults!(defaults, propsVariants);
1018
+ for (const k in extDefaults) {
1019
+ if (!Object.hasOwn(extDefaults, k)) continue;
1020
+ defaults[k] = extDefaults[k];
854
1021
  }
855
1022
  }
856
1023
 
@@ -864,7 +1031,9 @@ export function create({
864
1031
  defaults[k] = v;
865
1032
  }
866
1033
 
867
- if (!hasAnyDisabled) return defaults;
1034
+ if (!hasAnyDisabled) {
1035
+ return defaults;
1036
+ }
868
1037
 
869
1038
  // Filter disabled
870
1039
  const result: Record<string, unknown> = {};
@@ -872,7 +1041,7 @@ export function create({
872
1041
  return result;
873
1042
  }
874
1043
 
875
- const runComputedContext = (
1044
+ const runRefineContext = (
876
1045
  resolved: Record<string, unknown>,
877
1046
  userVariantProps: Record<string, unknown>,
878
1047
  filterOwnVariants: boolean,
@@ -891,28 +1060,32 @@ export function create({
891
1060
  let cStyle: StyleValue | null = null;
892
1061
  let changedVariants: Record<string, unknown> | null = null;
893
1062
 
894
- if (computed) {
1063
+ if (refine) {
895
1064
  let ownVariants = resolved;
896
1065
  if (filterOwnVariants) {
897
1066
  // When this component is being extended, `resolved` is the parent's
898
1067
  // workingResolved (a superset of our variant keys). Filter to our own
899
- // keys for `ctx.variants` so the user's `computed` callback sees the
1068
+ // keys for `ctx.variants` so the user's `refine` callback sees the
900
1069
  // shape declared by `VariantValues<V>` and not foreign parent keys.
901
1070
  const filteredVariants: Record<string, unknown> = {};
902
1071
  for (let i = 0; i < variantKeysLength; i++) {
903
1072
  const k = variantKeys[i];
904
- if (Object.hasOwn(resolved, k)) filteredVariants[k] = resolved[k];
1073
+ if (Object.hasOwn(resolved, k)) {
1074
+ filteredVariants[k] = resolved[k];
1075
+ }
905
1076
  }
906
1077
  ownVariants = filteredVariants;
907
1078
  }
908
- // Lazy-init updatedVariants — many computeds only inspect `variants`
909
- // or call setDefaultVariants for keys the user already set, so the
910
- // copy is unnecessary in the common case.
1079
+ // Lazy-init updatedVariants — many refine callbacks only inspect
1080
+ // `variants` or call setDefaultVariants for keys the user already set,
1081
+ // so the copy is unnecessary in the common case.
911
1082
  let updatedVariants: Record<string, unknown> | null = null;
912
1083
  const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
913
1084
  let localCStyle: StyleValue | null = null;
914
1085
  const ensureUpdated = (): Record<string, unknown> => {
915
- if (updatedVariants) return updatedVariants;
1086
+ if (updatedVariants) {
1087
+ return updatedVariants;
1088
+ }
916
1089
  const u: Record<string, unknown> = {};
917
1090
  Object.assign(u, ownVariants);
918
1091
  updatedVariants = u;
@@ -924,7 +1097,9 @@ export function create({
924
1097
  protect = false,
925
1098
  ) => {
926
1099
  if (shouldCollectChangedVariants) {
927
- if (!changedVariants) changedVariants = {};
1100
+ if (!changedVariants) {
1101
+ changedVariants = {};
1102
+ }
928
1103
  changedVariants[key] = value;
929
1104
  }
930
1105
  if (protect && protectedVariants) {
@@ -999,16 +1174,22 @@ export function create({
999
1174
  },
1000
1175
  addStyle: (newStyle: StyleValue) => {
1001
1176
  if (!collectOutput) return;
1002
- if (!localCStyle) localCStyle = {};
1177
+ if (!localCStyle) {
1178
+ localCStyle = {};
1179
+ }
1003
1180
  Object.assign(localCStyle, newStyle);
1004
1181
  },
1005
1182
  };
1006
- const result = computed(ctx);
1183
+ const result = refine(ctx);
1007
1184
  if (collectOutput && result != null) {
1008
1185
  const r = extractClassAndStylePrebuilt(result);
1009
- if (r.class != null) localCClasses?.push(r.class);
1186
+ if (r.class != null) {
1187
+ localCClasses?.push(r.class);
1188
+ }
1010
1189
  if (r.style) {
1011
- if (!localCStyle) localCStyle = {};
1190
+ if (!localCStyle) {
1191
+ localCStyle = {};
1192
+ }
1012
1193
  Object.assign(localCStyle, r.style);
1013
1194
  }
1014
1195
  }
@@ -1052,14 +1233,14 @@ export function create({
1052
1233
  pendingProtectedVariants,
1053
1234
  protectedVariantKeys,
1054
1235
  ) => {
1055
- // Run `computed` (if any). May modify resolved variants and emit classes
1236
+ // Run `refine` (if any). May modify resolved variants and emit classes
1056
1237
  // and styles.
1057
1238
  let workingResolved = resolved;
1058
1239
  let cClasses: ClassValue[] | null = null;
1059
1240
  let cStyle: StyleValue | null = null;
1060
1241
  let changedVariants: Record<string, unknown> | null = null;
1061
- if (computed) {
1062
- const computedResult = runComputedContext(
1242
+ if (refine) {
1243
+ const refineResult = runRefineContext(
1063
1244
  resolved,
1064
1245
  userVariantProps,
1065
1246
  true,
@@ -1068,20 +1249,20 @@ export function create({
1068
1249
  pendingProtectedVariants,
1069
1250
  protectedVariantKeys,
1070
1251
  );
1071
- workingResolved = computedResult.workingResolved;
1072
- cClasses = computedResult.classes;
1073
- cStyle = computedResult.style;
1074
- changedVariants = computedResult.changedVariants;
1252
+ workingResolved = refineResult.workingResolved;
1253
+ cClasses = refineResult.classes;
1254
+ cStyle = refineResult.style;
1255
+ changedVariants = refineResult.changedVariants;
1075
1256
  }
1076
1257
 
1077
1258
  // Run extends' contributions first (their full classes + styles) so our
1078
1259
  // own base style and variants apply on top, matching the original
1079
1260
  // ext1 → ext2 → … → current ordering.
1080
1261
  //
1081
- // Pass explicit user values plus computed changes as the extends'
1082
- // `userVariantProps`. This lets more-specific computed decisions stick
1262
+ // Pass explicit user values plus refine changes as the extends'
1263
+ // `userVariantProps`. This lets more-specific refine decisions stick
1083
1264
  // across re-runs while inherited static defaults can still be refined by
1084
- // the extended component's own computed chain.
1265
+ // the extended component's own refine chain.
1085
1266
  if (hasExtend) {
1086
1267
  // Build skip sets to pass to extends. Reuse precomputed values when no
1087
1268
  // caller-provided sets need merging.
@@ -1092,7 +1273,9 @@ export function create({
1092
1273
  extSkipKeys = skipKeys;
1093
1274
  } else {
1094
1275
  extSkipKeys = new Set(skipKeys);
1095
- for (const k of staticExtSkipKeys) extSkipKeys.add(k);
1276
+ for (const k of staticExtSkipKeys) {
1277
+ extSkipKeys.add(k);
1278
+ }
1096
1279
  }
1097
1280
 
1098
1281
  let extSkipVals: Record<string, Set<string>> | null;
@@ -1109,7 +1292,9 @@ export function create({
1109
1292
  const existing = extSkipVals[k];
1110
1293
  if (existing) {
1111
1294
  const merged = new Set<string>(existing);
1112
- for (const v of staticExtSkipValues[k]) merged.add(v);
1295
+ for (const v of staticExtSkipValues[k]) {
1296
+ merged.add(v);
1297
+ }
1113
1298
  extSkipVals[k] = merged;
1114
1299
  } else {
1115
1300
  extSkipVals[k] = staticExtSkipValues[k];
@@ -1118,7 +1303,7 @@ export function create({
1118
1303
  }
1119
1304
 
1120
1305
  const extUserVariantProps =
1121
- extMetasWithComputedCount > 0
1306
+ extMetasWithRefineCount > 0
1122
1307
  ? getExtUserVariantProps(
1123
1308
  userVariantProps,
1124
1309
  protectedVariants ?? null,
@@ -1165,21 +1350,24 @@ export function create({
1165
1350
  protectedVariantKeys,
1166
1351
  );
1167
1352
  }
1168
- // Only sync protected variants when a child computed resolver can
1353
+ // Only sync protected variants when a child refine resolver can
1169
1354
  // observe them. Otherwise extUserVariantProps may alias caller props.
1170
- if (protectedVariants && extMetasWithComputedCount > 0) {
1355
+ if (protectedVariants && extMetasWithRefineCount > 0) {
1171
1356
  Object.assign(extUserVariantProps, protectedVariants);
1172
1357
  }
1173
1358
  }
1174
1359
  }
1175
1360
 
1176
1361
  // Apply own base style (after extends' styles, matching original order).
1177
- if (hasBaseStyle) Object.assign(styleOut, baseStyle);
1362
+ if (hasBaseStyle) {
1363
+ Object.assign(styleOut, baseStyle);
1364
+ }
1178
1365
 
1179
1366
  // Apply own variants. Skip keys/values come from caller (e.g., parent
1180
- // wants its own computedVariants to override this variant).
1367
+ // wants its own function variant to override this variant).
1181
1368
  // `variantEntryNames` already excludes disabled keys (those with `null`
1182
- // value in config), so we don't re-check `disabledVariantKeys` here.
1369
+ // value in config) and function variants, so we don't re-check
1370
+ // `disabledVariantKeys` here.
1183
1371
  const ownSkipKeys = skipKeys;
1184
1372
  const ownSkipValues = skipValues;
1185
1373
  for (let i = 0; i < variantEntryCount; i++) {
@@ -1208,18 +1396,28 @@ export function create({
1208
1396
  if (selectedKey == null) continue;
1209
1397
  const v = variant.values[selectedKey];
1210
1398
  if (!v) continue;
1211
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1212
- if (v.style) Object.assign(styleOut, v.style);
1399
+ if (v.class != null) {
1400
+ classesOut.push(v.class as ClsxClassValue);
1401
+ }
1402
+ if (v.style) {
1403
+ Object.assign(styleOut, v.style);
1404
+ }
1213
1405
  } else if (variant.shorthand && selectedValue === true) {
1214
1406
  const v = variant.shorthand;
1215
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1216
- if (v.style) Object.assign(styleOut, v.style);
1407
+ if (v.class != null) {
1408
+ classesOut.push(v.class as ClsxClassValue);
1409
+ }
1410
+ if (v.style) {
1411
+ Object.assign(styleOut, v.style);
1412
+ }
1217
1413
  }
1218
1414
  }
1219
1415
 
1220
- // Apply computedVariants.
1221
- for (let i = 0; i < computedVariantCount; i++) {
1222
- const variantName = computedVariantNames[i];
1416
+ // Apply function variants — entries in `variants` whose value is a
1417
+ // function. They run after static variants and override any same-key
1418
+ // inherited variant via `staticExtSkipKeys`.
1419
+ for (let i = 0; i < functionVariantCount; i++) {
1420
+ const variantName = functionVariantNames[i];
1223
1421
  if (ownSkipKeys && ownSkipKeys.has(variantName)) continue;
1224
1422
  const selectedValue = workingResolved[variantName];
1225
1423
  if (selectedValue === undefined) continue;
@@ -1231,52 +1429,35 @@ export function create({
1231
1429
  ) {
1232
1430
  continue;
1233
1431
  }
1234
- const fn = computedVariantFns[i];
1432
+ const fn = functionVariantFns[i];
1235
1433
  const computedResult = fn(selectedValue);
1236
1434
  if (computedResult == null) continue;
1237
1435
  const r = extractClassAndStylePrebuilt(computedResult);
1238
- if (r.class != null) classesOut.push(r.class as ClsxClassValue);
1239
- if (r.style) Object.assign(styleOut, r.style);
1436
+ if (r.class != null) {
1437
+ classesOut.push(r.class as ClsxClassValue);
1438
+ }
1439
+ if (r.style) {
1440
+ Object.assign(styleOut, r.style);
1441
+ }
1240
1442
  }
1241
1443
 
1242
- // Apply `computed` results — must come after own variants/computedVariants.
1444
+ // Apply `refine` results — must come after own variants (static and
1445
+ // function).
1243
1446
  if (cClasses) {
1244
1447
  for (let i = 0; i < cClasses.length; i++) {
1245
1448
  classesOut.push(cClasses[i] as ClsxClassValue);
1246
1449
  }
1247
1450
  }
1248
- if (cStyle) Object.assign(styleOut, cStyle);
1451
+ if (cStyle) {
1452
+ Object.assign(styleOut, cStyle);
1453
+ }
1249
1454
 
1250
1455
  return workingResolved;
1251
1456
  };
1252
1457
 
1253
1458
  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
- }
1459
+ !refine && extMetasWithRefineCount === 0
1460
+ ? computeOnce
1280
1461
  : (
1281
1462
  resolved,
1282
1463
  userVariantProps,
@@ -1289,10 +1470,15 @@ export function create({
1289
1470
  pendingProtectedVariants,
1290
1471
  protectedVariantKeys,
1291
1472
  ) => {
1292
- runState ??= { remaining: MAX_COMPUTED_RUNS, warned: false };
1473
+ runState ??= { remaining: MAX_REFINE_RUNS };
1293
1474
  protectedVariants ??= {};
1294
1475
  protectedVariantKeys ??= new Set<string>();
1295
1476
  let workingResolved = resolved;
1477
+ // Union of variant keys that differed on non-converging iterations
1478
+ // inside the tracking window, so the refine-limit warning can name
1479
+ // every variant that contributed to the late-stage oscillation.
1480
+ // Lazy-init keeps convergent loops allocation-free.
1481
+ let unstableKeys: Set<string> | null = null;
1296
1482
  let lastClasses: ClsxClassValue[] = [];
1297
1483
  let lastStyle: StyleValue = {};
1298
1484
  let isFirstRun = true;
@@ -1356,10 +1542,24 @@ export function create({
1356
1542
  return nextResolved;
1357
1543
  }
1358
1544
 
1545
+ if (
1546
+ process.env.NODE_ENV !== "production" &&
1547
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1548
+ ) {
1549
+ if (!unstableKeys) {
1550
+ unstableKeys = new Set<string>();
1551
+ }
1552
+ accumulateUnstableVariantKeys(
1553
+ unstableKeys,
1554
+ workingResolved,
1555
+ nextResolved,
1556
+ );
1557
+ }
1558
+
1359
1559
  if (useDirectOutput && runState.remaining === 0) {
1360
1560
  // Keep the direct output from the last allowed run. Rolling
1361
1561
  // back here would drop it before the fallback copy below.
1362
- warnComputedLimit(runState);
1562
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1363
1563
  return nextResolved;
1364
1564
  }
1365
1565
 
@@ -1379,7 +1579,7 @@ export function create({
1379
1579
  isFirstRun = false;
1380
1580
  }
1381
1581
 
1382
- warnComputedLimit(runState);
1582
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1383
1583
 
1384
1584
  for (let i = 0; i < lastClasses.length; i++) {
1385
1585
  classesOut.push(lastClasses[i]);
@@ -1388,7 +1588,7 @@ export function create({
1388
1588
  return workingResolved;
1389
1589
  };
1390
1590
 
1391
- const resolveComputedOnce: ResolveComputedFn = (
1591
+ const resolveRefineOnce: ResolveRefineFn = (
1392
1592
  resolved,
1393
1593
  userVariantProps,
1394
1594
  filterOwnVariants = true,
@@ -1399,8 +1599,8 @@ export function create({
1399
1599
  ) => {
1400
1600
  let workingResolved = resolved;
1401
1601
  let changedVariants: Record<string, unknown> | null = null;
1402
- if (computed) {
1403
- const computedResult = runComputedContext(
1602
+ if (refine) {
1603
+ const refineResult = runRefineContext(
1404
1604
  resolved,
1405
1605
  userVariantProps,
1406
1606
  filterOwnVariants,
@@ -1409,21 +1609,21 @@ export function create({
1409
1609
  pendingProtectedVariants,
1410
1610
  protectedVariantKeys,
1411
1611
  );
1412
- workingResolved = computedResult.workingResolved;
1413
- changedVariants = computedResult.changedVariants;
1612
+ workingResolved = refineResult.workingResolved;
1613
+ changedVariants = refineResult.changedVariants;
1414
1614
  }
1415
1615
 
1416
- if (extMetasWithComputedCount > 0) {
1616
+ if (extMetasWithRefineCount > 0) {
1417
1617
  const extUserVariantProps = getExtUserVariantProps(
1418
1618
  userVariantProps,
1419
1619
  protectedVariants ?? null,
1420
1620
  changedVariants,
1421
1621
  );
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(
1622
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
1623
+ const meta = extMetasWithRefine[i];
1624
+ const resolveRefine = meta.resolveRefine;
1625
+ if (!resolveRefine) continue;
1626
+ workingResolved = resolveRefine(
1427
1627
  workingResolved,
1428
1628
  extUserVariantProps,
1429
1629
  true,
@@ -1441,8 +1641,8 @@ export function create({
1441
1641
  return workingResolved;
1442
1642
  };
1443
1643
 
1444
- const resolveComputed: ResolveComputedFn | null =
1445
- computed || extMetasWithComputedCount > 0
1644
+ const resolveRefine: ResolveRefineFn | null =
1645
+ refine || extMetasWithRefineCount > 0
1446
1646
  ? (
1447
1647
  resolved,
1448
1648
  userVariantProps,
@@ -1452,16 +1652,21 @@ export function create({
1452
1652
  pendingProtectedVariants,
1453
1653
  protectedVariantKeys,
1454
1654
  ) => {
1455
- runState ??= { remaining: MAX_COMPUTED_RUNS, warned: false };
1655
+ runState ??= { remaining: MAX_REFINE_RUNS };
1456
1656
  protectedVariants ??= {};
1457
1657
  protectedVariantKeys ??= new Set<string>();
1458
1658
  let workingResolved = resolved;
1659
+ // Union of variant keys that differed on non-converging iterations
1660
+ // inside the tracking window — see the compute loop above for the
1661
+ // shared rationale. Lazy-init keeps convergent loops
1662
+ // allocation-free.
1663
+ let unstableKeys: Set<string> | null = null;
1459
1664
  let reachedLimit = true;
1460
1665
 
1461
1666
  while (runState.remaining > 0) {
1462
1667
  runState.remaining -= 1;
1463
1668
  const nextPendingProtectedVariants: Record<string, unknown> = {};
1464
- const nextResolved = resolveComputedOnce(
1669
+ const nextResolved = resolveRefineOnce(
1465
1670
  workingResolved,
1466
1671
  userVariantProps,
1467
1672
  filterOwnVariants,
@@ -1495,11 +1700,24 @@ export function create({
1495
1700
  break;
1496
1701
  }
1497
1702
 
1703
+ if (
1704
+ process.env.NODE_ENV !== "production" &&
1705
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1706
+ ) {
1707
+ if (!unstableKeys) {
1708
+ unstableKeys = new Set<string>();
1709
+ }
1710
+ accumulateUnstableVariantKeys(
1711
+ unstableKeys,
1712
+ workingResolved,
1713
+ nextResolved,
1714
+ );
1715
+ }
1498
1716
  workingResolved = nextResolved;
1499
1717
  }
1500
1718
 
1501
1719
  if (reachedLimit) {
1502
- warnComputedLimit(runState);
1720
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1503
1721
  }
1504
1722
 
1505
1723
  return workingResolved;
@@ -1521,7 +1739,7 @@ export function create({
1521
1739
  Object.assign(resolved, staticDefaults);
1522
1740
 
1523
1741
  let userVariantProps: Record<string, unknown>;
1524
- if (extMetasWithComputedCount > 0) {
1742
+ if (extMetasWithRefineCount > 0) {
1525
1743
  // Some extends need a resolveDefaults pass. They expect a variant-only
1526
1744
  // object as `userProps`, so we extract one.
1527
1745
  const variantProps: Record<string, unknown> = {};
@@ -1531,12 +1749,12 @@ export function create({
1531
1749
  variantProps[key] = propsRecord[key];
1532
1750
  }
1533
1751
  }
1534
- for (let i = 0; i < extMetasWithComputedCount; i++) {
1535
- const meta = extMetasWithComputed[i];
1536
- const extComputed = meta.resolveDefaults!(resolved, variantProps);
1537
- for (const k in extComputed) {
1538
- if (!Object.hasOwn(extComputed, k)) continue;
1539
- resolved[k] = extComputed[k];
1752
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
1753
+ const meta = extMetasWithRefine[i];
1754
+ const extDefaults = meta.resolveDefaults!(resolved, variantProps);
1755
+ for (const k in extDefaults) {
1756
+ if (!Object.hasOwn(extDefaults, k)) continue;
1757
+ resolved[k] = extDefaults[k];
1540
1758
  }
1541
1759
  }
1542
1760
  for (const k in variantProps) {
@@ -1610,12 +1828,8 @@ export function create({
1610
1828
  const getVariants = (variants?: VariantValues<MergedVariants>) => {
1611
1829
  const variantProps = variants ?? EMPTY_DEFAULTS;
1612
1830
  let resolvedVariants = resolveVariantsHot(variantProps);
1613
- if (resolveComputed) {
1614
- resolvedVariants = resolveComputed(
1615
- resolvedVariants,
1616
- variantProps,
1617
- false,
1618
- );
1831
+ if (resolveRefine) {
1832
+ resolvedVariants = resolveRefine(resolvedVariants, variantProps, false);
1619
1833
  }
1620
1834
  return resolvedVariants as VariantValues<MergedVariants>;
1621
1835
  };
@@ -1642,8 +1856,9 @@ export function create({
1642
1856
  staticDefaults,
1643
1857
  resolveDefaults: resolveDefaultsFn,
1644
1858
  compute,
1645
- resolveComputed,
1859
+ resolveRefine,
1646
1860
  transformClass,
1861
+ functionVariantKeys,
1647
1862
  };
1648
1863
 
1649
1864
  const initComponent = <
@@ -1667,7 +1882,7 @@ export function create({
1667
1882
  const defaultComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1668
1883
  const { className, style } = computeResult(props);
1669
1884
  return { class: className, style };
1670
- }) as CVComponent<V, CV, E>;
1885
+ }) as CVComponent<V, E>;
1671
1886
  initComponent(defaultComponent, inputPropsKeys, (props = {}) => {
1672
1887
  return computeResult(props).style;
1673
1888
  });