classname-variants 1.0.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 ADDED
@@ -0,0 +1,173 @@
1
+ # classname-variants
2
+
3
+ Stitches-like [variant API](https://stitches.dev/docs/variants) for plain class names.
4
+
5
+ The library is framework agnostic and can be used with any kind of CSS flavor.
6
+
7
+ It is especially useful though if used with [Tailwind](https://tailwindcss.com/) and 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
+
9
+ # Basics
10
+
11
+ Let's aussume we want to build a button component with Tailwind CSS that comes in different sizes and colors.
12
+
13
+ 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_.
14
+
15
+ ```tsx
16
+ const button = variants({
17
+ base: "rounded text-white",
18
+ variants: {
19
+ color: {
20
+ brand: "bg-sky-500",
21
+ accent: "bg-teal-500",
22
+ },
23
+ size: {
24
+ small: "px-5 py-3 text-xs",
25
+ large: "px-6 py-4 text-base",
26
+ },
27
+ },
28
+ });
29
+ ```
30
+
31
+ The result is a function that expects an object which specifies what variants should be selected. When called, it returns a string containing the respective class names:
32
+
33
+ ```ts
34
+ document.write(`
35
+ <button class="${button({
36
+ color: "accent",
37
+ size: "large",
38
+ })}">
39
+ Click Me!
40
+ </button>
41
+ `);
42
+ ```
43
+
44
+ # Advanced Usage
45
+
46
+ ## Boolean variants
47
+
48
+ Variants can be of type `boolean` by using `"true"` as the key:
49
+
50
+ ```tsx
51
+ const button = variants({
52
+ base: "text-white",
53
+ variants: {
54
+ rounded: {
55
+ true: "rounded-full",
56
+ },
57
+ },
58
+ });
59
+ ```
60
+
61
+ ## Compound variants
62
+
63
+ The `compoundVariants` option can be used to apply class names based on a combination of other variants.
64
+
65
+ ```tsx
66
+ const button = variants({
67
+ variants: {
68
+ color: {
69
+ neutral: "bg-gray-200",
70
+ accent: "bg-teal-400",
71
+ },
72
+ outlined: {
73
+ true: "border-2",
74
+ },
75
+ },
76
+ compoundVariants: [
77
+ {
78
+ variants: {
79
+ color: "accent",
80
+ outlined: true,
81
+ },
82
+ className: "border-teal-500",
83
+ },
84
+ ],
85
+ });
86
+ ```
87
+
88
+ ## Default variants
89
+
90
+ The `defaultVariants` option can be used to select a variant by default:
91
+
92
+ ```ts
93
+ const button = variants({
94
+ variants: {
95
+ color: {
96
+ neutral: "bg-gray-200",
97
+ accent: "bg-teal-400",
98
+ },
99
+ },
100
+ defaultVariants: {
101
+ color: "neutral",
102
+ },
103
+ });
104
+ ```
105
+
106
+ # React
107
+
108
+ The library contains utility functions that are useful for writing React components.
109
+
110
+ It works much like `variants()` but instead of a class name string, the resulting function returns an object with props.
111
+
112
+ ```ts
113
+ import { variantProps } from "classname-variants/react";
114
+
115
+ const buttonProps = variantProps({
116
+ base: "rounded-md text-white",
117
+ variants: {
118
+ color: {
119
+ brand: "bg-sky-500",
120
+ accent: "bg-teal-500",
121
+ },
122
+ size: {
123
+ small: "px-5 py-3 text-xs",
124
+ large: "px-6 py-4 text-base",
125
+ },
126
+ rounded: {
127
+ true: "rounded-full",
128
+ },
129
+ },
130
+ defaultVariants: {
131
+ color: "brand",
132
+ },
133
+ });
134
+ ```
135
+
136
+ 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:
137
+
138
+ ```tsx
139
+ type Props = SX.IntrinsicElements["button"] & VariantProps<typeof buttonProps>;
140
+
141
+ function Button(props: Props) {
142
+ return <button {...buttonProps(props)} />;
143
+ }
144
+
145
+ function App() {
146
+ return (
147
+ <Button size="small" color="accent" onClick={console.log}>
148
+ Click Me!
149
+ </Button>
150
+ );
151
+ }
152
+ ```
153
+
154
+ # Bonus: styled-components, but with class names 💅
155
+
156
+ Things can be taken even a step further, resulting in a _styled-components_ like way of defining reusable components:
157
+
158
+ ```tsx
159
+ import { styled } from "classname-variants/react";
160
+
161
+ const Button = styled("button", {
162
+ variants: {
163
+ size: {
164
+ small: "text-xs",
165
+ large: "text-base",
166
+ },
167
+ },
168
+ });
169
+ ```
170
+
171
+ # License
172
+
173
+ MIT
package/example.tsx ADDED
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom";
3
+ import { styled } from "./src/react";
4
+
5
+ const Button = styled("button", {
6
+ base: "rounded-md px-5 py-2 text-white",
7
+ variants: {
8
+ color: {
9
+ neutral: "bg-gray-500",
10
+ accent: "bg-teal-500",
11
+ },
12
+ outlined: {
13
+ true: "border-2",
14
+ },
15
+ },
16
+ compoundVariants: [
17
+ {
18
+ variants: {
19
+ color: "accent",
20
+ outlined: true,
21
+ },
22
+ className: "border-teal-800",
23
+ },
24
+ ],
25
+ });
26
+
27
+ function App() {
28
+ return (
29
+ <>
30
+ <Button color="neutral" onClick={console.log}>
31
+ Neutral
32
+ </Button>
33
+ <Button color="accent" outlined onClick={console.log}>
34
+ Accent + Outlined
35
+ </Button>
36
+ </>
37
+ );
38
+ }
39
+
40
+ ReactDOM.render(<App />, document.getElementById("root"));
package/index.html ADDED
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, user-scalable=no"
8
+ />
9
+ <title>classname-variants</title>
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="./example.tsx"></script>
15
+ </body>
16
+ </html>
package/lib/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export declare type VariantDefinitions = Record<string, string>;
2
+ export declare type Variants = Record<string, VariantDefinitions>;
3
+ export declare type VariantsConfig<V extends Variants> = {
4
+ base?: string;
5
+ variants?: V;
6
+ compoundVariants?: CompoundVariant<V>[];
7
+ defaultVariants?: VariantSelection<V>;
8
+ };
9
+ export declare type CompoundVariant<V extends Variants> = {
10
+ variants: VariantSelection<V>;
11
+ className: string;
12
+ };
13
+ export declare type VariantSelection<V extends Variants> = {
14
+ [VariantName in keyof V]?: StringToBool<keyof V[VariantName]>;
15
+ };
16
+ declare type StringToBool<T> = T extends "true" | "false" ? boolean : T;
17
+ export declare function variants<V extends Variants>({ base, variants, compoundVariants, defaultVariants, }: VariantsConfig<V>): (props: VariantSelection<V>) => string;
18
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export function variants({ base, variants, compoundVariants, defaultVariants, }) {
2
+ return (props) => {
3
+ const res = [base];
4
+ const getSelected = (name) => { var _a; return (_a = props[name]) !== null && _a !== void 0 ? _a : defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[name]; };
5
+ for (let name in variants) {
6
+ const selected = getSelected(name);
7
+ const options = variants[name];
8
+ if (selected)
9
+ res.push(options[selected]);
10
+ }
11
+ for (let { variants, className } of compoundVariants !== null && compoundVariants !== void 0 ? compoundVariants : []) {
12
+ const isSelected = (name) => getSelected(name) == variants[name];
13
+ const allSelected = Object.keys(variants).every(isSelected);
14
+ if (allSelected)
15
+ res.push(className);
16
+ }
17
+ return res.filter(Boolean).join(" ");
18
+ };
19
+ }
package/lib/react.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { ComponentProps, ElementType, ForwardRefExoticComponent, PropsWithoutRef } from "react";
2
+ import { Variants, VariantsConfig, VariantSelection } from ".";
3
+ export declare type VariantProps<T> = T extends (props: infer VariantSelection) => any ? VariantSelection : never;
4
+ export declare function variantProps<V extends Variants>(config: VariantsConfig<V>): <T extends VariantSelection<V>>(props: T) => {
5
+ className: string;
6
+ } & Omit<T, keyof V>;
7
+ declare type StyledComponent<P extends ElementType, V extends Variants> = ForwardRefExoticComponent<PropsWithoutRef<ComponentProps<P> & VariantSelection<V>> & React.RefAttributes<P>>;
8
+ export declare function styled<P extends ElementType, V extends Variants>(type: P, config: VariantsConfig<V>): StyledComponent<P, V>;
9
+ export {};
package/lib/react.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createElement, forwardRef, } from "react";
2
+ import { variants } from ".";
3
+ export function variantProps(config) {
4
+ const mkClass = variants(config);
5
+ return (props) => {
6
+ const className = mkClass(props);
7
+ const result = { className };
8
+ for (let prop in props) {
9
+ if (config.variants && !(prop in config.variants)) {
10
+ result[prop] = props[prop];
11
+ }
12
+ }
13
+ return result;
14
+ };
15
+ }
16
+ export function styled(type, config) {
17
+ const styledProps = variantProps(config);
18
+ return forwardRef((props, ref) => createElement(type, { ...styledProps(props), ref }));
19
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "classname-variants",
3
+ "version": "1.0.0",
4
+ "description": "Variant API for plain class names",
5
+ "author": "Felix Gnass <fgnass@gmail.com>",
6
+ "license": "MIT",
7
+ "repository": "fgnass/classname-variants",
8
+ "type": "module",
9
+ "main": "lib/index.js",
10
+ "types": "lib/index.d.ts",
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "npx vite"
14
+ },
15
+ "keywords": [
16
+ "tailwind",
17
+ "css",
18
+ "classname",
19
+ "variants",
20
+ "react"
21
+ ],
22
+ "devDependencies": {
23
+ "@types/react": "^17.0.39",
24
+ "react": "^17.0.2",
25
+ "react-dom": "^17.0.2",
26
+ "typescript": "^4.5.5"
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "internal": true,
3
+ "module": "../lib/react.js",
4
+ "types": "../lib/react.d.ts"
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ export type VariantDefinitions = Record<string, string>;
2
+
3
+ export type Variants = Record<string, VariantDefinitions>;
4
+
5
+ export type VariantsConfig<V extends Variants> = {
6
+ base?: string;
7
+ variants?: V;
8
+ compoundVariants?: CompoundVariant<V>[];
9
+ defaultVariants?: VariantSelection<V>;
10
+ };
11
+
12
+ export type CompoundVariant<V extends Variants> = {
13
+ variants: VariantSelection<V>;
14
+ className: string;
15
+ };
16
+
17
+ export type VariantSelection<V extends Variants> = {
18
+ [VariantName in keyof V]?: StringToBool<keyof V[VariantName]>;
19
+ };
20
+
21
+ type StringToBool<T> = T extends "true" | "false" ? boolean : T;
22
+
23
+ export function variants<V extends Variants>({
24
+ base,
25
+ variants,
26
+ compoundVariants,
27
+ defaultVariants,
28
+ }: VariantsConfig<V>) {
29
+ return (props: VariantSelection<V>) => {
30
+ const res = [base];
31
+
32
+ const getSelected = (name: string) =>
33
+ props[name] ?? defaultVariants?.[name];
34
+
35
+ for (let name in variants) {
36
+ const selected = getSelected(name);
37
+ const options: any = variants[name];
38
+ if (selected) res.push(options[selected]);
39
+ }
40
+ for (let { variants, className } of compoundVariants ?? []) {
41
+ const isSelected = (name: string) => getSelected(name) == variants[name];
42
+ const allSelected = Object.keys(variants).every(isSelected);
43
+ if (allSelected) res.push(className);
44
+ }
45
+ return res.filter(Boolean).join(" ");
46
+ };
47
+ }
package/src/react.ts ADDED
@@ -0,0 +1,46 @@
1
+ import {
2
+ ComponentProps,
3
+ createElement,
4
+ ElementType,
5
+ forwardRef,
6
+ ForwardRefExoticComponent,
7
+ PropsWithoutRef,
8
+ } from "react";
9
+
10
+ import { Variants, variants, VariantsConfig, VariantSelection } from ".";
11
+
12
+ export type VariantProps<T> = T extends (props: infer VariantSelection) => any
13
+ ? VariantSelection
14
+ : never;
15
+
16
+ export function variantProps<V extends Variants>(config: VariantsConfig<V>) {
17
+ const mkClass = variants(config);
18
+ return <T extends VariantSelection<V>>(props: T) => {
19
+ const className = mkClass(props);
20
+ const result: any = { className };
21
+ for (let prop in props) {
22
+ if (config.variants && !(prop in config.variants)) {
23
+ result[prop] = props[prop];
24
+ }
25
+ }
26
+ return result as { className: string } & Omit<T, keyof V>;
27
+ };
28
+ }
29
+
30
+ type StyledComponent<
31
+ P extends ElementType,
32
+ V extends Variants
33
+ > = ForwardRefExoticComponent<
34
+ PropsWithoutRef<ComponentProps<P> & VariantSelection<V>> &
35
+ React.RefAttributes<P>
36
+ >;
37
+
38
+ export function styled<P extends ElementType, V extends Variants>(
39
+ type: P,
40
+ config: VariantsConfig<V>
41
+ ): StyledComponent<P, V> {
42
+ const styledProps = variantProps(config);
43
+ return forwardRef<P, ComponentProps<P> & VariantSelection<V>>((props, ref) =>
44
+ createElement(type, { ...styledProps(props), ref })
45
+ );
46
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "lib": ["ESNext", "dom"],
5
+ "jsx": "react",
6
+ "module": "ESNext",
7
+ "moduleResolution": "node",
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "isolatedModules": true,
13
+ "outDir": "lib",
14
+ "declaration": true
15
+ },
16
+ "include": ["./src"]
17
+ }