classname-variants 1.2.0 → 1.3.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/README.md CHANGED
@@ -4,13 +4,13 @@ Stitches-like [variant API](https://stitches.dev/docs/variants) for plain class
4
4
 
5
5
  The library is framework agnostic and can be used with any kind of CSS flavor.
6
6
 
7
- It is especially useful though if used with [Tailwind](https://tailwindcss.com/) or [CSS Modules](https://github.com/css-modules/css-modules) in cobmination with React, as it provides some [dedicated helpers](#React) and even allows for a _styled-components_ like API, but with [class names instead of styles](#bonus-styled-components-but-with-class-names-)!
7
+ It is especially useful though if used with [Tailwind](https://tailwindcss.com/) or [CSS Modules](https://github.com/css-modules/css-modules) in combination with React, as it provides some [dedicated helpers](#React) and even allows for a _styled-components_ like API, but with [class names instead of styles](#bonus-styled-components-but-with-class-names-)!
8
8
 
9
9
  [![Edit classname-variants/react](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/classname-variants-react-3bzjl?fontsize=14&hidenavigation=1&theme=dark)
10
10
 
11
11
  # Basics
12
12
 
13
- Let's aussume we want to build a button component with Tailwind CSS that comes in different sizes and colors.
13
+ Let's assume we want to build a button component with Tailwind CSS that comes in different sizes and colors.
14
14
 
15
15
  It consists of some _base classes_ that are always present as well as some optional classes that need to be added depending on the desired _variants_.
16
16
 
@@ -135,10 +135,10 @@ const buttonProps = variantProps({
135
135
  });
136
136
  ```
137
137
 
138
- This way a compontents' props (or part of them) can be directly spread into the target element. All variant-related props are used to construct the `className` property while all other props are passed through verbatim:
138
+ This way a component's props (or part of them) can be directly spread into the target element. All variant-related props are used to construct the `className` property while all other props are passed through verbatim:
139
139
 
140
140
  ```tsx
141
- type Props = SX.IntrinsicElements["button"] &
141
+ type Props = JSX.IntrinsicElements["button"] &
142
142
  VariantPropsOf<typeof buttonProps>;
143
143
 
144
144
  function Button(props: Props) {
@@ -154,9 +154,9 @@ function App() {
154
154
  }
155
155
  ```
156
156
 
157
- # Bonus: styled-components, but with class names 💅
157
+ # Bonus: styled-components, but for static CSS 💅
158
158
 
159
- Things can be taken even a step further, resulting in a _styled-components_ like way of defining reusable components:
159
+ Things can be taken even a step further, resulting in a _styled-components_ like way of defining reusable components. Under the hood, this does basically the same as the example above, but also handles _refs_ correctly:
160
160
 
161
161
  ```tsx
162
162
  import { styled } from "classname-variants/react";
@@ -171,7 +171,7 @@ const Button = styled("button", {
171
171
  });
172
172
  ```
173
173
 
174
- The same can be done with CSS modules:
174
+ Again, this is not limited to tailwind, so you could do the same with CSS modules:
175
175
 
176
176
  ```tsx
177
177
  import { styled } from "classname-variants/react";
@@ -187,7 +187,33 @@ const Button = styled("button", {
187
187
  });
188
188
  ```
189
189
 
190
- You can also style other custom React components as long as the accept a `className` prop.
190
+ **NOTE:** You can also style other custom React components as long as they accept a `className` prop.
191
+
192
+ ## Polymorphic components with "as"
193
+
194
+ If you want to keep all the variants you have defined for a component but want to render a different HTML tag or a different custom component, you can use the "as" prop to do so:
195
+
196
+ ```tsx
197
+ import { styled } from "classname-variants/react";
198
+ import styles from "./styles.module.css";
199
+
200
+ const Button = styled("button", {
201
+ variants: {
202
+ //...
203
+ },
204
+ });
205
+
206
+ function App() {
207
+ return (
208
+ <div>
209
+ <Button>I'm a button</Button>
210
+ <Button as="a" href="/">
211
+ I'm a link!
212
+ </Button>
213
+ </div>
214
+ );
215
+ }
216
+ ```
191
217
 
192
218
  # Tailwind IntelliSense
193
219
 
package/example.tsx CHANGED
@@ -12,7 +12,9 @@ function CustomComponent({
12
12
  return <div {...props}>{title}</div>;
13
13
  }
14
14
 
15
- const Card = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
15
+ const Card = styled("div", "bg-white p-4 border-2 rounded-lg");
16
+
17
+ const TitleCard = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
16
18
 
17
19
  const Button = styled("button", {
18
20
  base: "px-5 py-2 text-white disabled:bg-gray-400 disabled:text-gray-300",
@@ -51,7 +53,14 @@ function App() {
51
53
  <Button color="accent" disabled>
52
54
  Disabled
53
55
  </Button>
54
- <Card title="Hello" />
56
+ <TitleCard title="Hello" />
57
+ <Card>
58
+ <h1>Hello</h1>
59
+ <p>world</p>
60
+ </Card>
61
+ <Card as="a" href="https://example.com">
62
+ Link
63
+ </Card>
55
64
  </div>
56
65
  );
57
66
  }
@@ -0,0 +1 @@
1
+ export {};
package/lib/example.js ADDED
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom";
3
+ import { styled } from "./src/react";
4
+ function CustomComponent({ title, ...props }) {
5
+ return React.createElement("div", { ...props }, title);
6
+ }
7
+ const Card = styled("div", "bg-white p-4 border-2 rounded-lg");
8
+ const TitleCard = styled(CustomComponent, "bg-white p-4 border-2 rounded-lg");
9
+ const Button = styled("button", {
10
+ base: "px-5 py-2 text-white disabled:bg-gray-400 disabled:text-gray-300",
11
+ variants: {
12
+ color: {
13
+ neutral: "bg-slate-500 hover:bg-slate-400",
14
+ accent: "bg-teal-500 hover:bg-teal-400",
15
+ },
16
+ outlined: {
17
+ true: "border-2",
18
+ },
19
+ rounded: {
20
+ true: "rounded-full",
21
+ false: "rounded-sm",
22
+ },
23
+ },
24
+ compoundVariants: [
25
+ {
26
+ variants: { color: "accent", outlined: true },
27
+ className: "border-teal-600",
28
+ },
29
+ ],
30
+ defaultVariants: {
31
+ color: "neutral",
32
+ },
33
+ });
34
+ function App() {
35
+ return (React.createElement("div", { className: "flex justify-center items-center pt-8 gap-4 flex-wrap" },
36
+ React.createElement(Button, { onClick: console.log }, "Accent"),
37
+ React.createElement(Button, { rounded: true }, "Neutral + Rounded"),
38
+ React.createElement(Button, { color: "accent", outlined: true }, "Accent + Outlined"),
39
+ React.createElement(Button, { color: "accent", disabled: true }, "Disabled"),
40
+ React.createElement(TitleCard, { title: "Hello" }),
41
+ React.createElement(Card, null,
42
+ React.createElement("h1", null, "Hello"),
43
+ React.createElement("p", null, "world"))));
44
+ }
45
+ ReactDOM.render(React.createElement(App, null), document.getElementById("root"));
package/lib/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ComponentProps, ElementType, ForwardRefExoticComponent, PropsWithoutRef } from "react";
1
+ import { ComponentProps, ElementType, ReactElement } from "react";
2
2
  import { Variants, VariantsConfig, VariantOptions, Simplify } from "./index.js";
3
3
  /**
4
4
  * Utility type to infer the first argument of a variantProps function.
@@ -13,6 +13,18 @@ declare type VariantProps<C extends VariantsConfig<V>, V extends Variants = C["v
13
13
  export declare function variantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]>(config: Simplify<C>): <P extends VariantProps<C, C["variants"]>>(props: P) => {
14
14
  className: string;
15
15
  } & Omit<P, keyof C["variants"]>;
16
- declare type StyledComponent<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = C["variants"]> = ForwardRefExoticComponent<PropsWithoutRef<ComponentProps<T> & VariantOptions<C>> & React.RefAttributes<T>>;
17
- export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = C["variants"]>(type: T, config: string | Simplify<C>): StyledComponent<T, C>;
16
+ declare type VariantsOf<T> = T extends VariantsConfig<infer V> ? V : {};
17
+ declare type AsProps<T extends ElementType = ElementType> = {
18
+ as?: T;
19
+ };
20
+ declare type PolymorphicComponentProps<T extends ElementType> = AsProps<T> & Omit<ComponentProps<T>, "as">;
21
+ export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = VariantsOf<C>>(type: T, config: string | Simplify<C>): <As extends ElementType<any> = T>(props: AsProps<As> & Omit<ComponentProps<As>, "as"> & { [K in keyof C["variants"] as K extends keyof { [K_1 in keyof C["variants"] as C["variants"][K_1] extends {
22
+ true: any;
23
+ } | {
24
+ false: any;
25
+ } ? K_1 : never]: C["variants"][K_1]; } | keyof { [K_2 in keyof C["variants"] as K_2 extends keyof C["defaultVariants"] ? K_2 : never]: C["variants"][K_2]; } ? never : K]: { [K_3 in keyof C["variants"]]: keyof C["variants"][K_3] extends "true" | "false" ? boolean : keyof C["variants"][K_3]; }[K]; } & { [K_4 in keyof C["variants"] as K_4 extends keyof { [K_1 in keyof C["variants"] as C["variants"][K_1] extends {
26
+ true: any;
27
+ } | {
28
+ false: any;
29
+ } ? K_1 : never]: C["variants"][K_1]; } | keyof { [K_2 in keyof C["variants"] as K_2 extends keyof C["defaultVariants"] ? K_2 : never]: C["variants"][K_2]; } ? K_4 : never]?: { [K_3 in keyof C["variants"]]: keyof C["variants"][K_3] extends "true" | "false" ? boolean : keyof C["variants"][K_3]; }[K_4] | undefined; }) => ReactElement | null;
18
30
  export {};
package/lib/react.js CHANGED
@@ -21,5 +21,8 @@ export function styled(type, config) {
21
21
  const styledProps = typeof config === "string"
22
22
  ? variantProps({ base: config, variants: {} })
23
23
  : variantProps(config);
24
- return forwardRef((props, ref) => createElement(type, { ...styledProps(props), ref }));
24
+ const Component = forwardRef(({ as, ...props }, ref) => {
25
+ return createElement(as !== null && as !== void 0 ? as : type, { ...styledProps(props), ref });
26
+ });
27
+ return Component;
25
28
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Definition of the available variants and their options.
3
+ * @example
4
+ * {
5
+ * color: {
6
+ * white: "bg-white"
7
+ * green: "bg-green-500",
8
+ * },
9
+ * size: {
10
+ * small: "text-xs",
11
+ * large: "text-lg"
12
+ * }
13
+ * }
14
+ */
15
+ export declare type Variants = Record<string, Record<string, string>>;
16
+ /**
17
+ * Configuration including defaults and compound variants.
18
+ */
19
+ export interface VariantsConfig<V extends Variants> {
20
+ base?: string;
21
+ variants: V;
22
+ compoundVariants?: CompoundVariant<V>[];
23
+ defaultVariants?: Partial<OptionsOf<V>>;
24
+ }
25
+ /**
26
+ * Rules for class names that are applied for certain variant combinations.
27
+ */
28
+ export interface CompoundVariant<V extends Variants> {
29
+ variants: Partial<OptionsOf<V>>;
30
+ className: string;
31
+ }
32
+ /**
33
+ * Only the boolean variants, i.e. ones that have "true" or "false" as options.
34
+ */
35
+ declare type BooleanVariants<V extends Variants> = {
36
+ [K in keyof V as V[K] extends {
37
+ true: any;
38
+ } | {
39
+ false: any;
40
+ } ? K : never]: V[K];
41
+ };
42
+ /**
43
+ * Only the variants for which a default options is set.
44
+ */
45
+ declare type DefaultVariants<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = {
46
+ [K in keyof V as K extends keyof C["defaultVariants"] ? K : never]: C["variants"][K];
47
+ };
48
+ /**
49
+ * Names of all optional variants, i.e. booleans or ones with default options.
50
+ */
51
+ declare type OptionalVariantNames<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = keyof BooleanVariants<V> | keyof DefaultVariants<C>;
52
+ /**
53
+ * Possible options for all the optional variants.
54
+ *
55
+ * @example
56
+ * {
57
+ * color?: "white" | "green",
58
+ * rounded?: boolean | undefined
59
+ * }
60
+ */
61
+ declare type OptionalOptions<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = {
62
+ [K in keyof V as K extends OptionalVariantNames<C> ? K : never]?: OptionsOf<V>[K];
63
+ };
64
+ /**
65
+ * Possible options for all required variants.
66
+ *
67
+ * @example {
68
+ * size: "small" | "large"
69
+ * }
70
+ */
71
+ declare type RequiredOptions<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = {
72
+ [K in keyof V as K extends OptionalVariantNames<C> ? never : K]: OptionsOf<V>[K];
73
+ };
74
+ /**
75
+ * Utility type to extract the possible options.
76
+ * Converts "true" | "false" options into booleans.
77
+ *
78
+ * @example
79
+ * OptionsOf<{
80
+ * size: { small: "text-xs"; large: "text-lg" };
81
+ * rounded: { true: "rounded-full" }
82
+ * }>
83
+ * ==>
84
+ * {
85
+ * size: "text-xs" | "text-lg";
86
+ * rounded: boolean;
87
+ * }
88
+ */
89
+ declare type OptionsOf<V extends Variants> = {
90
+ [K in keyof V]: keyof V[K] extends "true" | "false" ? boolean : keyof V[K];
91
+ };
92
+ /**
93
+ * Extracts the possible options.
94
+ */
95
+ export declare type VariantOptions<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = RequiredOptions<C> & OptionalOptions<C>;
96
+ /**
97
+ * Without this conversion step, defaultVariants and compoundVariants will
98
+ * allow extra keys, i.e. non-existent variants.
99
+ * See https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts
100
+ */
101
+ export declare type Simplify<T> = {
102
+ [K in keyof T]: T[K];
103
+ };
104
+ export declare function variants<C extends VariantsConfig<V>, V extends Variants = C["variants"]>(config: Simplify<C>): (props: VariantOptions<C>) => string;
105
+ export {};
@@ -0,0 +1,27 @@
1
+ export function variants(config) {
2
+ const { base, variants, compoundVariants, defaultVariants } = config;
3
+ const isBooleanVariant = (name) => {
4
+ const v = variants === null || variants === void 0 ? void 0 : variants[name];
5
+ return v && ("false" in v || "true" in v);
6
+ };
7
+ return (props) => {
8
+ var _a;
9
+ const res = [base];
10
+ const getSelected = (name) => {
11
+ var _a, _b;
12
+ return (_b = (_a = props[name]) !== null && _a !== void 0 ? _a : defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[name]) !== null && _b !== void 0 ? _b : (isBooleanVariant(name) ? false : undefined);
13
+ };
14
+ for (let name in variants) {
15
+ const selected = getSelected(name);
16
+ if (selected !== undefined)
17
+ res.push((_a = variants[name]) === null || _a === void 0 ? void 0 : _a[selected]);
18
+ }
19
+ for (let { variants, className } of compoundVariants !== null && compoundVariants !== void 0 ? compoundVariants : []) {
20
+ const isSelected = (name) => getSelected(name) === variants[name];
21
+ if (Object.keys(variants).every(isSelected)) {
22
+ res.push(className);
23
+ }
24
+ }
25
+ return res.filter(Boolean).join(" ");
26
+ };
27
+ }
@@ -0,0 +1,19 @@
1
+ import { ComponentProps, ElementType, ForwardRefExoticComponent, PropsWithoutRef } from "react";
2
+ import { Variants, VariantsConfig, VariantOptions, Simplify } from "./index.js";
3
+ /**
4
+ * Utility type to infer the first argument of a variantProps function.
5
+ */
6
+ export declare type VariantPropsOf<T> = T extends (props: infer P) => any ? P : never;
7
+ /**
8
+ * Type for the variantProps() argument – consists of the VariantOptions and an optional className for chaining.
9
+ */
10
+ declare type VariantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]> = VariantOptions<C> & {
11
+ className?: string;
12
+ };
13
+ export declare function variantProps<C extends VariantsConfig<V>, V extends Variants = C["variants"]>(config: Simplify<C>): <P extends VariantProps<C, C["variants"]>>(props: P) => {
14
+ className: string;
15
+ } & Omit<P, keyof C["variants"]>;
16
+ declare type StyledComponent<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = C["variants"]> = ForwardRefExoticComponent<PropsWithoutRef<ComponentProps<T> & VariantOptions<C>> & React.RefAttributes<T>>;
17
+ declare type Vars<T> = T extends VariantsConfig<infer V> ? V : {};
18
+ export declare function styled<T extends ElementType, C extends VariantsConfig<V>, V extends Variants = Vars<C>>(type: T, config: string | Simplify<C>): StyledComponent<T, C>;
19
+ export {};
@@ -0,0 +1,25 @@
1
+ import { createElement, forwardRef, } from "react";
2
+ import { variants, } from "./index.js";
3
+ export function variantProps(config) {
4
+ const variantClassName = variants(config);
5
+ return (props) => {
6
+ const result = {};
7
+ // Pass-through all unrelated props
8
+ for (let prop in props) {
9
+ if (config.variants && !(prop in config.variants)) {
10
+ result[prop] = props[prop];
11
+ }
12
+ }
13
+ // Add the optionally passed className prop for chaining
14
+ result.className = [props.className, variantClassName(props)]
15
+ .filter(Boolean)
16
+ .join(" ");
17
+ return result;
18
+ };
19
+ }
20
+ export function styled(type, config) {
21
+ const styledProps = typeof config === "string"
22
+ ? variantProps({ base: config, variants: {} })
23
+ : variantProps(config);
24
+ return forwardRef((props, ref) => createElement(type, { ...styledProps(props), ref }));
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "classname-variants",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Variant API for plain class names",
5
5
  "author": "Felix Gnass <fgnass@gmail.com>",
6
6
  "license": "MIT",
@@ -44,6 +44,6 @@
44
44
  "@types/react": "^17.0.39",
45
45
  "react": "^17.0.2",
46
46
  "react-dom": "^17.0.2",
47
- "typescript": "^4.5.5"
47
+ "typescript": "^4.6.4"
48
48
  }
49
49
  }
package/src/react.ts CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  createElement,
4
4
  ElementType,
5
5
  forwardRef,
6
- ForwardRefExoticComponent,
7
- PropsWithoutRef,
6
+ ReactElement,
7
+ Ref,
8
8
  } from "react";
9
9
 
10
10
  import {
@@ -52,25 +52,31 @@ export function variantProps<
52
52
  };
53
53
  }
54
54
 
55
- type StyledComponent<
56
- T extends ElementType,
57
- C extends VariantsConfig<V>,
58
- V extends Variants = C["variants"]
59
- > = ForwardRefExoticComponent<
60
- PropsWithoutRef<ComponentProps<T> & VariantOptions<C>> &
61
- React.RefAttributes<T>
62
- >;
55
+ type VariantsOf<T> = T extends VariantsConfig<infer V> ? V : {};
56
+
57
+ type AsProps<T extends ElementType = ElementType> = {
58
+ as?: T;
59
+ };
60
+
61
+ type PolymorphicComponentProps<T extends ElementType> = AsProps<T> &
62
+ Omit<ComponentProps<T>, "as">;
63
63
 
64
64
  export function styled<
65
65
  T extends ElementType,
66
66
  C extends VariantsConfig<V>,
67
- V extends Variants = C["variants"]
68
- >(type: T, config: string | Simplify<C>): StyledComponent<T, C> {
67
+ V extends Variants = VariantsOf<C>
68
+ >(type: T, config: string | Simplify<C>) {
69
69
  const styledProps =
70
70
  typeof config === "string"
71
71
  ? variantProps({ base: config, variants: {} })
72
72
  : variantProps(config);
73
- return forwardRef<T, ComponentProps<T> & VariantOptions<C>>((props, ref) =>
74
- createElement(type, { ...styledProps(props), ref })
73
+
74
+ const Component: <As extends ElementType = T>(
75
+ props: PolymorphicComponentProps<As> & VariantOptions<C>
76
+ ) => ReactElement | null = forwardRef(
77
+ ({ as, ...props }: AsProps, ref: Ref<Element>) => {
78
+ return createElement(as ?? type, { ...styledProps(props), ref });
79
+ }
75
80
  );
81
+ return Component;
76
82
  }