clava 0.2.4 → 0.4.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
@@ -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,14 +41,33 @@ type ComputeFn = (
42
41
  skipValues: Record<string, Set<string>> | null,
43
42
  classesOut: ClsxClassValue[],
44
43
  styleOut: StyleValue,
45
- ) => void;
44
+ runState?: RefineRunState,
45
+ protectedVariants?: Record<string, unknown> | null,
46
+ pendingProtectedVariants?: Record<string, unknown> | null,
47
+ protectedVariantKeys?: Set<string> | null,
48
+ ) => Record<string, unknown>;
49
+
50
+ type ResolveRefineFn = (
51
+ resolved: Record<string, unknown>,
52
+ userVariantProps: Record<string, unknown>,
53
+ filterOwnVariants?: boolean,
54
+ runState?: RefineRunState,
55
+ protectedVariants?: Record<string, unknown> | null,
56
+ pendingProtectedVariants?: Record<string, unknown> | null,
57
+ protectedVariantKeys?: Set<string> | null,
58
+ ) => Record<string, unknown>;
59
+
60
+ interface RefineRunState {
61
+ remaining: number;
62
+ warned: boolean;
63
+ }
46
64
 
47
65
  // Internal metadata stored on components but hidden from public types.
48
66
  interface ComponentMeta {
49
67
  baseClass: string;
50
68
  staticDefaults: Record<string, unknown>;
51
- // Returns variants set via setDefaultVariants in the computed function chain.
52
- // null when this component has no resolveDefaults work to do (no `computed`
69
+ // Returns variants set via setDefaultVariants in the refine function chain.
70
+ // null when this component has no resolveDefaults work to do (no `refine`
53
71
  // and no extends with work).
54
72
  resolveDefaults:
55
73
  | ((
@@ -60,23 +78,97 @@ interface ComponentMeta {
60
78
  // Returns variant classes + style for this component, used by extending
61
79
  // components. Top-level rendering also routes through this.
62
80
  compute: ComputeFn;
81
+ resolveRefine: ResolveRefineFn | null;
63
82
  // Reference identity is used to detect mixed-factory `extend`. When a
64
83
  // component is extended by a parent from a different `create()` call, the
65
84
  // parent applies this transform to the extend's contribution before joining,
66
85
  // preserving each factory's transform boundary.
67
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>;
68
93
  }
69
94
 
70
95
  const META_KEY = "__meta";
71
96
 
72
- // eslint-disable-next-line @typescript-eslint/unbound-method
73
- const hasOwn = Object.prototype.hasOwnProperty;
74
-
75
97
  const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
76
98
  string,
77
99
  unknown
78
100
  >;
79
101
 
102
+ const MAX_REFINE_RUNS = 50;
103
+
104
+ function areVariantsEqual(
105
+ a: Record<string, unknown>,
106
+ b: Record<string, unknown>,
107
+ ): boolean {
108
+ for (const key in a) {
109
+ if (!Object.hasOwn(a, key)) continue;
110
+ if (!Object.is(a[key], b[key])) return false;
111
+ }
112
+ for (const key in b) {
113
+ if (!Object.hasOwn(b, key)) continue;
114
+ if (!Object.hasOwn(a, key)) return false;
115
+ }
116
+ return true;
117
+ }
118
+
119
+ function warnRefineLimit(runState: RefineRunState): void {
120
+ if (runState.warned) return;
121
+ runState.warned = true;
122
+ if (process.env.NODE_ENV !== "production") {
123
+ console.warn(
124
+ "Clava: Maximum refine iterations exceeded. This can happen when a " +
125
+ "refine callback calls setVariants or setDefaultVariants, but one " +
126
+ "of the variants changes on every run.",
127
+ );
128
+ }
129
+ }
130
+
131
+ function getExtUserVariantProps(
132
+ userVariantProps: Record<string, unknown>,
133
+ protectedVariants: Record<string, unknown> | null,
134
+ changedVariants: Record<string, unknown> | null,
135
+ ): Record<string, unknown> {
136
+ const extUserVariantProps: Record<string, unknown> = {};
137
+ Object.assign(extUserVariantProps, userVariantProps);
138
+ if (protectedVariants) {
139
+ Object.assign(extUserVariantProps, protectedVariants);
140
+ }
141
+ if (changedVariants) {
142
+ Object.assign(extUserVariantProps, changedVariants);
143
+ }
144
+ return extUserVariantProps;
145
+ }
146
+
147
+ function mergeVariants(
148
+ target: Record<string, unknown>,
149
+ source: Record<string, unknown>,
150
+ skipKeys?: Set<string> | null,
151
+ ): boolean {
152
+ let changed = false;
153
+ if (!skipKeys || skipKeys.size === 0) {
154
+ for (const key in source) {
155
+ if (!Object.hasOwn(source, key)) continue;
156
+ const value = source[key];
157
+ if (!Object.is(target[key], value)) changed = true;
158
+ target[key] = value;
159
+ }
160
+ return changed;
161
+ }
162
+ for (const key in source) {
163
+ if (!Object.hasOwn(source, key)) continue;
164
+ if (skipKeys.has(key)) continue;
165
+ const value = source[key];
166
+ if (!Object.is(target[key], value)) changed = true;
167
+ target[key] = value;
168
+ }
169
+ return changed;
170
+ }
171
+
80
172
  // Dynamic property access on function requires cast through unknown.
81
173
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
82
174
  return (component as unknown as Record<string, unknown>)[META_KEY] as
@@ -115,16 +207,14 @@ export type Variant<
115
207
 
116
208
  export interface CVConfig<
117
209
  V extends Variants = {},
118
- CV extends ComputedVariants = {},
119
210
  E extends AnyComponent[] = [],
120
211
  > {
121
212
  extend?: E;
122
213
  class?: ClassValue;
123
214
  style?: StyleValue;
124
215
  variants?: ExtendableVariants<V, E>;
125
- computedVariants?: CV;
126
- defaultVariants?: VariantValues<MergeVariants<V, CV, E>>;
127
- computed?: Computed<MergeVariants<V, CV, E>>;
216
+ defaultVariants?: VariantValues<MergeVariants<V, E>>;
217
+ refine?: Refine<MergeVariants<V, E>>;
128
218
  }
129
219
 
130
220
  interface CreateParams {
@@ -195,7 +285,7 @@ function extractClassAndStylePrebuilt(value: unknown): PrebuiltValue {
195
285
  * components.
196
286
  */
197
287
  function collectVariantKeys(
198
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
288
+ config: CVConfig<Variants, AnyComponent[]>,
199
289
  ): string[] {
200
290
  const keys = new Set<string>();
201
291
 
@@ -210,7 +300,7 @@ function collectVariantKeys(
210
300
 
211
301
  if (config.variants) {
212
302
  for (const key in config.variants) {
213
- if (!hasOwn.call(config.variants, key)) continue;
303
+ if (!Object.hasOwn(config.variants, key)) continue;
214
304
  const variant = (config.variants as Record<string, unknown>)[key];
215
305
  if (variant === null) {
216
306
  keys.delete(key);
@@ -220,18 +310,11 @@ function collectVariantKeys(
220
310
  }
221
311
  }
222
312
 
223
- if (config.computedVariants) {
224
- for (const key in config.computedVariants) {
225
- if (!hasOwn.call(config.computedVariants, key)) continue;
226
- keys.add(key);
227
- }
228
- }
229
-
230
313
  return Array.from(keys);
231
314
  }
232
315
 
233
316
  function isVariantDisabled(
234
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
317
+ config: CVConfig<Variants, AnyComponent[]>,
235
318
  key: string,
236
319
  ): boolean {
237
320
  return config.variants?.[key] === null;
@@ -245,7 +328,7 @@ function getVariantValueKey(value: unknown): string | undefined {
245
328
  }
246
329
 
247
330
  function isVariantValueDisabled(
248
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
331
+ config: CVConfig<Variants, AnyComponent[]>,
249
332
  key: string,
250
333
  value: unknown,
251
334
  ): boolean {
@@ -257,12 +340,12 @@ function isVariantValueDisabled(
257
340
  }
258
341
 
259
342
  function collectDisabledVariantKeys(
260
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
343
+ config: CVConfig<Variants, AnyComponent[]>,
261
344
  ): Set<string> {
262
345
  const keys = new Set<string>();
263
346
  if (!config.variants) return keys;
264
347
  for (const key in config.variants) {
265
- if (!hasOwn.call(config.variants, key)) continue;
348
+ if (!Object.hasOwn(config.variants, key)) continue;
266
349
  if ((config.variants as Record<string, unknown>)[key] === null) {
267
350
  keys.add(key);
268
351
  }
@@ -271,17 +354,17 @@ function collectDisabledVariantKeys(
271
354
  }
272
355
 
273
356
  function collectDisabledVariantValues(
274
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
357
+ config: CVConfig<Variants, AnyComponent[]>,
275
358
  ): Record<string, Set<string>> {
276
359
  const values: Record<string, Set<string>> = {};
277
360
  if (!config.variants) return values;
278
361
  for (const key in config.variants) {
279
- if (!hasOwn.call(config.variants, key)) continue;
362
+ if (!Object.hasOwn(config.variants, key)) continue;
280
363
  const variant = (config.variants as Record<string, unknown>)[key];
281
364
  if (!isRecordObject(variant)) continue;
282
365
  let bucket: Set<string> | undefined;
283
366
  for (const variantValue in variant) {
284
- if (!hasOwn.call(variant, variantValue)) continue;
367
+ if (!Object.hasOwn(variant, variantValue)) continue;
285
368
  if (variant[variantValue] !== null) continue;
286
369
  if (!bucket) {
287
370
  bucket = new Set<string>();
@@ -294,13 +377,13 @@ function collectDisabledVariantValues(
294
377
  }
295
378
 
296
379
  interface NormalizedSource {
297
- keys: string[];
380
+ propKeys: string[];
298
381
  variantKeys: string[];
299
382
  isComponent: boolean;
300
383
  }
301
384
 
302
385
  const EMPTY_SOURCE: NormalizedSource = {
303
- keys: [],
386
+ propKeys: [],
304
387
  variantKeys: [],
305
388
  isComponent: false,
306
389
  };
@@ -308,7 +391,7 @@ const EMPTY_SOURCE: NormalizedSource = {
308
391
  function normalizeKeySource(source: unknown): NormalizedSource {
309
392
  if (Array.isArray(source)) {
310
393
  return {
311
- keys: source as string[],
394
+ propKeys: source as string[],
312
395
  variantKeys: source as string[],
313
396
  isComponent: false,
314
397
  };
@@ -318,17 +401,14 @@ function normalizeKeySource(source: unknown): NormalizedSource {
318
401
  if (typeof source !== "object" && typeof source !== "function") {
319
402
  return EMPTY_SOURCE;
320
403
  }
321
- if (!("keys" in source)) return EMPTY_SOURCE;
322
- if (!("variantKeys" in source)) return EMPTY_SOURCE;
404
+ const typed = source as Record<string, unknown>;
405
+ if (typeof typed.getVariants !== "function") return EMPTY_SOURCE;
406
+ if (!Array.isArray(typed.propKeys)) return EMPTY_SOURCE;
407
+ if (!Array.isArray(typed.variantKeys)) return EMPTY_SOURCE;
323
408
 
324
- // Component-provided arrays are immutable metadata — reference directly.
325
- const typed = source as {
326
- keys: string[];
327
- variantKeys: string[];
328
- };
329
409
  return {
330
- keys: typed.keys,
331
- variantKeys: typed.variantKeys,
410
+ propKeys: typed.propKeys as string[],
411
+ variantKeys: typed.variantKeys as string[],
332
412
  isComponent: true,
333
413
  };
334
414
  }
@@ -368,7 +448,9 @@ function splitPropsImpl(
368
448
  const sourceResult: Record<string, unknown> = {};
369
449
 
370
450
  const effectiveKeys =
371
- source.isComponent && stylingClaimed ? source.variantKeys : source.keys;
451
+ source.isComponent && stylingClaimed
452
+ ? source.variantKeys
453
+ : source.propKeys;
372
454
 
373
455
  const effectiveKeysLength = effectiveKeys.length;
374
456
  for (let i = 0; i < effectiveKeysLength; i++) {
@@ -422,7 +504,7 @@ export const splitProps: SplitPropsFunction = ((
422
504
  ) => {
423
505
  const normalizedSource1 = normalizeKeySource(source1);
424
506
  return splitPropsImpl(
425
- normalizedSource1.keys,
507
+ normalizedSource1.propKeys,
426
508
  normalizedSource1.isComponent,
427
509
  props,
428
510
  sources,
@@ -455,7 +537,7 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
455
537
  const values: Record<string, PrebuiltValue> = {};
456
538
  let disabledValues: Set<string> | null = null;
457
539
  for (const key in variantDef) {
458
- if (!hasOwn.call(variantDef, key)) continue;
540
+ if (!Object.hasOwn(variantDef, key)) continue;
459
541
  const value = variantDef[key];
460
542
  if (value === null) {
461
543
  if (!disabledValues) disabledValues = new Set<string>();
@@ -479,14 +561,10 @@ export function create({
479
561
  }: CreateParams = {}) {
480
562
  const cx = (...classes: ClsxClassValue[]) => transformClass(clsx(...classes));
481
563
 
482
- const cv = <
483
- V extends Variants = {},
484
- CV extends ComputedVariants = {},
485
- const E extends AnyComponent[] = [],
486
- >(
487
- config: CVConfig<V, CV, E> = {},
488
- ): CVComponent<V, CV, E> => {
489
- type MergedVariants = MergeVariants<V, CV, E>;
564
+ const cv = <V extends Variants = {}, const E extends AnyComponent[] = []>(
565
+ config: CVConfig<V, E> = {},
566
+ ): CVComponent<V, E> => {
567
+ type MergedVariants = MergeVariants<V, E>;
490
568
 
491
569
  // ----- Pre-computed at creation time -----
492
570
  const variantKeys = collectVariantKeys(config);
@@ -503,41 +581,34 @@ export function create({
503
581
  const extend = config.extend;
504
582
  const hasExtend = !!extend && extend.length > 0;
505
583
  const variants = config.variants;
506
- const computedVariantsCfg = config.computedVariants;
507
- const computed = config.computed;
584
+ const refine = config.refine;
508
585
  const baseStyle = config.style;
509
586
  const hasBaseStyle = !!baseStyle;
510
587
 
511
- // Pre-build variant entries for fast iteration. For each variant key in
512
- // `variants`, we have a name and a PrebuiltVariant with normalized values.
588
+ // Split `variants` entries into static entries (object/shorthand) and
589
+ // function-variant entries. Static entries are pre-built into
590
+ // PrebuiltVariant for fast iteration. Function-variant entries override
591
+ // any same-key inherited variant (see `staticExtSkipKeys`).
513
592
  const variantEntryNames: string[] = [];
514
593
  const variantEntryDefs: PrebuiltVariant[] = [];
594
+ const functionVariantNames: string[] = [];
595
+ const functionVariantFns: Array<(value: unknown) => unknown> = [];
515
596
  if (variants) {
516
597
  for (const name in variants) {
517
- if (!hasOwn.call(variants, name)) continue;
598
+ if (!Object.hasOwn(variants, name)) continue;
518
599
  const variant = (variants as Record<string, unknown>)[name];
519
600
  if (variant === null) continue;
601
+ if (typeof variant === "function") {
602
+ functionVariantNames.push(name);
603
+ functionVariantFns.push(variant as (value: unknown) => unknown);
604
+ continue;
605
+ }
520
606
  variantEntryNames.push(name);
521
607
  variantEntryDefs.push(buildPrebuiltVariant(variant));
522
608
  }
523
609
  }
524
610
  const variantEntryCount = variantEntryNames.length;
525
-
526
- // Pre-built computed-variants entries.
527
- const computedVariantNames: string[] = [];
528
- const computedVariantFns: Array<(value: unknown) => unknown> = [];
529
- if (computedVariantsCfg) {
530
- for (const name in computedVariantsCfg) {
531
- if (!hasOwn.call(computedVariantsCfg, name)) continue;
532
- computedVariantNames.push(name);
533
- computedVariantFns.push(
534
- (computedVariantsCfg as Record<string, (value: unknown) => unknown>)[
535
- name
536
- ] as (value: unknown) => unknown,
537
- );
538
- }
539
- }
540
- const computedVariantCount = computedVariantNames.length;
611
+ const functionVariantCount = functionVariantNames.length;
541
612
 
542
613
  // Pre-compute static defaults. Includes:
543
614
  // - extended components' static defaults
@@ -553,11 +624,11 @@ export function create({
553
624
  }
554
625
  if (variants) {
555
626
  for (const name in variants) {
556
- if (!hasOwn.call(variants, name)) continue;
627
+ if (!Object.hasOwn(variants, name)) continue;
557
628
  const variantDef = (variants as Record<string, unknown>)[name];
558
629
  if (!isRecordObject(variantDef)) continue;
559
630
  if (
560
- hasOwn.call(variantDef, "false") &&
631
+ Object.hasOwn(variantDef, "false") &&
561
632
  staticDefaults[name] === undefined
562
633
  ) {
563
634
  staticDefaults[name] = false;
@@ -570,7 +641,7 @@ export function create({
570
641
  if (hasAnyDisabled) {
571
642
  // Filter disabled variants in-place
572
643
  for (const key in staticDefaults) {
573
- if (!hasOwn.call(staticDefaults, key)) continue;
644
+ if (!Object.hasOwn(staticDefaults, key)) continue;
574
645
  if (disabledVariantKeys.has(key)) {
575
646
  delete staticDefaults[key];
576
647
  continue;
@@ -596,7 +667,7 @@ export function create({
596
667
  const extBaseClassesArr: string[] = [];
597
668
  const extIsolated: boolean[] = [];
598
669
  let hasIsolatedExt = false;
599
- if (extend) {
670
+ if (hasExtend) {
600
671
  for (const ext of extend) {
601
672
  const meta = getComponentMeta(ext);
602
673
  if (!meta) continue;
@@ -616,26 +687,80 @@ export function create({
616
687
  }
617
688
  const extCount = extMetas.length;
618
689
 
619
- // Filter to only extends whose `resolveDefaults` actually does work
620
- // (config.computed exists, transitively). Iterating these in
621
- // `resolveVariantsHot` skips empty work.
622
- const extMetasWithResolveDefaults: ComponentMeta[] = [];
690
+ // Filter to only extends with refine work in their chain. `resolveDefaults`
691
+ // and `resolveRefine` are populated from the same transitive condition,
692
+ // so one bucket is enough for both resolver paths.
693
+ const extMetasWithRefine: ComponentMeta[] = [];
623
694
  for (let i = 0; i < extCount; i++) {
624
- if (extMetas[i].resolveDefaults) {
625
- extMetasWithResolveDefaults.push(extMetas[i]);
695
+ const meta = extMetas[i];
696
+ if (meta.resolveDefaults) {
697
+ extMetasWithRefine.push(meta);
698
+ }
699
+ }
700
+ const extMetasWithRefineCount = extMetasWithRefine.length;
701
+ const shouldCollectChangedVariants = extMetasWithRefineCount > 0;
702
+
703
+ // Function variant keys inherited from extends, filtered through this
704
+ // component's own variants: a static (object/shorthand) variant in this
705
+ // component replaces an inherited function variant for the same key.
706
+ // The closure is exposed on `ComponentMeta` so any further extending
707
+ // component can detect "ancestor's effective variant for K is a function"
708
+ // and skip it when overriding K with a non-function.
709
+ const functionVariantKeys = new Set<string>();
710
+ for (let i = 0; i < extCount; i++) {
711
+ const fnKeys = extMetas[i].functionVariantKeys;
712
+ for (const k of fnKeys) {
713
+ if (disabledVariantKeys.has(k)) continue;
714
+ functionVariantKeys.add(k);
715
+ }
716
+ }
717
+ for (let i = 0; i < functionVariantCount; i++) {
718
+ functionVariantKeys.add(functionVariantNames[i]);
719
+ }
720
+ for (let i = 0; i < variantEntryCount; i++) {
721
+ // A static variant in this component replaces an inherited function
722
+ // variant for the same key; from this component onward, the key is no
723
+ // longer a function variant.
724
+ functionVariantKeys.delete(variantEntryNames[i]);
725
+ }
726
+
727
+ // Static-variant keys in this component that override an inherited
728
+ // function variant. Type-level merge says child fully replaces, so the
729
+ // ancestor's function must not run with the child's (object-typed) value.
730
+ let staticVariantsOverridingExtFn: string[] | null = null;
731
+ if (variantEntryCount > 0 && extCount > 0) {
732
+ for (let i = 0; i < variantEntryCount; i++) {
733
+ const name = variantEntryNames[i];
734
+ for (let j = 0; j < extCount; j++) {
735
+ if (extMetas[j].functionVariantKeys.has(name)) {
736
+ if (!staticVariantsOverridingExtFn) {
737
+ staticVariantsOverridingExtFn = [];
738
+ }
739
+ staticVariantsOverridingExtFn.push(name);
740
+ break;
741
+ }
742
+ }
626
743
  }
627
744
  }
628
- const extMetasWithResolveDefaultsCount = extMetasWithResolveDefaults.length;
629
745
 
630
746
  // Pre-compute static skip key/value sets to pass to extends. These never
631
747
  // change across calls — when caller passes no skip sets, we reuse the same
632
748
  // object and avoid Set allocation.
633
749
  let staticExtSkipKeys: Set<string> | null = null;
634
- if (hasDisabledVariantKeys || computedVariantCount > 0) {
750
+ if (
751
+ hasDisabledVariantKeys ||
752
+ functionVariantCount > 0 ||
753
+ staticVariantsOverridingExtFn !== null
754
+ ) {
635
755
  staticExtSkipKeys = new Set<string>();
636
756
  for (const k of disabledVariantKeys) staticExtSkipKeys.add(k);
637
- for (let i = 0; i < computedVariantCount; i++) {
638
- staticExtSkipKeys.add(computedVariantNames[i]);
757
+ for (let i = 0; i < functionVariantCount; i++) {
758
+ staticExtSkipKeys.add(functionVariantNames[i]);
759
+ }
760
+ if (staticVariantsOverridingExtFn) {
761
+ for (const k of staticVariantsOverridingExtFn) {
762
+ staticExtSkipKeys.add(k);
763
+ }
639
764
  }
640
765
  }
641
766
  // Skip values are passed directly to extends. We can reuse the same object
@@ -652,12 +777,12 @@ export function create({
652
777
  ): void {
653
778
  if (!hasAnyDisabled) {
654
779
  for (const key in input) {
655
- if (hasOwn.call(input, key)) out[key] = input[key];
780
+ if (Object.hasOwn(input, key)) out[key] = input[key];
656
781
  }
657
782
  return;
658
783
  }
659
784
  for (const key in input) {
660
- if (!hasOwn.call(input, key)) continue;
785
+ if (!Object.hasOwn(input, key)) continue;
661
786
  if (disabledVariantKeys.has(key)) continue;
662
787
  const value = input[key];
663
788
  if (hasDisabledVariantValues) {
@@ -672,12 +797,12 @@ export function create({
672
797
 
673
798
  // Pre-create resolveDefaults function — used by parents during their
674
799
  // `resolveVariantsHot`. Returns the variants set via setDefaultVariants in
675
- // the computed function chain.
800
+ // the refine function chain.
676
801
  //
677
- // When this component has no `computed` and no `extend` with work, the
802
+ // When this component has no `refine` and no `extend` with work, the
678
803
  // function is null — callers can skip iterating it entirely.
679
804
  const resolveDefaultsFn: ComponentMeta["resolveDefaults"] =
680
- computed || extMetasWithResolveDefaultsCount > 0
805
+ refine || extMetasWithRefineCount > 0
681
806
  ? (
682
807
  childDefaults: Record<string, unknown>,
683
808
  userProps: Record<string, unknown> = EMPTY_DEFAULTS,
@@ -687,52 +812,54 @@ export function create({
687
812
  const resolvedVariants: Record<string, unknown> = {};
688
813
  Object.assign(resolvedVariants, staticDefaults);
689
814
  for (const key in childDefaults) {
690
- if (!hasOwn.call(childDefaults, key)) continue;
815
+ if (!Object.hasOwn(childDefaults, key)) continue;
691
816
  const v = childDefaults[key];
692
817
  if (v === undefined) continue;
693
818
  resolvedVariants[key] = v;
694
819
  }
695
820
  for (const key in userProps) {
696
- if (!hasOwn.call(userProps, key)) continue;
821
+ if (!Object.hasOwn(userProps, key)) continue;
697
822
  const v = userProps[key];
698
823
  if (v === undefined) continue;
699
824
  resolvedVariants[key] = v;
700
825
  }
701
826
 
702
- const computedDefaults: Record<string, unknown> = {};
827
+ const refineDefaults: Record<string, unknown> = {};
703
828
 
704
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
705
- const extDefaults = extMetasWithResolveDefaults[i]
706
- .resolveDefaults!(childDefaults, userProps);
829
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
830
+ const extDefaults = extMetasWithRefine[i].resolveDefaults!(
831
+ childDefaults,
832
+ userProps,
833
+ );
707
834
  for (const k in extDefaults) {
708
- if (!hasOwn.call(extDefaults, k)) continue;
709
- computedDefaults[k] = extDefaults[k];
835
+ if (!Object.hasOwn(extDefaults, k)) continue;
836
+ refineDefaults[k] = extDefaults[k];
710
837
  }
711
838
  }
712
839
 
713
- if (computed) {
714
- // Filter to own variant keys so `computed.ctx.variants` matches
840
+ if (refine) {
841
+ // Filter to own variant keys so `ctx.variants` matches
715
842
  // `VariantValues<V>` when this component is used as an extend by
716
843
  // a parent that adds extra variant keys (those keys would
717
844
  // otherwise leak through `userProps`).
718
845
  const ownVariants: Record<string, unknown> = {};
719
846
  for (let i = 0; i < variantKeysLength; i++) {
720
847
  const k = variantKeys[i];
721
- if (hasOwn.call(resolvedVariants, k)) {
848
+ if (Object.hasOwn(resolvedVariants, k)) {
722
849
  ownVariants[k] = resolvedVariants[k];
723
850
  }
724
851
  }
725
- computed({
852
+ refine({
726
853
  variants: ownVariants as VariantValues<Record<string, unknown>>,
727
854
  setVariants: noop,
728
855
  setDefaultVariants: (newDefaults) => {
729
856
  for (const key in newDefaults) {
730
- if (!hasOwn.call(newDefaults, key)) continue;
857
+ if (!Object.hasOwn(newDefaults, key)) continue;
731
858
  const value = (newDefaults as Record<string, unknown>)[key];
732
859
  if (userProps[key] !== undefined) continue;
733
860
  if (isVariantDisabled(config, key)) continue;
734
861
  if (isVariantValueDisabled(config, key, value)) continue;
735
- computedDefaults[key] = value;
862
+ refineDefaults[key] = value;
736
863
  }
737
864
  },
738
865
  addClass: noop,
@@ -740,12 +867,12 @@ export function create({
740
867
  });
741
868
  }
742
869
 
743
- return computedDefaults;
870
+ return refineDefaults;
744
871
  }
745
872
  : null;
746
873
 
747
874
  // Hot path: resolve variants by merging static defaults + extends'
748
- // computed defaults + user-provided props.
875
+ // refine defaults + user-provided props.
749
876
  function resolveVariantsHot(
750
877
  propsVariants: Record<string, unknown>,
751
878
  ): Record<string, unknown> {
@@ -753,14 +880,14 @@ export function create({
753
880
  const defaults: Record<string, unknown> = {};
754
881
  Object.assign(defaults, staticDefaults);
755
882
 
756
- // Apply computed defaults from extended components (only those that have
883
+ // Apply refine defaults from extended components (only those that have
757
884
  // actual work to do).
758
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
759
- const meta = extMetasWithResolveDefaults[i];
760
- const extComputed = meta.resolveDefaults!(defaults, propsVariants);
761
- for (const k in extComputed) {
762
- if (!hasOwn.call(extComputed, k)) continue;
763
- defaults[k] = extComputed[k];
885
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
886
+ const meta = extMetasWithRefine[i];
887
+ const extDefaults = meta.resolveDefaults!(defaults, propsVariants);
888
+ for (const k in extDefaults) {
889
+ if (!Object.hasOwn(extDefaults, k)) continue;
890
+ defaults[k] = extDefaults[k];
764
891
  }
765
892
  }
766
893
 
@@ -768,7 +895,7 @@ export function create({
768
895
  // contractually variant-only here — callers building from a full props
769
896
  // object filter to variant keys before calling.
770
897
  for (const k in propsVariants) {
771
- if (!hasOwn.call(propsVariants, k)) continue;
898
+ if (!Object.hasOwn(propsVariants, k)) continue;
772
899
  const v = propsVariants[k];
773
900
  if (v === undefined) continue;
774
901
  defaults[k] = v;
@@ -782,39 +909,44 @@ export function create({
782
909
  return result;
783
910
  }
784
911
 
785
- // Core compute path. Called both for top-level rendering (via
786
- // `computeResult`) and recursively when this component is used as an
787
- // `extend` target by another component. Pushes variant classes (excluding
788
- // base class) into `classesOut` and merges styles into `styleOut`.
789
- const compute: ComputeFn = (
790
- resolved,
791
- userVariantProps,
792
- skipKeys,
793
- skipValues,
794
- classesOut,
795
- styleOut,
796
- ) => {
797
- // Run `computed` (if any). May modify resolved variants and emit classes
798
- // and styles.
912
+ const runRefineContext = (
913
+ resolved: Record<string, unknown>,
914
+ userVariantProps: Record<string, unknown>,
915
+ filterOwnVariants: boolean,
916
+ collectOutput: boolean,
917
+ protectedVariants: Record<string, unknown> | null | undefined,
918
+ pendingProtectedVariants: Record<string, unknown> | null | undefined,
919
+ protectedVariantKeys: Set<string> | null | undefined,
920
+ ): {
921
+ workingResolved: Record<string, unknown>;
922
+ changedVariants: Record<string, unknown> | null;
923
+ classes: ClassValue[] | null;
924
+ style: StyleValue | null;
925
+ } => {
799
926
  let workingResolved = resolved;
800
927
  let cClasses: ClassValue[] | null = null;
801
928
  let cStyle: StyleValue | null = null;
802
-
803
- if (computed) {
804
- // When this component is being extended, `resolved` is the parent's
805
- // workingResolved (a superset of our variant keys). Filter to our own
806
- // keys for `ctx.variants` so the user's `computed` callback sees the
807
- // shape declared by `VariantValues<V>` and not foreign parent keys.
808
- const ownVariants: Record<string, unknown> = {};
809
- for (let i = 0; i < variantKeysLength; i++) {
810
- const k = variantKeys[i];
811
- if (hasOwn.call(resolved, k)) ownVariants[k] = resolved[k];
929
+ let changedVariants: Record<string, unknown> | null = null;
930
+
931
+ if (refine) {
932
+ let ownVariants = resolved;
933
+ if (filterOwnVariants) {
934
+ // When this component is being extended, `resolved` is the parent's
935
+ // workingResolved (a superset of our variant keys). Filter to our own
936
+ // keys for `ctx.variants` so the user's `refine` callback sees the
937
+ // shape declared by `VariantValues<V>` and not foreign parent keys.
938
+ const filteredVariants: Record<string, unknown> = {};
939
+ for (let i = 0; i < variantKeysLength; i++) {
940
+ const k = variantKeys[i];
941
+ if (Object.hasOwn(resolved, k)) filteredVariants[k] = resolved[k];
942
+ }
943
+ ownVariants = filteredVariants;
812
944
  }
813
- // Lazy-init updatedVariants — many computeds only inspect `variants`
814
- // or call setDefaultVariants for keys the user already set, so the
815
- // copy is unnecessary in the common case.
945
+ // Lazy-init updatedVariants — many refine callbacks only inspect
946
+ // `variants` or call setDefaultVariants for keys the user already set,
947
+ // so the copy is unnecessary in the common case.
816
948
  let updatedVariants: Record<string, unknown> | null = null;
817
- const localCClasses: ClassValue[] = [];
949
+ const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
818
950
  let localCStyle: StyleValue | null = null;
819
951
  const ensureUpdated = (): Record<string, unknown> => {
820
952
  if (updatedVariants) return updatedVariants;
@@ -823,17 +955,40 @@ export function create({
823
955
  updatedVariants = u;
824
956
  return u;
825
957
  };
958
+ const setChangedVariant = (
959
+ key: string,
960
+ value: unknown,
961
+ protect = false,
962
+ ) => {
963
+ if (shouldCollectChangedVariants) {
964
+ if (!changedVariants) changedVariants = {};
965
+ changedVariants[key] = value;
966
+ }
967
+ if (protect && protectedVariants) {
968
+ protectedVariants[key] = value;
969
+ protectedVariantKeys?.add(key);
970
+ }
971
+ };
972
+ const getCurrentVariantValue = (key: string) => {
973
+ return updatedVariants ? updatedVariants[key] : ownVariants[key];
974
+ };
826
975
  const ctx = {
827
976
  variants: ownVariants as VariantValues<Record<string, unknown>>,
828
977
  setVariants: (
829
978
  newVariants: VariantValues<Record<string, unknown>>,
830
979
  ) => {
831
980
  if (!hasAnyDisabled) {
832
- Object.assign(ensureUpdated(), newVariants);
981
+ for (const key in newVariants) {
982
+ if (!Object.hasOwn(newVariants, key)) continue;
983
+ const value = (newVariants as Record<string, unknown>)[key];
984
+ setChangedVariant(key, value, true);
985
+ if (getCurrentVariantValue(key) === value) continue;
986
+ ensureUpdated()[key] = value;
987
+ }
833
988
  return;
834
989
  }
835
990
  for (const key in newVariants) {
836
- if (!hasOwn.call(newVariants, key)) continue;
991
+ if (!Object.hasOwn(newVariants, key)) continue;
837
992
  if (disabledVariantKeys.has(key)) continue;
838
993
  const value = (newVariants as Record<string, unknown>)[key];
839
994
  if (hasDisabledVariantValues) {
@@ -845,6 +1000,8 @@ export function create({
845
1000
  continue;
846
1001
  }
847
1002
  }
1003
+ setChangedVariant(key, value, true);
1004
+ if (getCurrentVariantValue(key) === value) continue;
848
1005
  ensureUpdated()[key] = value;
849
1006
  }
850
1007
  },
@@ -852,8 +1009,9 @@ export function create({
852
1009
  newDefaults: VariantValues<Record<string, unknown>>,
853
1010
  ) => {
854
1011
  for (const key in newDefaults) {
855
- if (!hasOwn.call(newDefaults, key)) continue;
1012
+ if (!Object.hasOwn(newDefaults, key)) continue;
856
1013
  if (userVariantProps[key] !== undefined) continue;
1014
+ if (protectedVariantKeys?.has(key)) continue;
857
1015
  const value = (newDefaults as Record<string, unknown>)[key];
858
1016
  if (hasAnyDisabled) {
859
1017
  if (disabledVariantKeys.has(key)) continue;
@@ -865,21 +1023,27 @@ export function create({
865
1023
  continue;
866
1024
  }
867
1025
  }
1026
+ setChangedVariant(key, value);
1027
+ if (pendingProtectedVariants) {
1028
+ pendingProtectedVariants[key] = value;
1029
+ }
1030
+ if (getCurrentVariantValue(key) === value) continue;
868
1031
  ensureUpdated()[key] = value;
869
1032
  }
870
1033
  },
871
1034
  addClass: (className: ClassValue) => {
872
- localCClasses.push(className);
1035
+ localCClasses?.push(className);
873
1036
  },
874
1037
  addStyle: (newStyle: StyleValue) => {
1038
+ if (!collectOutput) return;
875
1039
  if (!localCStyle) localCStyle = {};
876
1040
  Object.assign(localCStyle, newStyle);
877
1041
  },
878
1042
  };
879
- const result = computed(ctx);
880
- if (result != null) {
1043
+ const result = refine(ctx);
1044
+ if (collectOutput && result != null) {
881
1045
  const r = extractClassAndStylePrebuilt(result);
882
- if (r.class != null) localCClasses.push(r.class);
1046
+ if (r.class != null) localCClasses?.push(r.class);
883
1047
  if (r.style) {
884
1048
  if (!localCStyle) localCStyle = {};
885
1049
  Object.assign(localCStyle, r.style);
@@ -888,79 +1052,133 @@ export function create({
888
1052
  cClasses = localCClasses;
889
1053
  cStyle = localCStyle;
890
1054
  if (updatedVariants) {
1055
+ const nextResolved: Record<string, unknown> = {};
1056
+ Object.assign(nextResolved, workingResolved);
891
1057
  if (hasAnyDisabled) {
892
1058
  const filteredUpdated: Record<string, unknown> = {};
893
1059
  filterDisabledInto(updatedVariants, filteredUpdated);
894
- workingResolved = filteredUpdated;
1060
+ Object.assign(nextResolved, filteredUpdated);
895
1061
  } else {
896
- workingResolved = updatedVariants;
1062
+ Object.assign(nextResolved, updatedVariants);
897
1063
  }
1064
+ workingResolved = nextResolved;
898
1065
  }
899
1066
  }
900
1067
 
901
- // Build skip sets to pass to extends. Reuse precomputed values when no
902
- // caller-provided sets need merging.
903
- let extSkipKeys: Set<string> | null;
904
- if (skipKeys === null) {
905
- extSkipKeys = staticExtSkipKeys;
906
- } else if (staticExtSkipKeys === null) {
907
- extSkipKeys = skipKeys;
908
- } else {
909
- extSkipKeys = new Set(skipKeys);
910
- for (const k of staticExtSkipKeys) extSkipKeys.add(k);
911
- }
1068
+ return {
1069
+ workingResolved,
1070
+ changedVariants,
1071
+ classes: cClasses,
1072
+ style: cStyle,
1073
+ };
1074
+ };
912
1075
 
913
- let extSkipVals: Record<string, Set<string>> | null;
914
- if (skipValues === null) {
915
- extSkipVals = staticExtSkipValues;
916
- } else if (staticExtSkipValues === null) {
917
- extSkipVals = skipValues;
918
- } else {
919
- extSkipVals = {};
920
- for (const k in skipValues) {
921
- extSkipVals[k] = skipValues[k];
922
- }
923
- for (const k in staticExtSkipValues) {
924
- const existing = extSkipVals[k];
925
- if (existing) {
926
- const merged = new Set<string>(existing);
927
- for (const v of staticExtSkipValues[k]) merged.add(v);
928
- extSkipVals[k] = merged;
929
- } else {
930
- extSkipVals[k] = staticExtSkipValues[k];
931
- }
932
- }
1076
+ // Core compute path. Called both for top-level rendering (via
1077
+ // `computeResult`) and recursively when this component is used as an
1078
+ // `extend` target by another component. Pushes variant classes (excluding
1079
+ // base class) into `classesOut` and merges styles into `styleOut`.
1080
+ const computeOnce: ComputeFn = (
1081
+ resolved,
1082
+ userVariantProps,
1083
+ skipKeys,
1084
+ skipValues,
1085
+ classesOut,
1086
+ styleOut,
1087
+ runState,
1088
+ protectedVariants,
1089
+ pendingProtectedVariants,
1090
+ protectedVariantKeys,
1091
+ ) => {
1092
+ // Run `refine` (if any). May modify resolved variants and emit classes
1093
+ // and styles.
1094
+ let workingResolved = resolved;
1095
+ let cClasses: ClassValue[] | null = null;
1096
+ let cStyle: StyleValue | null = null;
1097
+ let changedVariants: Record<string, unknown> | null = null;
1098
+ if (refine) {
1099
+ const refineResult = runRefineContext(
1100
+ resolved,
1101
+ userVariantProps,
1102
+ true,
1103
+ true,
1104
+ protectedVariants,
1105
+ pendingProtectedVariants,
1106
+ protectedVariantKeys,
1107
+ );
1108
+ workingResolved = refineResult.workingResolved;
1109
+ cClasses = refineResult.classes;
1110
+ cStyle = refineResult.style;
1111
+ changedVariants = refineResult.changedVariants;
933
1112
  }
934
1113
 
935
1114
  // Run extends' contributions first (their full classes + styles) so our
936
1115
  // own base style and variants apply on top, matching the original
937
1116
  // ext1 → ext2 → … → current ordering.
938
1117
  //
939
- // `workingResolved` is passed as the extends' `userVariantProps`. This
940
- // is deliberate by the time `compute` runs, the resolveDefaults chain
941
- // and our own `computed`'s `setDefaultVariants` have already produced
942
- // the most-specific resolution for every key. Treating those values as
943
- // "user-provided" makes extends' own `setDefaultVariants` skip them, so
944
- // extends emit variant classes that match what we resolved (rather than
945
- // re-running their own defaults and emitting a different class).
946
- // Replacing this with the original `userVariantProps` looks cleaner but
947
- // breaks "child computed setDefaultVariants overrides parent computed
948
- // setDefaultVariants" in `tests/computed.test.ts` — extends would then
949
- // overwrite values the descendant already resolved.
1118
+ // Pass explicit user values plus refine changes as the extends'
1119
+ // `userVariantProps`. This lets more-specific refine decisions stick
1120
+ // across re-runs while inherited static defaults can still be refined by
1121
+ // the extended component's own refine chain.
950
1122
  if (hasExtend) {
1123
+ // Build skip sets to pass to extends. Reuse precomputed values when no
1124
+ // caller-provided sets need merging.
1125
+ let extSkipKeys: Set<string> | null;
1126
+ if (skipKeys === null) {
1127
+ extSkipKeys = staticExtSkipKeys;
1128
+ } else if (staticExtSkipKeys === null) {
1129
+ extSkipKeys = skipKeys;
1130
+ } else {
1131
+ extSkipKeys = new Set(skipKeys);
1132
+ for (const k of staticExtSkipKeys) extSkipKeys.add(k);
1133
+ }
1134
+
1135
+ let extSkipVals: Record<string, Set<string>> | null;
1136
+ if (skipValues === null) {
1137
+ extSkipVals = staticExtSkipValues;
1138
+ } else if (staticExtSkipValues === null) {
1139
+ extSkipVals = skipValues;
1140
+ } else {
1141
+ extSkipVals = {};
1142
+ for (const k in skipValues) {
1143
+ extSkipVals[k] = skipValues[k];
1144
+ }
1145
+ for (const k in staticExtSkipValues) {
1146
+ const existing = extSkipVals[k];
1147
+ if (existing) {
1148
+ const merged = new Set<string>(existing);
1149
+ for (const v of staticExtSkipValues[k]) merged.add(v);
1150
+ extSkipVals[k] = merged;
1151
+ } else {
1152
+ extSkipVals[k] = staticExtSkipValues[k];
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ const extUserVariantProps =
1158
+ extMetasWithRefineCount > 0
1159
+ ? getExtUserVariantProps(
1160
+ userVariantProps,
1161
+ protectedVariants ?? null,
1162
+ changedVariants,
1163
+ )
1164
+ : userVariantProps;
951
1165
  for (let i = 0; i < extCount; i++) {
952
1166
  if (hasIsolatedExt && extIsolated[i]) {
953
1167
  // Isolated extend (different factory): gather its variant classes
954
1168
  // into a scratch array, then push the joined string after applying
955
1169
  // its own transformClass. Our outer transform applies on top.
956
1170
  const extClasses: ClsxClassValue[] = [];
957
- extMetas[i].compute(
958
- workingResolved,
1171
+ workingResolved = extMetas[i].compute(
959
1172
  workingResolved,
1173
+ extUserVariantProps,
960
1174
  extSkipKeys,
961
1175
  extSkipVals,
962
1176
  extClasses,
963
1177
  styleOut,
1178
+ runState,
1179
+ protectedVariants,
1180
+ pendingProtectedVariants,
1181
+ protectedVariantKeys,
964
1182
  );
965
1183
  if (extClasses.length > 0) {
966
1184
  const joined = clsx(extClasses);
@@ -971,15 +1189,24 @@ export function create({
971
1189
  }
972
1190
  }
973
1191
  } else {
974
- extMetas[i].compute(
975
- workingResolved,
1192
+ workingResolved = extMetas[i].compute(
976
1193
  workingResolved,
1194
+ extUserVariantProps,
977
1195
  extSkipKeys,
978
1196
  extSkipVals,
979
1197
  classesOut,
980
1198
  styleOut,
1199
+ runState,
1200
+ protectedVariants,
1201
+ pendingProtectedVariants,
1202
+ protectedVariantKeys,
981
1203
  );
982
1204
  }
1205
+ // Only sync protected variants when a child refine resolver can
1206
+ // observe them. Otherwise extUserVariantProps may alias caller props.
1207
+ if (protectedVariants && extMetasWithRefineCount > 0) {
1208
+ Object.assign(extUserVariantProps, protectedVariants);
1209
+ }
983
1210
  }
984
1211
  }
985
1212
 
@@ -987,9 +1214,10 @@ export function create({
987
1214
  if (hasBaseStyle) Object.assign(styleOut, baseStyle);
988
1215
 
989
1216
  // Apply own variants. Skip keys/values come from caller (e.g., parent
990
- // wants its own computedVariants to override this variant).
1217
+ // wants its own function variant to override this variant).
991
1218
  // `variantEntryNames` already excludes disabled keys (those with `null`
992
- // value in config), so we don't re-check `disabledVariantKeys` here.
1219
+ // value in config) and function variants, so we don't re-check
1220
+ // `disabledVariantKeys` here.
993
1221
  const ownSkipKeys = skipKeys;
994
1222
  const ownSkipValues = skipValues;
995
1223
  for (let i = 0; i < variantEntryCount; i++) {
@@ -1027,9 +1255,11 @@ export function create({
1027
1255
  }
1028
1256
  }
1029
1257
 
1030
- // Apply computedVariants.
1031
- for (let i = 0; i < computedVariantCount; i++) {
1032
- const variantName = computedVariantNames[i];
1258
+ // Apply function variants — entries in `variants` whose value is a
1259
+ // function. They run after static variants and override any same-key
1260
+ // inherited variant via `staticExtSkipKeys`.
1261
+ for (let i = 0; i < functionVariantCount; i++) {
1262
+ const variantName = functionVariantNames[i];
1033
1263
  if (ownSkipKeys && ownSkipKeys.has(variantName)) continue;
1034
1264
  const selectedValue = workingResolved[variantName];
1035
1265
  if (selectedValue === undefined) continue;
@@ -1041,7 +1271,7 @@ export function create({
1041
1271
  ) {
1042
1272
  continue;
1043
1273
  }
1044
- const fn = computedVariantFns[i];
1274
+ const fn = functionVariantFns[i];
1045
1275
  const computedResult = fn(selectedValue);
1046
1276
  if (computedResult == null) continue;
1047
1277
  const r = extractClassAndStylePrebuilt(computedResult);
@@ -1049,15 +1279,274 @@ export function create({
1049
1279
  if (r.style) Object.assign(styleOut, r.style);
1050
1280
  }
1051
1281
 
1052
- // Apply `computed` results — must come after own variants/computedVariants.
1282
+ // Apply `refine` results — must come after own variants (static and
1283
+ // function).
1053
1284
  if (cClasses) {
1054
1285
  for (let i = 0; i < cClasses.length; i++) {
1055
1286
  classesOut.push(cClasses[i] as ClsxClassValue);
1056
1287
  }
1057
1288
  }
1058
1289
  if (cStyle) Object.assign(styleOut, cStyle);
1290
+
1291
+ return workingResolved;
1292
+ };
1293
+
1294
+ const compute: ComputeFn =
1295
+ !refine && extMetasWithRefineCount === 0
1296
+ ? (
1297
+ resolved,
1298
+ userVariantProps,
1299
+ skipKeys,
1300
+ skipValues,
1301
+ classesOut,
1302
+ styleOut,
1303
+ runState,
1304
+ protectedVariants,
1305
+ pendingProtectedVariants,
1306
+ protectedVariantKeys,
1307
+ ) => {
1308
+ return computeOnce(
1309
+ resolved,
1310
+ userVariantProps,
1311
+ skipKeys,
1312
+ skipValues,
1313
+ classesOut,
1314
+ styleOut,
1315
+ runState,
1316
+ protectedVariants,
1317
+ pendingProtectedVariants,
1318
+ protectedVariantKeys,
1319
+ );
1320
+ }
1321
+ : (
1322
+ resolved,
1323
+ userVariantProps,
1324
+ skipKeys,
1325
+ skipValues,
1326
+ classesOut,
1327
+ styleOut,
1328
+ runState,
1329
+ protectedVariants,
1330
+ pendingProtectedVariants,
1331
+ protectedVariantKeys,
1332
+ ) => {
1333
+ runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1334
+ protectedVariants ??= {};
1335
+ protectedVariantKeys ??= new Set<string>();
1336
+ let workingResolved = resolved;
1337
+ let lastClasses: ClsxClassValue[] = [];
1338
+ let lastStyle: StyleValue = {};
1339
+ let isFirstRun = true;
1340
+
1341
+ while (runState.remaining > 0) {
1342
+ runState.remaining -= 1;
1343
+ let useDirectOutput = isFirstRun;
1344
+ if (useDirectOutput) {
1345
+ for (const key in styleOut) {
1346
+ if (Object.hasOwn(styleOut, key)) {
1347
+ useDirectOutput = false;
1348
+ break;
1349
+ }
1350
+ }
1351
+ }
1352
+ const classCount = classesOut.length;
1353
+ const nextPendingProtectedVariants: Record<string, unknown> = {};
1354
+ const nextClasses: ClsxClassValue[] = useDirectOutput
1355
+ ? classesOut
1356
+ : [];
1357
+ const nextStyle: StyleValue = useDirectOutput ? styleOut : {};
1358
+ const nextResolved = computeOnce(
1359
+ workingResolved,
1360
+ userVariantProps,
1361
+ skipKeys,
1362
+ skipValues,
1363
+ nextClasses,
1364
+ nextStyle,
1365
+ runState,
1366
+ protectedVariants,
1367
+ nextPendingProtectedVariants,
1368
+ protectedVariantKeys,
1369
+ );
1370
+
1371
+ let protectedChanged: boolean;
1372
+ if (pendingProtectedVariants) {
1373
+ protectedChanged = mergeVariants(
1374
+ pendingProtectedVariants,
1375
+ nextPendingProtectedVariants,
1376
+ protectedVariantKeys,
1377
+ );
1378
+ } else {
1379
+ protectedChanged = mergeVariants(
1380
+ protectedVariants,
1381
+ nextPendingProtectedVariants,
1382
+ protectedVariantKeys,
1383
+ );
1384
+ }
1385
+
1386
+ if (
1387
+ !protectedChanged &&
1388
+ (nextResolved === workingResolved ||
1389
+ areVariantsEqual(workingResolved, nextResolved))
1390
+ ) {
1391
+ if (!useDirectOutput) {
1392
+ for (let i = 0; i < nextClasses.length; i++) {
1393
+ classesOut.push(nextClasses[i]);
1394
+ }
1395
+ Object.assign(styleOut, nextStyle);
1396
+ }
1397
+ return nextResolved;
1398
+ }
1399
+
1400
+ if (useDirectOutput && runState.remaining === 0) {
1401
+ // Keep the direct output from the last allowed run. Rolling
1402
+ // back here would drop it before the fallback copy below.
1403
+ warnRefineLimit(runState);
1404
+ return nextResolved;
1405
+ }
1406
+
1407
+ if (useDirectOutput) {
1408
+ classesOut.length = classCount;
1409
+ for (const key in styleOut) {
1410
+ if (Object.hasOwn(styleOut, key)) {
1411
+ Reflect.deleteProperty(styleOut, key);
1412
+ }
1413
+ }
1414
+ } else {
1415
+ lastClasses = nextClasses;
1416
+ lastStyle = nextStyle;
1417
+ }
1418
+
1419
+ workingResolved = nextResolved;
1420
+ isFirstRun = false;
1421
+ }
1422
+
1423
+ warnRefineLimit(runState);
1424
+
1425
+ for (let i = 0; i < lastClasses.length; i++) {
1426
+ classesOut.push(lastClasses[i]);
1427
+ }
1428
+ Object.assign(styleOut, lastStyle);
1429
+ return workingResolved;
1430
+ };
1431
+
1432
+ const resolveRefineOnce: ResolveRefineFn = (
1433
+ resolved,
1434
+ userVariantProps,
1435
+ filterOwnVariants = true,
1436
+ runState,
1437
+ protectedVariants,
1438
+ pendingProtectedVariants,
1439
+ protectedVariantKeys,
1440
+ ) => {
1441
+ let workingResolved = resolved;
1442
+ let changedVariants: Record<string, unknown> | null = null;
1443
+ if (refine) {
1444
+ const refineResult = runRefineContext(
1445
+ resolved,
1446
+ userVariantProps,
1447
+ filterOwnVariants,
1448
+ false,
1449
+ protectedVariants,
1450
+ pendingProtectedVariants,
1451
+ protectedVariantKeys,
1452
+ );
1453
+ workingResolved = refineResult.workingResolved;
1454
+ changedVariants = refineResult.changedVariants;
1455
+ }
1456
+
1457
+ if (extMetasWithRefineCount > 0) {
1458
+ const extUserVariantProps = getExtUserVariantProps(
1459
+ userVariantProps,
1460
+ protectedVariants ?? null,
1461
+ changedVariants,
1462
+ );
1463
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
1464
+ const meta = extMetasWithRefine[i];
1465
+ const resolveRefine = meta.resolveRefine;
1466
+ if (!resolveRefine) continue;
1467
+ workingResolved = resolveRefine(
1468
+ workingResolved,
1469
+ extUserVariantProps,
1470
+ true,
1471
+ runState,
1472
+ protectedVariants,
1473
+ pendingProtectedVariants,
1474
+ protectedVariantKeys,
1475
+ );
1476
+ if (protectedVariants) {
1477
+ Object.assign(extUserVariantProps, protectedVariants);
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ return workingResolved;
1059
1483
  };
1060
1484
 
1485
+ const resolveRefine: ResolveRefineFn | null =
1486
+ refine || extMetasWithRefineCount > 0
1487
+ ? (
1488
+ resolved,
1489
+ userVariantProps,
1490
+ filterOwnVariants = true,
1491
+ runState,
1492
+ protectedVariants,
1493
+ pendingProtectedVariants,
1494
+ protectedVariantKeys,
1495
+ ) => {
1496
+ runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1497
+ protectedVariants ??= {};
1498
+ protectedVariantKeys ??= new Set<string>();
1499
+ let workingResolved = resolved;
1500
+ let reachedLimit = true;
1501
+
1502
+ while (runState.remaining > 0) {
1503
+ runState.remaining -= 1;
1504
+ const nextPendingProtectedVariants: Record<string, unknown> = {};
1505
+ const nextResolved = resolveRefineOnce(
1506
+ workingResolved,
1507
+ userVariantProps,
1508
+ filterOwnVariants,
1509
+ runState,
1510
+ protectedVariants,
1511
+ nextPendingProtectedVariants,
1512
+ protectedVariantKeys,
1513
+ );
1514
+ let protectedChanged: boolean;
1515
+ if (pendingProtectedVariants) {
1516
+ protectedChanged = mergeVariants(
1517
+ pendingProtectedVariants,
1518
+ nextPendingProtectedVariants,
1519
+ protectedVariantKeys,
1520
+ );
1521
+ } else {
1522
+ protectedChanged = mergeVariants(
1523
+ protectedVariants,
1524
+ nextPendingProtectedVariants,
1525
+ protectedVariantKeys,
1526
+ );
1527
+ }
1528
+
1529
+ if (
1530
+ !protectedChanged &&
1531
+ (nextResolved === workingResolved ||
1532
+ areVariantsEqual(workingResolved, nextResolved))
1533
+ ) {
1534
+ workingResolved = nextResolved;
1535
+ reachedLimit = false;
1536
+ break;
1537
+ }
1538
+
1539
+ workingResolved = nextResolved;
1540
+ }
1541
+
1542
+ if (reachedLimit) {
1543
+ warnRefineLimit(runState);
1544
+ }
1545
+
1546
+ return workingResolved;
1547
+ }
1548
+ : null;
1549
+
1061
1550
  // Top-level: resolves variants from user props, calls compute, then
1062
1551
  // assembles className and style with user-provided class/style overrides.
1063
1552
  const computeResult = (
@@ -1073,26 +1562,26 @@ export function create({
1073
1562
  Object.assign(resolved, staticDefaults);
1074
1563
 
1075
1564
  let userVariantProps: Record<string, unknown>;
1076
- if (extMetasWithResolveDefaultsCount > 0) {
1565
+ if (extMetasWithRefineCount > 0) {
1077
1566
  // Some extends need a resolveDefaults pass. They expect a variant-only
1078
1567
  // object as `userProps`, so we extract one.
1079
1568
  const variantProps: Record<string, unknown> = {};
1080
1569
  for (let i = 0; i < variantKeysLength; i++) {
1081
1570
  const key = variantKeys[i];
1082
- if (hasOwn.call(propsRecord, key)) {
1571
+ if (Object.hasOwn(propsRecord, key)) {
1083
1572
  variantProps[key] = propsRecord[key];
1084
1573
  }
1085
1574
  }
1086
- for (let i = 0; i < extMetasWithResolveDefaultsCount; i++) {
1087
- const meta = extMetasWithResolveDefaults[i];
1088
- const extComputed = meta.resolveDefaults!(resolved, variantProps);
1089
- for (const k in extComputed) {
1090
- if (!hasOwn.call(extComputed, k)) continue;
1091
- resolved[k] = extComputed[k];
1575
+ for (let i = 0; i < extMetasWithRefineCount; i++) {
1576
+ const meta = extMetasWithRefine[i];
1577
+ const extDefaults = meta.resolveDefaults!(resolved, variantProps);
1578
+ for (const k in extDefaults) {
1579
+ if (!Object.hasOwn(extDefaults, k)) continue;
1580
+ resolved[k] = extDefaults[k];
1092
1581
  }
1093
1582
  }
1094
1583
  for (const k in variantProps) {
1095
- if (!hasOwn.call(variantProps, k)) continue;
1584
+ if (!Object.hasOwn(variantProps, k)) continue;
1096
1585
  const v = variantProps[k];
1097
1586
  if (v === undefined) continue;
1098
1587
  resolved[k] = v;
@@ -1100,11 +1589,11 @@ export function create({
1100
1589
  userVariantProps = variantProps;
1101
1590
  } else {
1102
1591
  // Fast path: walk variantKeys directly against propsRecord. Use
1103
- // hasOwn so a polluted Object.prototype can't introduce variant
1592
+ // Object.hasOwn so a polluted Object.prototype can't introduce variant
1104
1593
  // values the user didn't pass.
1105
1594
  for (let i = 0; i < variantKeysLength; i++) {
1106
1595
  const key = variantKeys[i];
1107
- if (!hasOwn.call(propsRecord, key)) continue;
1596
+ if (!Object.hasOwn(propsRecord, key)) continue;
1108
1597
  const v = propsRecord[key];
1109
1598
  if (v === undefined) continue;
1110
1599
  resolved[key] = v;
@@ -1162,67 +1651,8 @@ export function create({
1162
1651
  const getVariants = (variants?: VariantValues<MergedVariants>) => {
1163
1652
  const variantProps = variants ?? EMPTY_DEFAULTS;
1164
1653
  let resolvedVariants = resolveVariantsHot(variantProps);
1165
- // Run computed function to get variants set via setVariants and
1166
- // setDefaultVariants
1167
- if (computed) {
1168
- const updatedVariants: Record<string, unknown> = {};
1169
- Object.assign(updatedVariants, resolvedVariants);
1170
- const ctx = {
1171
- variants: resolvedVariants as VariantValues<Record<string, unknown>>,
1172
- setVariants: (
1173
- newVariants: VariantValues<Record<string, unknown>>,
1174
- ) => {
1175
- if (!hasAnyDisabled) {
1176
- Object.assign(updatedVariants, newVariants);
1177
- return;
1178
- }
1179
- for (const key in newVariants) {
1180
- if (!hasOwn.call(newVariants, key)) continue;
1181
- if (disabledVariantKeys.has(key)) continue;
1182
- const value = (newVariants as Record<string, unknown>)[key];
1183
- if (hasDisabledVariantValues) {
1184
- const valueKey = getVariantValueKey(value);
1185
- if (
1186
- valueKey != null &&
1187
- disabledVariantValues[key]?.has(valueKey)
1188
- ) {
1189
- continue;
1190
- }
1191
- }
1192
- updatedVariants[key] = value;
1193
- }
1194
- },
1195
- setDefaultVariants: (
1196
- newDefaults: VariantValues<Record<string, unknown>>,
1197
- ) => {
1198
- for (const key in newDefaults) {
1199
- if (!hasOwn.call(newDefaults, key)) continue;
1200
- if (variantProps[key] !== undefined) continue;
1201
- const value = (newDefaults as Record<string, unknown>)[key];
1202
- if (hasAnyDisabled) {
1203
- if (disabledVariantKeys.has(key)) continue;
1204
- const valueKey = getVariantValueKey(value);
1205
- if (
1206
- valueKey != null &&
1207
- disabledVariantValues[key]?.has(valueKey)
1208
- ) {
1209
- continue;
1210
- }
1211
- }
1212
- updatedVariants[key] = value;
1213
- }
1214
- },
1215
- addClass: noop,
1216
- addStyle: noop,
1217
- };
1218
- computed(ctx);
1219
- if (hasAnyDisabled) {
1220
- const filteredUpdated: Record<string, unknown> = {};
1221
- filterDisabledInto(updatedVariants, filteredUpdated);
1222
- resolvedVariants = filteredUpdated;
1223
- } else {
1224
- resolvedVariants = updatedVariants;
1225
- }
1654
+ if (resolveRefine) {
1655
+ resolvedVariants = resolveRefine(resolvedVariants, variantProps, false);
1226
1656
  }
1227
1657
  return resolvedVariants as VariantValues<MergedVariants>;
1228
1658
  };
@@ -1233,10 +1663,12 @@ export function create({
1233
1663
  // `transformClass(clsx(allClasses))` at render time, so applying it here
1234
1664
  // would compound (double for own-render, triple+ for extend chains) and
1235
1665
  // misbehave for non-idempotent transforms.
1236
- const computedBaseClass = clsx(
1237
- ...(extBaseClassesArr as ClsxClassValue[]),
1238
- config.class as ClsxClassValue,
1239
- );
1666
+ const computedBaseClass = hasExtend
1667
+ ? clsx(
1668
+ ...(extBaseClassesArr as ClsxClassValue[]),
1669
+ config.class as ClsxClassValue,
1670
+ )
1671
+ : clsx(config.class as ClsxClassValue);
1240
1672
 
1241
1673
  // Shared closures across the default and modal components.
1242
1674
  const classFn = (props: ComponentProps<MergedVariants> = {}) => {
@@ -1247,7 +1679,9 @@ export function create({
1247
1679
  staticDefaults,
1248
1680
  resolveDefaults: resolveDefaultsFn,
1249
1681
  compute,
1682
+ resolveRefine,
1250
1683
  transformClass,
1684
+ functionVariantKeys,
1251
1685
  };
1252
1686
 
1253
1687
  const initComponent = <
@@ -1255,15 +1689,14 @@ export function create({
1255
1689
  T extends ModalComponent<MergedVariants, R>,
1256
1690
  >(
1257
1691
  c: T,
1258
- keys: string[],
1692
+ propKeys: string[],
1259
1693
  style: T["style"],
1260
1694
  ): T => {
1261
1695
  c.class = classFn;
1262
1696
  c.style = style;
1263
1697
  c.getVariants = getVariants;
1264
- c.keys = keys;
1265
1698
  c.variantKeys = variantKeys;
1266
- c.propKeys = keys;
1699
+ c.propKeys = propKeys;
1267
1700
  setComponentMeta(c, meta);
1268
1701
  return c;
1269
1702
  };
@@ -1272,7 +1705,7 @@ export function create({
1272
1705
  const defaultComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1273
1706
  const { className, style } = computeResult(props);
1274
1707
  return { class: className, style };
1275
- }) as CVComponent<V, CV, E>;
1708
+ }) as CVComponent<V, E>;
1276
1709
  initComponent(defaultComponent, inputPropsKeys, (props = {}) => {
1277
1710
  return computeResult(props).style;
1278
1711
  });