clava 0.1.0 → 0.1.2

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
@@ -1,29 +1,28 @@
1
1
  import clsx, { type ClassValue as ClsxClassValue } from "clsx";
2
-
3
2
  import type {
4
- Variants,
5
- ComputedVariants,
6
3
  AnyComponent,
7
- Component,
8
- StyleProps,
9
4
  ClassValue,
10
- StyleValue,
11
- StyleClassValue,
12
- VariantValues,
5
+ Component,
6
+ ComponentProps,
7
+ ComponentResult,
13
8
  Computed,
9
+ ComputedVariants,
14
10
  ExtendableVariants,
11
+ HTMLObjProps,
12
+ HTMLProps,
13
+ JSXProps,
15
14
  MergeVariants,
16
15
  ModalComponent,
17
- ComponentResult,
18
- JSXProps,
19
- HTMLProps,
20
- HTMLObjProps,
21
- OnlyVariantsComponent,
22
- ComponentProps,
23
16
  SplitPropsFunction,
17
+ StyleClassValue,
18
+ StyleProps,
19
+ StyleValue,
20
+ VariantValues,
21
+ Variants,
24
22
  } from "./types.ts";
25
-
26
23
  import {
24
+ type Mode,
25
+ getClassPropertyName,
27
26
  htmlObjStyleToStyleValue,
28
27
  htmlStyleToStyleValue,
29
28
  isHTMLObjStyle,
@@ -33,9 +32,6 @@ import {
33
32
  styleValueToJSXStyle,
34
33
  } from "./utils.ts";
35
34
 
36
- const MODES = ["jsx", "html", "htmlObj"] as const;
37
- type Mode = (typeof MODES)[number];
38
-
39
35
  export type { ClassValue, StyleValue, StyleClassValue };
40
36
 
41
37
  export type VariantProps<T extends Pick<AnyComponent, "getVariants">> =
@@ -124,7 +120,7 @@ function collectVariantKeys(
124
120
  // Collect from extended components
125
121
  if (config.extend) {
126
122
  for (const ext of config.extend) {
127
- for (const key of ext.onlyVariants.keys) {
123
+ for (const key of ext.variantKeys) {
128
124
  keys.add(key as string);
129
125
  }
130
126
  }
@@ -329,6 +325,7 @@ function processVariants(
329
325
  function processComputed(
330
326
  config: CVConfig<Variants, ComputedVariants, AnyComponent[]>,
331
327
  resolvedVariants: Record<string, unknown>,
328
+ propsVariants: Record<string, unknown>,
332
329
  ): {
333
330
  classes: ClassValue[];
334
331
  style: StyleValue;
@@ -347,9 +344,9 @@ function processComputed(
347
344
  setDefaultVariants: (
348
345
  newDefaults: VariantValues<Record<string, unknown>>,
349
346
  ) => {
350
- // Only apply defaults for variants not already set
347
+ // Only apply defaults for variants not explicitly set in props
351
348
  for (const [key, value] of Object.entries(newDefaults)) {
352
- if (resolvedVariants[key] === undefined) {
349
+ if (propsVariants[key] === undefined) {
353
350
  updatedVariants[key] = value;
354
351
  }
355
352
  }
@@ -367,46 +364,67 @@ function processComputed(
367
364
  return { classes, style, updatedVariants };
368
365
  }
369
366
 
370
- /**
371
- * Normalizes a key source (array or component) to an object with keys and defaults.
372
- */
373
- function normalizeKeySource(source: unknown): {
367
+ interface NormalizedSource {
374
368
  keys: string[];
369
+ variantKeys: string[];
375
370
  defaults: Record<string, unknown>;
376
- } {
371
+ isComponent: boolean;
372
+ }
373
+
374
+ /**
375
+ * Normalizes a key source (array or component) to an object with keys, variantKeys, defaults, and isComponent flag.
376
+ */
377
+ function normalizeKeySource(source: unknown): NormalizedSource {
377
378
  if (Array.isArray(source)) {
378
- return { keys: source as string[], defaults: {} };
379
+ return {
380
+ keys: source as string[],
381
+ variantKeys: source as string[],
382
+ defaults: {},
383
+ isComponent: false,
384
+ };
379
385
  }
380
- // Components are functions with keys property, onlyVariants are objects with keys
386
+ // Components are functions with keys and variantKeys properties
381
387
  if (
382
388
  source &&
383
389
  (typeof source === "object" || typeof source === "function") &&
384
- "keys" in source
390
+ "keys" in source &&
391
+ "variantKeys" in source
385
392
  ) {
386
393
  const keys = [...(source as { keys: string[] }).keys] as string[];
394
+ const variantKeys = [
395
+ ...(source as { variantKeys: string[] }).variantKeys,
396
+ ] as string[];
387
397
  const defaults =
388
398
  "getVariants" in source
389
399
  ? (
390
400
  source as { getVariants: () => Record<string, unknown> }
391
401
  ).getVariants()
392
402
  : {};
393
- return { keys, defaults };
403
+ return { keys, variantKeys, defaults, isComponent: true };
394
404
  }
395
- return { keys: [], defaults: {} };
405
+ return { keys: [], variantKeys: [], defaults: {}, isComponent: false };
396
406
  }
397
407
 
398
408
  /**
399
409
  * Splits props into multiple groups based on key sources.
410
+ * Only the first component claims styling props (class/className/style).
411
+ * Subsequent components only receive variant props.
412
+ * Arrays always receive their listed keys but don't claim styling props.
400
413
  */
401
414
  function splitPropsImpl(
402
415
  selfKeys: string[],
416
+ selfVariantKeys: string[],
403
417
  selfDefaults: Record<string, unknown>,
418
+ selfIsComponent: boolean,
404
419
  props: Record<string, unknown>,
405
- sources: Array<{ keys: string[]; defaults: Record<string, unknown> }>,
420
+ sources: NormalizedSource[],
406
421
  ): Record<string, unknown>[] {
407
422
  const allUsedKeys = new Set<string>(selfKeys);
408
423
  const results: Record<string, unknown>[] = [];
409
424
 
425
+ // Track if styling has been claimed by a component
426
+ let stylingClaimed = selfIsComponent;
427
+
410
428
  // Self result with defaults
411
429
  const selfResult: Record<string, unknown> = {};
412
430
  // First apply defaults
@@ -426,20 +444,33 @@ function splitPropsImpl(
426
444
  // Process each source
427
445
  for (const source of sources) {
428
446
  const sourceResult: Record<string, unknown> = {};
429
- // First apply defaults
447
+
448
+ // Determine which keys this source should use
449
+ // Components use variantKeys if styling has already been claimed
450
+ // Arrays always use their listed keys
451
+ const effectiveKeys =
452
+ source.isComponent && stylingClaimed ? source.variantKeys : source.keys;
453
+
454
+ // First apply defaults (only for variant keys if component and styling claimed)
430
455
  for (const [key, value] of Object.entries(source.defaults)) {
431
- if (source.keys.includes(key)) {
456
+ if (effectiveKeys.includes(key)) {
432
457
  sourceResult[key] = value;
433
458
  }
434
459
  }
460
+
435
461
  // Then override with props
436
- for (const key of source.keys) {
462
+ for (const key of effectiveKeys) {
437
463
  allUsedKeys.add(key);
438
464
  if (key in props) {
439
465
  sourceResult[key] = props[key];
440
466
  }
441
467
  }
442
468
  results.push(sourceResult);
469
+
470
+ // If this is a component that hasn't claimed styling yet, mark styling as claimed
471
+ if (source.isComponent && !stylingClaimed) {
472
+ stylingClaimed = true;
473
+ }
443
474
  }
444
475
 
445
476
  // Rest - keys not used by anyone
@@ -457,6 +488,9 @@ function splitPropsImpl(
457
488
  /**
458
489
  * Splits props into multiple groups based on key sources.
459
490
  * Each source gets its own result object containing all its matching keys.
491
+ * The first component source claims styling props (class/className/style).
492
+ * Subsequent components only receive variant props.
493
+ * Arrays receive their listed keys but don't claim styling props.
460
494
  * The last element is always the "rest" containing keys not claimed by any source.
461
495
  *
462
496
  * @example
@@ -464,8 +498,10 @@ function splitPropsImpl(
464
498
  * const [buttonProps, inputProps, rest] = splitProps(
465
499
  * props,
466
500
  * buttonComponent,
467
- * inputComponent.onlyVariants,
501
+ * inputComponent,
468
502
  * );
503
+ * // buttonProps has class/style + button variants
504
+ * // inputProps has only input variants (no class/style)
469
505
  * ```
470
506
  */
471
507
  export const splitProps: SplitPropsFunction = ((
@@ -477,13 +513,15 @@ export const splitProps: SplitPropsFunction = ((
477
513
  const normalizedSources = sources.map(normalizeKeySource);
478
514
  return splitPropsImpl(
479
515
  normalizedSource1.keys,
516
+ normalizedSource1.variantKeys,
480
517
  normalizedSource1.defaults,
518
+ normalizedSource1.isComponent,
481
519
  props,
482
520
  normalizedSources,
483
521
  );
484
522
  }) as SplitPropsFunction;
485
523
 
486
- export function create<M extends Mode>({
524
+ export function create<M extends Mode = "jsx">({
487
525
  defaultMode = "jsx" as M,
488
526
  transformClass = (className) => className,
489
527
  }: CreateParams<M> = {}) {
@@ -502,9 +540,6 @@ export function create<M extends Mode>({
502
540
  config as CVConfig<Variants, ComputedVariants, AnyComponent[]>,
503
541
  );
504
542
 
505
- const getClassPropertyName = (mode: Mode) =>
506
- mode === "jsx" ? "className" : "class";
507
-
508
543
  const getPropsKeys = (mode: Mode) => [
509
544
  getClassPropertyName(mode),
510
545
  "style",
@@ -535,6 +570,7 @@ export function create<M extends Mode>({
535
570
  const computedResult = processComputed(
536
571
  config as CVConfig<Variants, ComputedVariants, AnyComponent[]>,
537
572
  resolvedVariants,
573
+ variantProps,
538
574
  );
539
575
  resolvedVariants = computedResult.updatedVariants;
540
576
 
@@ -642,17 +678,9 @@ export function create<M extends Mode>({
642
678
 
643
679
  component.keys = propsKeys as (keyof MergedVariants | keyof R)[];
644
680
 
645
- component.onlyVariants = {
646
- getVariants: (
647
- variants?: VariantValues<MergedVariants>,
648
- ): VariantValues<MergedVariants> => {
649
- return resolveVariants(
650
- config as CVConfig<Variants, ComputedVariants, AnyComponent[]>,
651
- variants as VariantValues<Record<string, unknown>>,
652
- ) as VariantValues<MergedVariants>;
653
- },
654
- keys: variantKeys as (keyof MergedVariants)[],
655
- } as OnlyVariantsComponent<MergedVariants>;
681
+ component.variantKeys = variantKeys as (keyof MergedVariants)[];
682
+
683
+ component.propKeys = propsKeys as (keyof MergedVariants | keyof R)[];
656
684
 
657
685
  // Compute base class (without variants) - includes extended base classes
658
686
  const extendedBaseClasses: ClassValue[] = [];
@@ -0,0 +1,40 @@
1
+ import type { CSSProperties, ComponentProps } from "react";
2
+ import { expect, expectTypeOf, test } from "vitest";
3
+ import type { JSXProps } from "./types.ts";
4
+ import { type VariantProps, cv, splitProps } from "./index.ts";
5
+
6
+ test("splitProps", () => {
7
+ const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
8
+
9
+ interface Props
10
+ extends ComponentProps<"div">, VariantProps<typeof component> {}
11
+ const props: Props = { className: "custom", size: "md", id: "my-div" };
12
+
13
+ const [variantProps, rest] = splitProps(props, component);
14
+ expectTypeOf(variantProps.style).toEqualTypeOf<CSSProperties | undefined>();
15
+ expectTypeOf(variantProps.className).toEqualTypeOf<string | undefined>();
16
+ expect(variantProps.className).toBe("custom");
17
+ expect(variantProps).toEqual({ size: "md", className: "custom" });
18
+ expect(
19
+ // @ts-expect-error rest props should not have className
20
+ rest.className,
21
+ ).toBeUndefined();
22
+ expect(
23
+ // @ts-expect-error rest props should not have style
24
+ rest.style,
25
+ ).toBeUndefined();
26
+ expect(rest).toEqual({ id: "my-div" });
27
+ });
28
+
29
+ test("component props", () => {
30
+ const component = cv({
31
+ style: { fontSize: "16px" },
32
+ variants: { size: { sm: "sm", md: "md" } },
33
+ });
34
+ const props = component({ size: "sm", className: "custom" });
35
+ expectTypeOf(props).toEqualTypeOf<JSXProps>();
36
+ expect(props).toEqual({
37
+ className: "sm custom",
38
+ style: { fontSize: "16px" },
39
+ });
40
+ });
@@ -0,0 +1,36 @@
1
+ import type { ComponentProps, JSX } from "solid-js";
2
+ import { expect, expectTypeOf, test } from "vitest";
3
+ import { type VariantProps, create, splitProps } from "./index.ts";
4
+ import { type HTMLObjProps } from "./types.ts";
5
+
6
+ const { cv } = create({ defaultMode: "htmlObj" });
7
+
8
+ test("splitProps", () => {
9
+ const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
10
+
11
+ interface Props
12
+ extends ComponentProps<"div">, VariantProps<typeof component> {}
13
+ const props: Props = { class: "custom", size: "md", id: "my-div" };
14
+
15
+ const [variantProps, rest] = splitProps(props, component);
16
+ expectTypeOf(variantProps.style).toEqualTypeOf<
17
+ string | JSX.CSSProperties | undefined
18
+ >();
19
+ expectTypeOf(variantProps.class).toEqualTypeOf<string | undefined>();
20
+ expect(variantProps.class).toBe("custom");
21
+ expect(variantProps).toEqual({ size: "md", class: "custom" });
22
+ expect(rest).toEqual({ id: "my-div" });
23
+ });
24
+
25
+ test("component props", () => {
26
+ const component = cv({
27
+ style: { fontSize: "16px" },
28
+ variants: { size: { sm: "sm", md: "md" } },
29
+ });
30
+ const props = component({ size: "sm", className: "custom" });
31
+ expectTypeOf(props).toEqualTypeOf<HTMLObjProps>();
32
+ expect(props).toEqual({
33
+ class: "sm custom",
34
+ style: { "font-size": "16px" },
35
+ });
36
+ });