clava 0.2.0 → 0.2.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
@@ -21,8 +21,6 @@ import type {
21
21
  Variants,
22
22
  } from "./types.ts";
23
23
  import {
24
- type Mode,
25
- getClassPropertyName,
26
24
  htmlObjStyleToStyleValue,
27
25
  htmlStyleToStyleValue,
28
26
  isHTMLObjStyle,
@@ -50,6 +48,9 @@ const META_KEY = "__meta";
50
48
  const SKIP_STYLE_KEYS = Symbol("skipStyleKeys");
51
49
  const SKIP_STYLE_VARIANT_VALUES = Symbol("skipStyleVariantValues");
52
50
 
51
+ // eslint-disable-next-line @typescript-eslint/unbound-method
52
+ const hasOwn = Object.prototype.hasOwnProperty;
53
+
53
54
  // Dynamic property access on function requires cast through unknown
54
55
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
55
56
  return (component as unknown as Record<string, unknown>)[META_KEY] as
@@ -67,7 +68,7 @@ function setComponentMeta(component: AnyComponent, meta: ComponentMeta): void {
67
68
  */
68
69
  function assign<T extends object>(target: T, source: T): void {
69
70
  for (const key in source) {
70
- if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
71
+ if (!hasOwn.call(source, key)) continue;
71
72
  (target as Record<string, unknown>)[key] = (
72
73
  source as Record<string, unknown>
73
74
  )[key];
@@ -150,30 +151,30 @@ function normalizeStyle(style: unknown): StyleValue {
150
151
  }
151
152
 
152
153
  /**
153
- * Extracts class and style from a style-class value object.
154
+ * Pre-extracts the class and (normalized) style from a variant value once at
155
+ * component creation time. Returns `null` if the value contributes nothing.
154
156
  */
155
- function extractStyleClass(value: StyleClassValue): {
157
+ interface PrebuiltValue {
156
158
  class: ClassValue;
157
- style: StyleValue;
158
- } {
159
- return { class: value.class, style: normalizeStyle(value.style) };
159
+ style: StyleValue | null;
160
160
  }
161
161
 
162
- /**
163
- * Extracts class and style from a variant value (either a class value or a
164
- * style-class object).
165
- */
166
- function extractClassAndStyle(value: unknown): {
167
- class: ClassValue;
168
- style: StyleValue;
169
- } {
162
+ function extractStyleClassPrebuilt(value: StyleClassValue): PrebuiltValue {
163
+ const styleNorm = normalizeStyle(value.style);
164
+ return {
165
+ class: value.class ?? null,
166
+ style: styleNorm && Object.keys(styleNorm).length > 0 ? styleNorm : null,
167
+ };
168
+ }
169
+
170
+ function extractClassAndStylePrebuilt(value: unknown): PrebuiltValue {
170
171
  if (isStyleClassValue(value)) {
171
- return extractStyleClass(value);
172
+ return extractStyleClassPrebuilt(value);
172
173
  }
173
174
  if (isRecordObject(value)) {
174
- return { class: null, style: {} };
175
+ return { class: null, style: null };
175
176
  }
176
- return { class: value as ClassValue, style: {} };
177
+ return { class: value as ClassValue, style: null };
177
178
  }
178
179
 
179
180
  /**
@@ -185,18 +186,19 @@ function collectVariantKeys(
185
186
  ): string[] {
186
187
  const keys = new Set<string>();
187
188
 
188
- // Collect from extended components
189
189
  if (config.extend) {
190
190
  for (const ext of config.extend) {
191
- for (const key of ext.variantKeys) {
192
- keys.add(key as string);
191
+ const extKeys = ext.variantKeys as readonly string[];
192
+ for (let i = 0; i < extKeys.length; i++) {
193
+ keys.add(extKeys[i]);
193
194
  }
194
195
  }
195
196
  }
196
197
 
197
- // Collect from variants
198
198
  if (config.variants) {
199
- for (const [key, variant] of Object.entries(config.variants)) {
199
+ for (const key in config.variants) {
200
+ if (!hasOwn.call(config.variants, key)) continue;
201
+ const variant = (config.variants as Record<string, unknown>)[key];
200
202
  if (variant === null) {
201
203
  keys.delete(key);
202
204
  continue;
@@ -205,9 +207,9 @@ function collectVariantKeys(
205
207
  }
206
208
  }
207
209
 
208
- // Collect from computedVariants
209
210
  if (config.computedVariants) {
210
- for (const key of Object.keys(config.computedVariants)) {
211
+ for (const key in config.computedVariants) {
212
+ if (!hasOwn.call(config.computedVariants, key)) continue;
211
213
  keys.add(key);
212
214
  }
213
215
  }
@@ -241,26 +243,14 @@ function isVariantValueDisabled(
241
243
  return variant[valueKey] === null;
242
244
  }
243
245
 
244
- function filterDisabledVariants(
245
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
246
- variants: Record<string, unknown>,
247
- ): Record<string, unknown> {
248
- const filtered: Record<string, unknown> = {};
249
- for (const [key, value] of Object.entries(variants)) {
250
- if (isVariantDisabled(config, key)) continue;
251
- if (isVariantValueDisabled(config, key, value)) continue;
252
- filtered[key] = value;
253
- }
254
- return filtered;
255
- }
256
-
257
246
  function collectDisabledVariantKeys(
258
247
  config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
259
248
  ): Set<string> {
260
249
  const keys = new Set<string>();
261
250
  if (!config.variants) return keys;
262
- for (const [key, value] of Object.entries(config.variants)) {
263
- if (value === null) {
251
+ for (const key in config.variants) {
252
+ if (!hasOwn.call(config.variants, key)) continue;
253
+ if ((config.variants as Record<string, unknown>)[key] === null) {
264
254
  keys.add(key);
265
255
  }
266
256
  }
@@ -272,162 +262,24 @@ function collectDisabledVariantValues(
272
262
  ): Record<string, Set<string>> {
273
263
  const values: Record<string, Set<string>> = {};
274
264
  if (!config.variants) return values;
275
- for (const [key, variant] of Object.entries(config.variants)) {
265
+ for (const key in config.variants) {
266
+ if (!hasOwn.call(config.variants, key)) continue;
267
+ const variant = (config.variants as Record<string, unknown>)[key];
276
268
  if (!isRecordObject(variant)) continue;
277
- for (const [variantValue, variantEntry] of Object.entries(variant)) {
278
- if (variantEntry !== null) continue;
279
- if (!values[key]) {
280
- values[key] = new Set<string>();
269
+ let bucket: Set<string> | undefined;
270
+ for (const variantValue in variant) {
271
+ if (!hasOwn.call(variant, variantValue)) continue;
272
+ if (variant[variantValue] !== null) continue;
273
+ if (!bucket) {
274
+ bucket = new Set<string>();
275
+ values[key] = bucket;
281
276
  }
282
- values[key].add(variantValue);
277
+ bucket.add(variantValue);
283
278
  }
284
279
  }
285
280
  return values;
286
281
  }
287
282
 
288
- function mergeDisabledVariantValues(
289
- base: Record<string, Set<string>>,
290
- override: Record<string, Set<string>>,
291
- ): Record<string, Set<string>> {
292
- const merged: Record<string, Set<string>> = {};
293
- for (const [key, values] of Object.entries(base)) {
294
- merged[key] = new Set(values);
295
- }
296
- for (const [key, values] of Object.entries(override)) {
297
- if (!merged[key]) {
298
- merged[key] = new Set<string>();
299
- }
300
- for (const value of values) {
301
- merged[key].add(value);
302
- }
303
- }
304
- return merged;
305
- }
306
-
307
- /**
308
- * Collects static default variants from extended components and the current
309
- * config. Also handles implicit boolean defaults (when only `false` key
310
- * exists). This does NOT trigger computed functions - use collectDefaultVariants
311
- * for that.
312
- */
313
- function collectStaticDefaults(
314
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
315
- ): Record<string, unknown> {
316
- const defaults: Record<string, unknown> = {};
317
-
318
- // Collect static defaults from extended components (via metadata to avoid
319
- // triggering computed functions)
320
- if (config.extend) {
321
- for (const ext of config.extend) {
322
- const meta = getComponentMeta(ext);
323
- if (meta) {
324
- Object.assign(defaults, meta.staticDefaults);
325
- }
326
- }
327
- }
328
-
329
- // Handle implicit boolean defaults from variants
330
- // If a variant has a `false` key, default to false when no value is provided
331
- if (config.variants) {
332
- for (const [variantName, variantDef] of Object.entries(config.variants)) {
333
- if (!isRecordObject(variantDef)) continue;
334
- const keys = Object.keys(variantDef);
335
- const hasFalse = keys.includes("false");
336
- if (hasFalse && !defaults[variantName]) {
337
- defaults[variantName] = false;
338
- }
339
- }
340
- }
341
-
342
- // Override with current config's static defaults
343
- if (config.defaultVariants) {
344
- Object.assign(defaults, config.defaultVariants);
345
- }
346
-
347
- return filterDisabledVariants(config, defaults);
348
- }
349
-
350
- /**
351
- * Collects default variants from extended components and the current config.
352
- * This includes both static defaults and computed defaults (from
353
- * setDefaultVariants in extended components' computed functions). Priority:
354
- * parent static < child static < parent computed < child computed.
355
- */
356
- function collectDefaultVariants(
357
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
358
- propsVariants: Record<string, unknown> = {},
359
- ): Record<string, unknown> {
360
- // Start with static defaults (parent static < child static)
361
- const defaults = collectStaticDefaults(config);
362
-
363
- // Apply computed defaults from extended components
364
- // Parent's setDefaultVariants should override child's static defaults
365
- if (!config.extend) return defaults;
366
-
367
- // Pass full static defaults (not just this config's defaultVariants)
368
- // so that intermediate components' defaults are visible to ancestors
369
- for (const ext of config.extend) {
370
- const meta = getComponentMeta(ext);
371
- if (!meta) continue;
372
- Object.assign(defaults, meta.resolveDefaults(defaults, propsVariants));
373
- }
374
-
375
- return filterDisabledVariants(config, defaults);
376
- }
377
-
378
- /**
379
- * Filters out keys with undefined values from an object.
380
- */
381
- function filterUndefined(
382
- obj: Record<string, unknown>,
383
- ): Record<string, unknown> {
384
- const result: Record<string, unknown> = {};
385
- for (const [key, value] of Object.entries(obj)) {
386
- if (value === undefined) continue;
387
- result[key] = value;
388
- }
389
- return result;
390
- }
391
-
392
- /**
393
- * Resolves variant values by merging defaults with provided props. Props with
394
- * undefined values are filtered out so they don't override defaults.
395
- */
396
- function resolveVariants(
397
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
398
- props: Record<string, unknown> = {},
399
- ): Record<string, unknown> {
400
- const defaults = collectDefaultVariants(config, props);
401
- return filterDisabledVariants(config, {
402
- ...defaults,
403
- ...filterUndefined(props),
404
- });
405
- }
406
-
407
- /**
408
- * Gets the value for a single variant based on the variant definition and the
409
- * selected value.
410
- */
411
- function getVariantResult(
412
- variantDef: unknown,
413
- selectedValue: unknown,
414
- ): { class: ClassValue; style: StyleValue } {
415
- // Shorthand variant: `disabled: "disabled-class"` means { true: "..." }
416
- if (!isRecordObject(variantDef)) {
417
- if (selectedValue === true) {
418
- return extractClassAndStyle(variantDef);
419
- }
420
- return { class: null, style: {} };
421
- }
422
-
423
- // Object variant: { sm: "...", lg: "..." }
424
- const key = String(selectedValue);
425
- const value = variantDef[key];
426
- if (value === undefined) return { class: null, style: {} };
427
-
428
- return extractClassAndStyle(value);
429
- }
430
-
431
283
  /**
432
284
  * Extracts classes from fullClass that are not in baseClass. Uses string
433
285
  * comparison optimization: if fullClass starts with baseClass, just take the
@@ -450,206 +302,23 @@ function extractVariantClasses(fullClass: string, baseClass: string): string {
450
302
  .join(" ");
451
303
  }
452
304
 
453
- function computeExtendedStyles(
454
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
455
- resolvedVariants: Record<string, unknown>,
456
- overrideVariantKeys: Set<string> = new Set(),
457
- overrideVariantValues: Record<string, Set<string>> = {},
458
- ): {
459
- baseClasses: ClassValue[];
460
- variantClasses: ClassValue[];
461
- style: StyleValue;
462
- } {
463
- const baseClasses: ClassValue[] = [];
464
- const variantClasses: ClassValue[] = [];
465
- const style: StyleValue = {};
466
-
467
- if (!config.extend) return { baseClasses, variantClasses, style };
468
- const hasOverrideVariantKeys = overrideVariantKeys.size > 0;
469
- const hasOverrideVariantValues =
470
- Object.keys(overrideVariantValues).length > 0;
471
-
472
- for (const ext of config.extend) {
473
- // Pass actual variant values but mark which keys should skip styling.
474
- // Using a Symbol property keeps variant values clean for computed functions
475
- // while still allowing us to skip styling for overridden keys.
476
- const propsForExt: Record<string | symbol, unknown> = {
477
- ...resolvedVariants,
478
- };
479
- if (hasOverrideVariantKeys) {
480
- propsForExt[SKIP_STYLE_KEYS] = overrideVariantKeys;
481
- }
482
- if (hasOverrideVariantValues) {
483
- propsForExt[SKIP_STYLE_VARIANT_VALUES] = overrideVariantValues;
484
- }
485
-
486
- const extResult = ext(
487
- propsForExt as ComponentProps<Record<string, unknown>>,
488
- );
489
- assign(style, normalizeStyle(extResult.style));
490
-
491
- // Get base class from internal metadata (no variants)
492
- const meta = getComponentMeta(ext);
493
- const baseClass = meta?.baseClass ?? "";
494
- baseClasses.push(baseClass);
495
-
496
- // Get full class with variants
497
- const fullClass =
498
- "className" in extResult ? extResult.className : extResult.class;
499
-
500
- const variantPortion = extractVariantClasses(fullClass, baseClass);
501
- if (variantPortion) {
502
- variantClasses.push(variantPortion);
503
- }
504
- }
505
-
506
- return { baseClasses, variantClasses, style };
507
- }
508
-
509
- /**
510
- * Computes class and style from the component's own variants and
511
- * computedVariants (not extended components).
512
- */
513
- function computeVariantStyles(
514
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
515
- resolvedVariants: Record<string | symbol, unknown>,
516
- skipStyleKeys: Set<string> = new Set(),
517
- skipVariantValues: Record<string, Set<string>> = {},
518
- ): { classes: ClassValue[]; style: StyleValue } {
519
- const classes: ClassValue[] = [];
520
- const style: StyleValue = {};
521
-
522
- // Process current component's variants
523
- if (config.variants) {
524
- for (const [variantName, variantDef] of Object.entries(config.variants)) {
525
- // Skip styling for variants that are overridden by child's computedVariants
526
- if (skipStyleKeys.has(variantName)) continue;
527
-
528
- const selectedValue = resolvedVariants[variantName];
529
- if (selectedValue === undefined) continue;
530
- const selectedKey = getVariantValueKey(selectedValue);
531
- if (selectedKey && skipVariantValues[variantName]?.has(selectedKey)) {
532
- continue;
533
- }
534
-
535
- const result = getVariantResult(variantDef, selectedValue);
536
- classes.push(result.class);
537
- assign(style, result.style);
538
- }
539
- }
540
-
541
- // Process computedVariants
542
- if (config.computedVariants) {
543
- for (const [variantName, computeFn] of Object.entries(
544
- config.computedVariants,
545
- )) {
546
- // Skip styling for variants that are overridden by child's computedVariants
547
- if (skipStyleKeys.has(variantName)) continue;
548
-
549
- const selectedValue = resolvedVariants[variantName];
550
- if (selectedValue === undefined) continue;
551
- const selectedKey = getVariantValueKey(selectedValue);
552
- if (selectedKey && skipVariantValues[variantName]?.has(selectedKey)) {
553
- continue;
554
- }
555
-
556
- const computedResult = computeFn(selectedValue);
557
- const result = extractClassAndStyle(computedResult);
558
- classes.push(result.class);
559
- assign(style, result.style);
560
- }
561
- }
562
-
563
- return { classes, style };
564
- }
565
-
566
- /**
567
- * Runs the computed function if present, returning classes, styles, and updated
568
- * variants.
569
- */
570
- function runComputedFunction(
571
- config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
572
- resolvedVariants: Record<string, unknown>,
573
- propsVariants: Record<string, unknown>,
574
- ): {
575
- classes: ClassValue[];
576
- style: StyleValue;
577
- updatedVariants: Record<string, unknown>;
578
- } {
579
- const classes: ClassValue[] = [];
580
- const style: StyleValue = {};
581
- const updatedVariants = { ...resolvedVariants };
582
-
583
- if (!config.computed) {
584
- return { classes, style, updatedVariants };
585
- }
586
-
587
- const context = {
588
- variants: resolvedVariants,
589
- setVariants: (newVariants: VariantValues<Record<string, unknown>>) => {
590
- Object.assign(
591
- updatedVariants,
592
- filterDisabledVariants(config, newVariants),
593
- );
594
- },
595
- setDefaultVariants: (
596
- newDefaults: VariantValues<Record<string, unknown>>,
597
- ) => {
598
- // Only apply defaults for variants not explicitly set in props
599
- for (const [key, value] of Object.entries(newDefaults)) {
600
- if (propsVariants[key] === undefined) {
601
- if (isVariantDisabled(config, key)) continue;
602
- if (isVariantValueDisabled(config, key, value)) continue;
603
- updatedVariants[key] = value;
604
- }
605
- }
606
- },
607
- addClass: (className: ClassValue) => {
608
- classes.push(className);
609
- },
610
- addStyle: (newStyle: StyleValue) => {
611
- assign(style, newStyle);
612
- },
613
- };
614
-
615
- const computedResult = config.computed(context);
616
- if (computedResult != null) {
617
- const result = extractClassAndStyle(computedResult);
618
- classes.push(result.class);
619
- assign(style, result.style);
620
- }
621
-
622
- return {
623
- classes,
624
- style,
625
- updatedVariants: filterDisabledVariants(config, updatedVariants),
626
- };
627
- }
628
-
629
305
  interface NormalizedSource {
630
306
  keys: string[];
631
307
  variantKeys: string[];
632
- defaults: Record<string, unknown>;
633
308
  isComponent: boolean;
634
309
  }
635
310
 
636
311
  const EMPTY_SOURCE: NormalizedSource = {
637
312
  keys: [],
638
313
  variantKeys: [],
639
- defaults: {},
640
314
  isComponent: false,
641
315
  };
642
316
 
643
- /**
644
- * Normalizes a key source (array or component) to an object with keys,
645
- * variantKeys, defaults, and isComponent flag.
646
- */
647
317
  function normalizeKeySource(source: unknown): NormalizedSource {
648
318
  if (Array.isArray(source)) {
649
319
  return {
650
320
  keys: source as string[],
651
321
  variantKeys: source as string[],
652
- defaults: {},
653
322
  isComponent: false,
654
323
  };
655
324
  }
@@ -661,16 +330,14 @@ function normalizeKeySource(source: unknown): NormalizedSource {
661
330
  if (!("keys" in source)) return EMPTY_SOURCE;
662
331
  if (!("variantKeys" in source)) return EMPTY_SOURCE;
663
332
 
664
- // Source is a component with keys and variantKeys properties
333
+ // Component-provided arrays are immutable metadata reference directly.
665
334
  const typed = source as {
666
335
  keys: string[];
667
336
  variantKeys: string[];
668
- getVariants?: () => Record<string, unknown>;
669
337
  };
670
338
  return {
671
- keys: [...typed.keys],
672
- variantKeys: [...typed.variantKeys],
673
- defaults: typed.getVariants?.() ?? {},
339
+ keys: typed.keys,
340
+ variantKeys: typed.variantKeys,
674
341
  isComponent: true,
675
342
  };
676
343
  }
@@ -687,51 +354,63 @@ function splitPropsImpl(
687
354
  props: Record<string, unknown>,
688
355
  sources: NormalizedSource[],
689
356
  ): Record<string, unknown>[] {
690
- const allUsedKeys = new Set<string>(selfKeys);
357
+ const sourcesLength = sources.length;
691
358
  const results: Record<string, unknown>[] = [];
692
-
693
- // Track if styling has been claimed by a component
694
359
  let stylingClaimed = selfIsComponent;
695
360
 
696
- // Self result
697
361
  const selfResult: Record<string, unknown> = {};
698
- for (const key of selfKeys) {
699
- if (key in props) {
362
+ const selfKeysLength = selfKeys.length;
363
+ for (let i = 0; i < selfKeysLength; i++) {
364
+ const key = selfKeys[i];
365
+ if (key !== undefined && key in props) {
700
366
  selfResult[key] = props[key];
701
367
  }
702
368
  }
703
369
  results.push(selfResult);
704
370
 
705
- // Process each source
706
- for (const source of sources) {
371
+ // Track effective key arrays for the rest computation — for typical inputs
372
+ // a linear scan beats building a Set up-front.
373
+ const effectiveKeyArrays: string[][] = [selfKeys];
374
+
375
+ for (let s = 0; s < sourcesLength; s++) {
376
+ const source = sources[s];
377
+ if (source === undefined) continue;
707
378
  const sourceResult: Record<string, unknown> = {};
708
379
 
709
- // Determine which keys this source should use
710
- // Components use variantKeys if styling has already been claimed
711
- // Arrays always use their listed keys
712
380
  const effectiveKeys =
713
381
  source.isComponent && stylingClaimed ? source.variantKeys : source.keys;
714
382
 
715
- for (const key of effectiveKeys) {
716
- allUsedKeys.add(key);
717
- if (key in props) {
383
+ const effectiveKeysLength = effectiveKeys.length;
384
+ for (let i = 0; i < effectiveKeysLength; i++) {
385
+ const key = effectiveKeys[i];
386
+ if (key !== undefined && key in props) {
718
387
  sourceResult[key] = props[key];
719
388
  }
720
389
  }
721
390
  results.push(sourceResult);
391
+ effectiveKeyArrays.push(effectiveKeys);
722
392
 
723
- // If this is a component that hasn't claimed styling yet, mark styling as claimed
724
393
  if (source.isComponent && !stylingClaimed) {
725
394
  stylingClaimed = true;
726
395
  }
727
396
  }
728
397
 
729
- // Rest - keys not used by anyone
730
398
  const rest: Record<string, unknown> = {};
731
- for (const [key, value] of Object.entries(props)) {
732
- if (!allUsedKeys.has(key)) {
733
- rest[key] = value;
399
+ const propKeys = Object.keys(props);
400
+ const propKeysLength = propKeys.length;
401
+ const groupCount = sourcesLength + 1;
402
+ outer: for (let i = 0; i < propKeysLength; i++) {
403
+ const key = propKeys[i];
404
+ if (key === undefined) continue;
405
+ for (let g = 0; g < groupCount; g++) {
406
+ const arr = effectiveKeyArrays[g];
407
+ if (arr === undefined) continue;
408
+ const arrLength = arr.length;
409
+ for (let j = 0; j < arrLength; j++) {
410
+ if (arr[j] === key) continue outer;
411
+ }
734
412
  }
413
+ rest[key] = props[key];
735
414
  }
736
415
  results.push(rest);
737
416
 
@@ -745,16 +424,6 @@ function splitPropsImpl(
745
424
  * only receive variant props. Arrays receive their listed keys but don't claim
746
425
  * styling props. The last element is always the "rest" containing keys not
747
426
  * claimed by any source.
748
- * @example
749
- * ```ts
750
- * const [buttonProps, inputProps, rest] = splitProps(
751
- * props,
752
- * buttonComponent,
753
- * inputComponent,
754
- * );
755
- * // buttonProps has class/style + button variants
756
- * // inputProps has only input variants (no class/style)
757
- * ```
758
427
  */
759
428
  export const splitProps: SplitPropsFunction = ((
760
429
  props: Record<string, unknown>,
@@ -762,7 +431,11 @@ export const splitProps: SplitPropsFunction = ((
762
431
  ...sources: unknown[]
763
432
  ) => {
764
433
  const normalizedSource1 = normalizeKeySource(source1);
765
- const normalizedSources = sources.map(normalizeKeySource);
434
+ const sourcesLength = sources.length;
435
+ const normalizedSources: NormalizedSource[] = [];
436
+ for (let i = 0; i < sourcesLength; i++) {
437
+ normalizedSources.push(normalizeKeySource(sources[i]));
438
+ }
766
439
  return splitPropsImpl(
767
440
  normalizedSource1.keys,
768
441
  normalizedSource1.isComponent,
@@ -771,6 +444,48 @@ export const splitProps: SplitPropsFunction = ((
771
444
  );
772
445
  }) as SplitPropsFunction;
773
446
 
447
+ /**
448
+ * A pre-built variant. Maps variant value keys (or the literal "true"/"false"
449
+ * strings for boolean variants) to PrebuiltValue. Includes a "shorthand"
450
+ * fallback when the variant is a single class value (treated as `{ true: ... }`).
451
+ */
452
+ interface PrebuiltVariant {
453
+ // For object variants: map of value -> prebuilt class/style
454
+ values: Record<string, PrebuiltValue> | null;
455
+ // For shorthand variants: the value to use when selectedValue is true
456
+ shorthand: PrebuiltValue | null;
457
+ // Set of value keys that are disabled (value === null in the original variant
458
+ // definition)
459
+ disabledValues: Set<string> | null;
460
+ }
461
+
462
+ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
463
+ if (!isRecordObject(variantDef)) {
464
+ return {
465
+ values: null,
466
+ shorthand: extractClassAndStylePrebuilt(variantDef),
467
+ disabledValues: null,
468
+ };
469
+ }
470
+ const values: Record<string, PrebuiltValue> = {};
471
+ let disabledValues: Set<string> | null = null;
472
+ for (const key in variantDef) {
473
+ if (!hasOwn.call(variantDef, key)) continue;
474
+ const value = variantDef[key];
475
+ if (value === null) {
476
+ if (!disabledValues) disabledValues = new Set<string>();
477
+ disabledValues.add(key);
478
+ continue;
479
+ }
480
+ values[key] = extractClassAndStylePrebuilt(value);
481
+ }
482
+ return {
483
+ values,
484
+ shorthand: null,
485
+ disabledValues,
486
+ };
487
+ }
488
+
774
489
  /**
775
490
  * Creates the resolveDefaults function for a component. This function returns
776
491
  * only the variants set via setDefaultVariants in the computed function. Used
@@ -778,57 +493,61 @@ export const splitProps: SplitPropsFunction = ((
778
493
  */
779
494
  function createResolveDefaults(
780
495
  config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
496
+ staticDefaults: Record<string, unknown>,
781
497
  ): ComponentMeta["resolveDefaults"] {
498
+ const computed = config.computed;
499
+ const extend = config.extend;
782
500
  return (childDefaults, userProps = {}) => {
783
- // Get static defaults (including from extended components)
784
- const staticDefaults = collectStaticDefaults(config);
785
-
786
501
  // Merge: parent static < child static < user props
787
502
  // This is what parent's computed will see in `variants`
788
- const resolvedVariants = {
789
- ...staticDefaults,
790
- ...filterUndefined(childDefaults),
791
- ...filterUndefined(userProps),
792
- };
503
+ const resolvedVariants: Record<string, unknown> = {};
504
+ Object.assign(resolvedVariants, staticDefaults);
505
+ for (const key in childDefaults) {
506
+ if (!hasOwn.call(childDefaults, key)) continue;
507
+ const v = childDefaults[key];
508
+ if (v === undefined) continue;
509
+ resolvedVariants[key] = v;
510
+ }
511
+ for (const key in userProps) {
512
+ if (!hasOwn.call(userProps, key)) continue;
513
+ const v = userProps[key];
514
+ if (v === undefined) continue;
515
+ resolvedVariants[key] = v;
516
+ }
793
517
 
794
518
  // Track which keys are set via setDefaultVariants
795
519
  const computedDefaults: Record<string, unknown> = {};
796
520
 
797
521
  // Propagate to extended components so their computed functions can run
798
- // This allows grandparent computed functions to see grandchild defaults
799
- if (config.extend) {
800
- for (const ext of config.extend) {
522
+ if (extend) {
523
+ for (const ext of extend) {
801
524
  const meta = getComponentMeta(ext);
802
525
  if (!meta) continue;
803
- Object.assign(
804
- computedDefaults,
805
- meta.resolveDefaults(childDefaults, userProps),
806
- );
526
+ const extDefaults = meta.resolveDefaults(childDefaults, userProps);
527
+ for (const k in extDefaults) {
528
+ if (hasOwn.call(extDefaults, k)) {
529
+ computedDefaults[k] = extDefaults[k];
530
+ }
531
+ }
807
532
  }
808
533
  }
809
534
 
810
- if (config.computed) {
811
- config.computed({
535
+ if (computed) {
536
+ computed({
812
537
  variants: resolvedVariants as VariantValues<Record<string, unknown>>,
813
- setVariants: () => {
814
- // Not relevant for collecting defaults
815
- },
538
+ setVariants: () => {},
816
539
  setDefaultVariants: (newDefaults) => {
817
- // Only apply defaults for variants not explicitly set by user
818
- // (child's static defaults should not block setDefaultVariants)
819
- for (const [key, value] of Object.entries(newDefaults)) {
540
+ for (const key in newDefaults) {
541
+ if (!hasOwn.call(newDefaults, key)) continue;
542
+ const value = (newDefaults as Record<string, unknown>)[key];
820
543
  if (userProps[key] !== undefined) continue;
821
544
  if (isVariantDisabled(config, key)) continue;
822
545
  if (isVariantValueDisabled(config, key, value)) continue;
823
546
  computedDefaults[key] = value;
824
547
  }
825
548
  },
826
- addClass: () => {
827
- // Not relevant for collecting defaults
828
- },
829
- addStyle: () => {
830
- // Not relevant for collecting defaults
831
- },
549
+ addClass: () => {},
550
+ addStyle: () => {},
832
551
  });
833
552
  }
834
553
 
@@ -853,115 +572,502 @@ export function create({
853
572
  ): CVComponent<V, CV, E> => {
854
573
  type MergedVariants = MergeVariants<V, CV, E>;
855
574
 
575
+ // ----- Pre-computed at creation time -----
856
576
  const variantKeys = collectVariantKeys(config);
857
577
  const disabledVariantKeys = collectDisabledVariantKeys(config);
858
578
  const disabledVariantValues = collectDisabledVariantValues(config);
579
+ const hasDisabledVariantKeys = disabledVariantKeys.size > 0;
580
+ const hasDisabledVariantValues =
581
+ Object.keys(disabledVariantValues).length > 0;
582
+ const hasAnyDisabled = hasDisabledVariantKeys || hasDisabledVariantValues;
583
+
859
584
  const inputPropsKeys = ["class", "className", "style", ...variantKeys];
860
585
 
861
- const getPropsKeys = (mode: Mode) => [
862
- getClassPropertyName(mode),
863
- "style",
864
- ...variantKeys,
865
- ];
586
+ const extend = config.extend;
587
+ const hasExtend = !!extend && extend.length > 0;
588
+ const variants = config.variants;
589
+ const computedVariantsCfg = config.computedVariants;
590
+ const computed = config.computed;
591
+ const baseStyle = config.style;
592
+ const baseClass: ClassValue =
593
+ config.class === undefined ? null : (config.class as ClassValue);
594
+
595
+ // Pre-build variant entries for fast iteration. For each variant key in
596
+ // `variants`, we have a name and a PrebuiltVariant with normalized values.
597
+ const variantEntryNames: string[] = [];
598
+ const variantEntryDefs: PrebuiltVariant[] = [];
599
+ if (variants) {
600
+ for (const name in variants) {
601
+ if (!hasOwn.call(variants, name)) continue;
602
+ const variant = (variants as Record<string, unknown>)[name];
603
+ if (variant === null) continue;
604
+ variantEntryNames.push(name);
605
+ variantEntryDefs.push(buildPrebuiltVariant(variant));
606
+ }
607
+ }
608
+ const variantEntryCount = variantEntryNames.length;
609
+
610
+ // Pre-built computed-variants entries.
611
+ const computedVariantNames: string[] = [];
612
+ const computedVariantFns: Array<(value: unknown) => unknown> = [];
613
+ if (computedVariantsCfg) {
614
+ for (const name in computedVariantsCfg) {
615
+ if (!hasOwn.call(computedVariantsCfg, name)) continue;
616
+ computedVariantNames.push(name);
617
+ computedVariantFns.push(
618
+ (computedVariantsCfg as Record<string, (value: unknown) => unknown>)[
619
+ name
620
+ ] as (value: unknown) => unknown,
621
+ );
622
+ }
623
+ }
624
+ const computedVariantCount = computedVariantNames.length;
625
+
626
+ // Pre-compute static defaults. Includes:
627
+ // - extended components' static defaults
628
+ // - implicit boolean defaults (variants with a `false` key default to false)
629
+ // - this config's defaultVariants (overriding the above)
630
+ // Then filtered through disabled-variants.
631
+ const staticDefaults: Record<string, unknown> = {};
632
+ if (extend) {
633
+ for (const ext of extend) {
634
+ const meta = getComponentMeta(ext);
635
+ if (meta) Object.assign(staticDefaults, meta.staticDefaults);
636
+ }
637
+ }
638
+ if (variants) {
639
+ for (const name in variants) {
640
+ if (!hasOwn.call(variants, name)) continue;
641
+ const variantDef = (variants as Record<string, unknown>)[name];
642
+ if (!isRecordObject(variantDef)) continue;
643
+ if (
644
+ hasOwn.call(variantDef, "false") &&
645
+ staticDefaults[name] === undefined
646
+ ) {
647
+ staticDefaults[name] = false;
648
+ }
649
+ }
650
+ }
651
+ if (config.defaultVariants) {
652
+ Object.assign(staticDefaults, config.defaultVariants);
653
+ }
654
+ if (hasAnyDisabled) {
655
+ // Filter disabled variants in-place
656
+ for (const key in staticDefaults) {
657
+ if (!hasOwn.call(staticDefaults, key)) continue;
658
+ if (disabledVariantKeys.has(key)) {
659
+ delete staticDefaults[key];
660
+ continue;
661
+ }
662
+ if (hasDisabledVariantValues) {
663
+ const value = staticDefaults[key];
664
+ const valueKey = getVariantValueKey(value);
665
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
666
+ delete staticDefaults[key];
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ // Pre-build extended component info, so we don't have to call
673
+ // `getComponentMeta` per render.
674
+ const extEntries: AnyComponent[] = extend ? (extend as AnyComponent[]) : [];
675
+ const extBaseClassesArr: string[] = [];
676
+ const extMetas: (ComponentMeta | undefined)[] = [];
677
+ if (extend) {
678
+ for (const ext of extend) {
679
+ const meta = getComponentMeta(ext);
680
+ extMetas.push(meta);
681
+ extBaseClassesArr.push(meta?.baseClass ?? "");
682
+ }
683
+ }
684
+ const extCount = extEntries.length;
685
+
686
+ // Inlined "filter disabled" - mutates `out` adding only allowed entries.
687
+ // Most components have no disabled variants, in which case we can skip
688
+ // the filter entirely.
689
+ function filterDisabledInto(
690
+ input: Record<string, unknown>,
691
+ out: Record<string, unknown>,
692
+ ): void {
693
+ if (!hasAnyDisabled) {
694
+ for (const key in input) {
695
+ if (hasOwn.call(input, key)) out[key] = input[key];
696
+ }
697
+ return;
698
+ }
699
+ for (const key in input) {
700
+ if (!hasOwn.call(input, key)) continue;
701
+ if (disabledVariantKeys.has(key)) continue;
702
+ const value = input[key];
703
+ if (hasDisabledVariantValues) {
704
+ const valueKey = getVariantValueKey(value);
705
+ if (valueKey != null && disabledVariantValues[key]?.has(valueKey)) {
706
+ continue;
707
+ }
708
+ }
709
+ out[key] = value;
710
+ }
711
+ }
866
712
 
713
+ // Pre-create default-variants resolver which is referenced during the hot
714
+ // path through extended components' meta. The closure captures
715
+ // staticDefaults, extend, computed, etc.
716
+ const resolveDefaultsFn = createResolveDefaults(config, staticDefaults);
717
+
718
+ // Resolve variants: defaults -> computed defaults from extended -> props.
719
+ function resolveVariantsHot(
720
+ propsVariants: Record<string, unknown>,
721
+ ): Record<string, unknown> {
722
+ // Start with static defaults
723
+ const defaults: Record<string, unknown> = {};
724
+ Object.assign(defaults, staticDefaults);
725
+
726
+ // Apply computed defaults from extended components
727
+ if (hasExtend) {
728
+ for (let i = 0; i < extCount; i++) {
729
+ const meta = extMetas[i];
730
+ if (!meta) continue;
731
+ const extComputed = meta.resolveDefaults(defaults, propsVariants);
732
+ for (const k in extComputed) {
733
+ if (hasOwn.call(extComputed, k)) {
734
+ defaults[k] = extComputed[k];
735
+ }
736
+ }
737
+ }
738
+ }
739
+
740
+ // Now merge: defaults < propsVariants (filter undefined)
741
+ // Apply propsVariants on top
742
+ for (const k in propsVariants) {
743
+ if (!hasOwn.call(propsVariants, k)) continue;
744
+ const v = propsVariants[k];
745
+ if (v === undefined) continue;
746
+ defaults[k] = v;
747
+ }
748
+
749
+ // Filter disabled
750
+ const result: Record<string, unknown> = {};
751
+ filterDisabledInto(defaults, result);
752
+ return result;
753
+ }
754
+
755
+ // Hot path: build a fresh result.
867
756
  const computeResult = (
868
757
  props: ComponentProps<MergedVariants> = {},
869
758
  ): { className: string; style: StyleValue } => {
870
- const allClasses: ClassValue[] = [];
871
- const allStyle: StyleValue = {};
872
-
873
759
  // Extract skip style keys from props (set by child's computedVariants)
874
- const skipStyleKeys =
875
- ((props as Record<symbol, unknown>)[SKIP_STYLE_KEYS] as
876
- | Set<string>
877
- | undefined) ?? new Set<string>();
878
- const skipStyleVariantValues =
879
- ((props as Record<symbol, unknown>)[SKIP_STYLE_VARIANT_VALUES] as
880
- | Record<string, Set<string>>
881
- | undefined) ?? {};
882
-
883
- // Extract variant props from input
760
+ const skipStyleKeysIn = (props as Record<symbol, unknown>)[
761
+ SKIP_STYLE_KEYS
762
+ ] as Set<string> | undefined;
763
+ const skipStyleVariantValuesIn = (props as Record<symbol, unknown>)[
764
+ SKIP_STYLE_VARIANT_VALUES
765
+ ] as Record<string, Set<string>> | undefined;
766
+
767
+ // Extract variant props from input. Also remember the propsVariants for
768
+ // computed-defaults application.
884
769
  const variantProps: Record<string, unknown> = {};
885
- for (const key of variantKeys) {
770
+ for (let i = 0; i < variantKeys.length; i++) {
771
+ const key = variantKeys[i];
886
772
  if (key in props) {
887
- variantProps[key] = props[key];
773
+ variantProps[key] = (props as Record<string, unknown>)[key];
888
774
  }
889
775
  }
776
+
890
777
  // Resolve variants with defaults
891
- let resolvedVariants = resolveVariants(config, variantProps);
892
- // Process computed first to potentially update variants
893
- const computedResult = runComputedFunction(
894
- config,
895
- resolvedVariants,
896
- variantProps,
897
- );
898
- resolvedVariants = computedResult.updatedVariants;
899
-
900
- // Collect computedVariants keys that will override extended variants.
901
- // Combine with incoming skip keys to propagate through the extend chain.
902
- const currentVariantKeys = new Set<string>(skipStyleKeys);
903
- for (const key of disabledVariantKeys) {
904
- currentVariantKeys.add(key);
778
+ let resolvedVariants = resolveVariantsHot(variantProps);
779
+
780
+ // Run computed function (may update variants and emit class/style)
781
+ let computedClassesArr: ClassValue[] | null = null;
782
+ let computedStyleObj: StyleValue | null = null;
783
+
784
+ if (computed) {
785
+ const updatedVariants: Record<string, unknown> = {};
786
+ Object.assign(updatedVariants, resolvedVariants);
787
+ const cClasses: ClassValue[] = [];
788
+ let cStyle: StyleValue | null = null;
789
+ const ctx = {
790
+ variants: resolvedVariants as VariantValues<Record<string, unknown>>,
791
+ setVariants: (
792
+ newVariants: VariantValues<Record<string, unknown>>,
793
+ ) => {
794
+ const filtered: Record<string, unknown> = {};
795
+ filterDisabledInto(
796
+ newVariants as Record<string, unknown>,
797
+ filtered,
798
+ );
799
+ Object.assign(updatedVariants, filtered);
800
+ },
801
+ setDefaultVariants: (
802
+ newDefaults: VariantValues<Record<string, unknown>>,
803
+ ) => {
804
+ for (const key in newDefaults) {
805
+ if (!hasOwn.call(newDefaults, key)) continue;
806
+ if (variantProps[key] !== undefined) continue;
807
+ if (disabledVariantKeys.has(key)) continue;
808
+ const value = (newDefaults as Record<string, unknown>)[key];
809
+ const valueKey = getVariantValueKey(value);
810
+ if (
811
+ valueKey != null &&
812
+ disabledVariantValues[key]?.has(valueKey)
813
+ ) {
814
+ continue;
815
+ }
816
+ updatedVariants[key] = value;
817
+ }
818
+ },
819
+ addClass: (className: ClassValue) => {
820
+ cClasses.push(className);
821
+ },
822
+ addStyle: (newStyle: StyleValue) => {
823
+ if (!cStyle) cStyle = {};
824
+ assign(cStyle, newStyle);
825
+ },
826
+ };
827
+ const result = computed(ctx);
828
+ if (result != null) {
829
+ const r = extractClassAndStylePrebuilt(result);
830
+ if (r.class != null) cClasses.push(r.class);
831
+ if (r.style) {
832
+ if (!cStyle) cStyle = {};
833
+ assign(cStyle, r.style);
834
+ }
835
+ }
836
+ // Apply filterDisabled to updatedVariants
837
+ const filteredUpdated: Record<string, unknown> = {};
838
+ filterDisabledInto(updatedVariants, filteredUpdated);
839
+ resolvedVariants = filteredUpdated;
840
+ computedClassesArr = cClasses;
841
+ computedStyleObj = cStyle;
905
842
  }
906
- const computedVariantKeys = new Set<string>(currentVariantKeys);
907
- if (config.computedVariants) {
908
- for (const key of Object.keys(config.computedVariants)) {
909
- computedVariantKeys.add(key);
843
+
844
+ // Compute skip-style sets for the extended components and current
845
+ // component. Only allocate when needed.
846
+ const hasSkipKeys = !!skipStyleKeysIn || disabledVariantKeys.size > 0;
847
+ let currentVariantKeys: Set<string> | null = null;
848
+ if (hasSkipKeys) {
849
+ currentVariantKeys = new Set<string>();
850
+ if (skipStyleKeysIn) {
851
+ for (const k of skipStyleKeysIn) currentVariantKeys.add(k);
910
852
  }
853
+ for (const k of disabledVariantKeys) currentVariantKeys.add(k);
911
854
  }
912
- const computedVariantValues = mergeDisabledVariantValues(
913
- skipStyleVariantValues,
914
- disabledVariantValues,
915
- );
916
-
917
- // Process extended components (separates base and variant classes)
918
- const extendedResult = computeExtendedStyles(
919
- config,
920
- resolvedVariants,
921
- computedVariantKeys,
922
- computedVariantValues,
923
- );
924
-
925
- // 1. Extended base classes first
926
- allClasses.push(...extendedResult.baseClasses);
927
- assign(allStyle, extendedResult.style);
928
-
929
- // 2. Current component's base class
930
- allClasses.push(config.class);
931
-
932
- // 3. Add base style
933
- if (config.style) {
934
- assign(allStyle, config.style);
855
+ // computedVariantKeys is currentVariantKeys + computedVariants names
856
+ let computedVariantKeysSet: Set<string> | null = null;
857
+ if (hasExtend) {
858
+ if (currentVariantKeys || computedVariantNames.length > 0) {
859
+ computedVariantKeysSet = new Set<string>();
860
+ if (currentVariantKeys) {
861
+ for (const k of currentVariantKeys) computedVariantKeysSet.add(k);
862
+ }
863
+ for (let i = 0; i < computedVariantNames.length; i++) {
864
+ computedVariantKeysSet.add(computedVariantNames[i]);
865
+ }
866
+ }
935
867
  }
936
868
 
937
- // 4. Extended variant classes
938
- allClasses.push(...extendedResult.variantClasses);
939
-
940
- // 5. Current component's variants (skip keys that are overridden)
941
- const variantsResult = computeVariantStyles(
942
- config,
943
- resolvedVariants,
944
- currentVariantKeys,
945
- computedVariantValues,
946
- );
947
- allClasses.push(...variantsResult.classes);
948
- assign(allStyle, variantsResult.style);
949
-
950
- // Add computed results
951
- allClasses.push(...computedResult.classes);
952
- assign(allStyle, computedResult.style);
953
-
954
- // Merge class from props
955
- if ("class" in props) {
956
- allClasses.push(props.class);
869
+ // computedVariantValues = mergeDisabledVariantValues(skipIn, disabledValues)
870
+ let computedVariantValues: Record<string, Set<string>> | null = null;
871
+ const hasInValues = !!skipStyleVariantValuesIn;
872
+ const disabledValuesKeys = Object.keys(disabledVariantValues);
873
+ const hasDisabledValues = disabledValuesKeys.length > 0;
874
+ if (hasExtend && (hasInValues || hasDisabledValues)) {
875
+ computedVariantValues = {};
876
+ if (hasInValues) {
877
+ for (const k in skipStyleVariantValuesIn) {
878
+ if (!hasOwn.call(skipStyleVariantValuesIn, k)) continue;
879
+ const set = new Set<string>();
880
+ for (const v of skipStyleVariantValuesIn[k]) {
881
+ set.add(v);
882
+ }
883
+ computedVariantValues[k] = set;
884
+ }
885
+ }
886
+ for (let i = 0; i < disabledValuesKeys.length; i++) {
887
+ const k = disabledValuesKeys[i];
888
+ let bucket = computedVariantValues[k];
889
+ if (!bucket) {
890
+ bucket = new Set<string>();
891
+ computedVariantValues[k] = bucket;
892
+ }
893
+ for (const v of disabledVariantValues[k]) bucket.add(v);
894
+ }
957
895
  }
958
- if ("className" in props) {
959
- allClasses.push(props.className);
896
+
897
+ // ----- Build classes/styles in proper order -----
898
+ // 1. Extended base classes & their styles (with skip applied)
899
+ // 2. Current base class & base style
900
+ // 3. Extended variant classes
901
+ // 4. Current variants
902
+ // 5. computed results
903
+ // 6. props.class / props.className
904
+ // 7. props.style
905
+ const allClasses: ClassValue[] = [];
906
+ const allStyle: StyleValue = {};
907
+
908
+ // Process extended components
909
+ if (hasExtend) {
910
+ const hasComputedVariantKeysSet =
911
+ !!computedVariantKeysSet && computedVariantKeysSet.size > 0;
912
+ const hasComputedVariantValues =
913
+ !!computedVariantValues &&
914
+ Object.keys(computedVariantValues).length > 0;
915
+ const hasSkipForExt =
916
+ hasComputedVariantKeysSet || hasComputedVariantValues;
917
+
918
+ const extVariantClasses: ClassValue[] = [];
919
+
920
+ for (let i = 0; i < extCount; i++) {
921
+ const ext = extEntries[i];
922
+ const extBaseClass = extBaseClassesArr[i];
923
+ let propsForExt: Record<string | symbol, unknown>;
924
+ if (hasSkipForExt) {
925
+ propsForExt = {};
926
+ // Copy resolvedVariants
927
+ for (const k in resolvedVariants) {
928
+ if (hasOwn.call(resolvedVariants, k)) {
929
+ propsForExt[k] = resolvedVariants[k];
930
+ }
931
+ }
932
+ if (hasComputedVariantKeysSet) {
933
+ propsForExt[SKIP_STYLE_KEYS] = computedVariantKeysSet;
934
+ }
935
+ if (hasComputedVariantValues) {
936
+ propsForExt[SKIP_STYLE_VARIANT_VALUES] = computedVariantValues;
937
+ }
938
+ } else {
939
+ propsForExt = resolvedVariants as Record<string | symbol, unknown>;
940
+ }
941
+
942
+ const extResult = ext(
943
+ propsForExt as ComponentProps<Record<string, unknown>>,
944
+ );
945
+ // ext may be a modal component (.html / .htmlObj), whose style is a
946
+ // CSS string or hyphen-keyed object — normalize before merging.
947
+ if (extResult.style != null) {
948
+ assign(allStyle, normalizeStyle(extResult.style));
949
+ }
950
+
951
+ allClasses.push(extBaseClass);
952
+ const fullClass =
953
+ "className" in extResult ? extResult.className : extResult.class;
954
+ const variantPortion = extractVariantClasses(fullClass, extBaseClass);
955
+ if (variantPortion) extVariantClasses.push(variantPortion);
956
+ }
957
+
958
+ // 2. Current base class
959
+ allClasses.push(baseClass);
960
+ if (baseStyle) assign(allStyle, baseStyle);
961
+
962
+ // 4. Extended variant classes
963
+ for (let i = 0; i < extVariantClasses.length; i++) {
964
+ allClasses.push(extVariantClasses[i]);
965
+ }
966
+ } else {
967
+ // No extends: just current base
968
+ allClasses.push(baseClass);
969
+ if (baseStyle) assign(allStyle, baseStyle);
960
970
  }
961
971
 
962
- // Merge style from props
963
- if (props.style != null) {
964
- assign(allStyle, normalizeStyle(props.style));
972
+ // 5. Current component's variants (skip keys overridden)
973
+ // Walk pre-built variant entries
974
+ for (let i = 0; i < variantEntryCount; i++) {
975
+ const variantName = variantEntryNames[i];
976
+ const variant = variantEntryDefs[i];
977
+ if (currentVariantKeys && currentVariantKeys.has(variantName)) continue;
978
+ const selectedValue = resolvedVariants[variantName];
979
+ if (selectedValue === undefined) continue;
980
+ const selectedKey = getVariantValueKey(selectedValue);
981
+ // disabled values from current config:
982
+ if (
983
+ variant.disabledValues &&
984
+ selectedKey != null &&
985
+ variant.disabledValues.has(selectedKey)
986
+ ) {
987
+ continue;
988
+ }
989
+ // skipVariantValues comes from skipStyleVariantValuesIn (only relevant
990
+ // if this is being called as an extended component). For top-level it
991
+ // would be undefined.
992
+ if (
993
+ skipStyleVariantValuesIn &&
994
+ selectedKey != null &&
995
+ skipStyleVariantValuesIn[variantName]?.has(selectedKey)
996
+ ) {
997
+ continue;
998
+ }
999
+
1000
+ if (variant.values) {
1001
+ if (selectedKey == null) continue;
1002
+ const v = variant.values[selectedKey];
1003
+ if (!v) continue;
1004
+ if (v.class != null) allClasses.push(v.class);
1005
+ if (v.style) assign(allStyle, v.style);
1006
+ } else if (variant.shorthand) {
1007
+ // shorthand: applies when selectedValue === true
1008
+ if (selectedValue === true) {
1009
+ const v = variant.shorthand;
1010
+ if (v.class != null) allClasses.push(v.class);
1011
+ if (v.style) assign(allStyle, v.style);
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ // computedVariants
1017
+ for (let i = 0; i < computedVariantCount; i++) {
1018
+ const variantName = computedVariantNames[i];
1019
+ const fn = computedVariantFns[i];
1020
+ if (currentVariantKeys && currentVariantKeys.has(variantName)) continue;
1021
+ const selectedValue = resolvedVariants[variantName];
1022
+ if (selectedValue === undefined) continue;
1023
+ const selectedKey = getVariantValueKey(selectedValue);
1024
+ if (
1025
+ skipStyleVariantValuesIn &&
1026
+ selectedKey != null &&
1027
+ skipStyleVariantValuesIn[variantName]?.has(selectedKey)
1028
+ ) {
1029
+ continue;
1030
+ }
1031
+ const computedResult = fn(selectedValue);
1032
+ if (computedResult == null) continue;
1033
+ const r = extractClassAndStylePrebuilt(computedResult);
1034
+ if (r.class != null) allClasses.push(r.class);
1035
+ if (r.style) assign(allStyle, r.style);
1036
+ }
1037
+
1038
+ // computed function results
1039
+ if (computedClassesArr) {
1040
+ for (let i = 0; i < computedClassesArr.length; i++) {
1041
+ allClasses.push(computedClassesArr[i]);
1042
+ }
1043
+ }
1044
+ if (computedStyleObj) assign(allStyle, computedStyleObj);
1045
+
1046
+ // props.class / props.className
1047
+ if ("class" in props)
1048
+ allClasses.push((props as { class: ClassValue }).class);
1049
+ if ("className" in props)
1050
+ allClasses.push((props as { className: ClassValue }).className);
1051
+
1052
+ // props.style
1053
+ const psv = (props as { style?: unknown }).style;
1054
+ if (psv != null) {
1055
+ // Fast path: if it's an object with no keys, skip
1056
+ if (typeof psv === "string") {
1057
+ if (psv.length > 0) {
1058
+ assign(allStyle, htmlStyleToStyleValue(psv));
1059
+ }
1060
+ } else if (typeof psv === "object") {
1061
+ // Could be HTMLObj or JSX form. Don't allocate when empty.
1062
+ let hasAnyKey = false;
1063
+ for (const _ in psv) {
1064
+ hasAnyKey = true;
1065
+ break;
1066
+ }
1067
+ if (hasAnyKey) {
1068
+ assign(allStyle, normalizeStyle(psv));
1069
+ }
1070
+ }
965
1071
  }
966
1072
 
967
1073
  return {
@@ -971,119 +1077,137 @@ export function create({
971
1077
  };
972
1078
 
973
1079
  const getVariants = (variants?: VariantValues<MergedVariants>) => {
974
- const variantProps = variants ?? {};
975
- const resolvedVariants = resolveVariants(config, variantProps);
1080
+ const variantProps = (variants ?? {}) as Record<string, unknown>;
1081
+ let resolvedVariants = resolveVariantsHot(variantProps);
976
1082
  // Run computed function to get variants set via setVariants and
977
1083
  // setDefaultVariants
978
- const { updatedVariants } = runComputedFunction(
979
- config,
980
- resolvedVariants,
981
- variantProps,
982
- );
983
- return updatedVariants as VariantValues<MergedVariants>;
1084
+ if (computed) {
1085
+ const updatedVariants: Record<string, unknown> = {};
1086
+ Object.assign(updatedVariants, resolvedVariants);
1087
+ const ctx = {
1088
+ variants: resolvedVariants as VariantValues<Record<string, unknown>>,
1089
+ setVariants: (
1090
+ newVariants: VariantValues<Record<string, unknown>>,
1091
+ ) => {
1092
+ const filtered: Record<string, unknown> = {};
1093
+ filterDisabledInto(
1094
+ newVariants as Record<string, unknown>,
1095
+ filtered,
1096
+ );
1097
+ Object.assign(updatedVariants, filtered);
1098
+ },
1099
+ setDefaultVariants: (
1100
+ newDefaults: VariantValues<Record<string, unknown>>,
1101
+ ) => {
1102
+ for (const key in newDefaults) {
1103
+ if (!hasOwn.call(newDefaults, key)) continue;
1104
+ if (variantProps[key] !== undefined) continue;
1105
+ if (disabledVariantKeys.has(key)) continue;
1106
+ const value = (newDefaults as Record<string, unknown>)[key];
1107
+ const valueKey = getVariantValueKey(value);
1108
+ if (
1109
+ valueKey != null &&
1110
+ disabledVariantValues[key]?.has(valueKey)
1111
+ ) {
1112
+ continue;
1113
+ }
1114
+ updatedVariants[key] = value;
1115
+ }
1116
+ },
1117
+ addClass: () => {},
1118
+ addStyle: () => {},
1119
+ };
1120
+ computed(ctx);
1121
+ const filteredUpdated: Record<string, unknown> = {};
1122
+ filterDisabledInto(updatedVariants, filteredUpdated);
1123
+ resolvedVariants = filteredUpdated;
1124
+ }
1125
+ return resolvedVariants as VariantValues<MergedVariants>;
984
1126
  };
985
1127
 
986
- // Compute base class (without variants) - includes extended base classes
987
- const extendedBaseClasses: ClassValue[] = [];
988
- if (config.extend) {
989
- for (const ext of config.extend) {
990
- const meta = getComponentMeta(ext);
991
- extendedBaseClasses.push(meta?.baseClass ?? "");
992
- }
993
- }
994
- const baseClass = cx(
995
- ...(extendedBaseClasses as ClsxClassValue[]),
1128
+ // Compute base class (without variants) - includes extended base classes.
1129
+ // Reuses `extBaseClassesArr` (built earlier) so we don't walk `extend` and
1130
+ // call `getComponentMeta` a second time.
1131
+ const computedBaseClass = cx(
1132
+ ...(extBaseClassesArr as ClsxClassValue[]),
996
1133
  config.class as ClsxClassValue,
997
1134
  );
998
1135
 
999
- // Compute static defaults once at creation time (without triggering
1000
- // computed functions)
1001
- const staticDefaults = collectStaticDefaults(config);
1136
+ // Shared closures across the default and modal components.
1137
+ const classFn = (props: ComponentProps<MergedVariants> = {}) => {
1138
+ return computeResult(props).className;
1139
+ };
1140
+ const meta: ComponentMeta = {
1141
+ baseClass: computedBaseClass,
1142
+ staticDefaults,
1143
+ resolveDefaults: resolveDefaultsFn,
1144
+ };
1002
1145
 
1003
- const initializeComponent = <
1146
+ const initComponent = <
1004
1147
  R extends ComponentResult,
1005
1148
  T extends ModalComponent<MergedVariants, R>,
1006
1149
  >(
1007
- component: T,
1008
- propsKeys: string[],
1150
+ c: T,
1151
+ keys: string[],
1152
+ style: T["style"],
1009
1153
  ): T => {
1010
- component.class = (props: ComponentProps<MergedVariants> = {}) => {
1011
- return computeResult(props).className;
1012
- };
1013
-
1014
- component.getVariants = getVariants;
1015
- component.keys = propsKeys;
1016
- component.variantKeys = variantKeys;
1017
- component.propKeys = propsKeys;
1018
-
1019
- // Store internal metadata hidden from public types
1020
- setComponentMeta(component, {
1021
- baseClass,
1022
- staticDefaults,
1023
- resolveDefaults: createResolveDefaults(config),
1024
- });
1025
-
1026
- return component;
1154
+ c.class = classFn;
1155
+ c.style = style;
1156
+ c.getVariants = getVariants;
1157
+ c.keys = keys;
1158
+ c.variantKeys = variantKeys;
1159
+ c.propKeys = keys;
1160
+ setComponentMeta(c, meta);
1161
+ return c;
1027
1162
  };
1028
1163
 
1029
- const createDefaultComponent = (): CVComponent<V, CV, E> => {
1030
- const component = ((props: ComponentProps<MergedVariants> = {}) => {
1031
- const { className, style } = computeResult(props);
1032
- return { class: className, style };
1033
- }) as CVComponent<V, CV, E>;
1034
-
1035
- component.style = (props: ComponentProps<MergedVariants> = {}) => {
1036
- return computeResult(props).style;
1037
- };
1038
-
1039
- return initializeComponent(component, inputPropsKeys);
1040
- };
1041
-
1042
- const createModalComponent = <R extends ComponentResult>(
1043
- mode: Mode,
1044
- ): ModalComponent<MergedVariants, R> => {
1045
- const propsKeys = getPropsKeys(mode);
1046
-
1047
- const component = ((props: ComponentProps<MergedVariants> = {}) => {
1048
- const { className, style } = computeResult(props);
1049
-
1050
- if (mode === "jsx") {
1051
- return { className, style: styleValueToJSXStyle(style) };
1052
- }
1053
- if (mode === "html") {
1054
- return { class: className, style: styleValueToHTMLStyle(style) };
1055
- }
1056
- // htmlObj
1057
- return { class: className, style: styleValueToHTMLObjStyle(style) };
1058
- }) as ModalComponent<MergedVariants, R>;
1059
-
1060
- component.class = (props: ComponentProps<MergedVariants> = {}) => {
1061
- return computeResult(props).className;
1062
- };
1063
-
1064
- component.style = (props: ComponentProps<MergedVariants> = {}) => {
1065
- const { style } = computeResult(props);
1066
- if (mode === "jsx") return styleValueToJSXStyle(style);
1067
- if (mode === "html") return styleValueToHTMLStyle(style);
1068
- return styleValueToHTMLObjStyle(style);
1069
- };
1070
- return initializeComponent(component, propsKeys);
1071
- };
1164
+ // Default component
1165
+ const defaultComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1166
+ const { className, style } = computeResult(props);
1167
+ return { class: className, style };
1168
+ }) as CVComponent<V, CV, E>;
1169
+ initComponent(defaultComponent, inputPropsKeys, (props = {}) => {
1170
+ return computeResult(props).style;
1171
+ });
1172
+
1173
+ // JSX component
1174
+ const jsxComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1175
+ const { className, style } = computeResult(props);
1176
+ return { className, style: styleValueToJSXStyle(style) };
1177
+ }) as ModalComponent<MergedVariants, JSXProps>;
1178
+ initComponent(
1179
+ jsxComponent,
1180
+ ["className", "style", ...variantKeys],
1181
+ (props = {}) => styleValueToJSXStyle(computeResult(props).style),
1182
+ );
1072
1183
 
1073
- const defaultComponent = createDefaultComponent();
1184
+ // HTML component
1185
+ const htmlComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1186
+ const { className, style } = computeResult(props);
1187
+ return { class: className, style: styleValueToHTMLStyle(style) };
1188
+ }) as ModalComponent<MergedVariants, HTMLProps>;
1189
+ initComponent(
1190
+ htmlComponent,
1191
+ ["class", "style", ...variantKeys],
1192
+ (props = {}) => styleValueToHTMLStyle(computeResult(props).style),
1193
+ );
1074
1194
 
1075
- // Create all modal variants
1076
- const jsxComponent = createModalComponent<JSXProps>("jsx");
1077
- const htmlComponent = createModalComponent<HTMLProps>("html");
1078
- const htmlObjComponent = createModalComponent<HTMLObjProps>("htmlObj");
1195
+ // HTMLObj component
1196
+ const htmlObjComponent = ((props: ComponentProps<MergedVariants> = {}) => {
1197
+ const { className, style } = computeResult(props);
1198
+ return { class: className, style: styleValueToHTMLObjStyle(style) };
1199
+ }) as ModalComponent<MergedVariants, HTMLObjProps>;
1200
+ initComponent(
1201
+ htmlObjComponent,
1202
+ ["class", "style", ...variantKeys],
1203
+ (props = {}) => styleValueToHTMLObjStyle(computeResult(props).style),
1204
+ );
1079
1205
 
1080
- // Build the final component
1081
- const component = defaultComponent;
1082
- component.jsx = jsxComponent;
1083
- component.html = htmlComponent;
1084
- component.htmlObj = htmlObjComponent;
1206
+ defaultComponent.jsx = jsxComponent;
1207
+ defaultComponent.html = htmlComponent;
1208
+ defaultComponent.htmlObj = htmlObjComponent;
1085
1209
 
1086
- return component;
1210
+ return defaultComponent;
1087
1211
  };
1088
1212
 
1089
1213
  return { cv, cx };