Skip to content

Commit cc6f96f

Browse files
committed
wip
1 parent a54b170 commit cc6f96f

File tree

11 files changed

+884
-2986
lines changed

11 files changed

+884
-2986
lines changed

README.md

Lines changed: 0 additions & 539 deletions
This file was deleted.

package.json

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,21 @@
4343
},
4444
"homepage": "https://github.com/jackardios/react-tailwind-variants#readme",
4545
"devDependencies": {
46-
"@changesets/cli": "^2.26.0",
47-
"@testing-library/dom": "^10.4.0",
48-
"@testing-library/jest-dom": "^6.6.3",
46+
"@changesets/cli": "^2.29.6",
47+
"@testing-library/dom": "^10.4.1",
48+
"@testing-library/jest-dom": "^6.8.0",
4949
"@testing-library/react": "^16.3.0",
50-
"@types/react": "^19.1.0",
51-
"@types/react-dom": "^19.1.0",
50+
"@types/react": "^19.1.11",
51+
"@types/react-dom": "^19.1.7",
5252
"@vitejs/plugin-react": "^4.7.0",
5353
"jsdom": "^26.1.0",
54-
"react-dom": "^19.1.0",
54+
"react-dom": "^19.1.1",
5555
"tsup": "^8.5.0",
56-
"typescript": "^5.8.3",
56+
"typescript": "^5.9.2",
5757
"vitest": "^3.2.4"
5858
},
59-
"dependencies": {
60-
"@radix-ui/react-slot": "^1.2.3"
61-
},
6259
"peerDependencies": {
63-
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
64-
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0",
65-
"tailwind-merge": "^1.10.0 || ^2.0.0 || ^3.0.0"
60+
"react": "^19.0",
61+
"react-dom": "^19.0"
6662
}
6763
}

pnpm-lock.yaml

Lines changed: 423 additions & 515 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cx.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/index.ts

Lines changed: 302 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,305 @@
1-
export { type CxOptions, type CxReturn, cx } from './cx';
2-
3-
export {
4-
type VariantsConfig,
5-
type VariantsSchema,
6-
type VariantOptions,
7-
variants,
8-
} from './variants';
9-
10-
export {
11-
type StyledComponent,
12-
type VariantPropsOf,
13-
type VariantsConfigOf,
14-
variantProps,
15-
extractVariantsConfig,
16-
styled,
17-
} from './react';
1+
import {
2+
cloneElement,
3+
createElement,
4+
HTMLAttributes,
5+
isValidElement,
6+
ReactNode,
7+
Ref,
8+
type ComponentPropsWithRef,
9+
type ElementType,
10+
type ReactElement,
11+
} from 'react';
12+
import {
13+
getRefProperty,
14+
hasOwnProperty,
15+
mergeProps,
16+
useMergeRefs,
17+
} from './utils';
18+
19+
type PickRequiredKeys<T> = {
20+
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
21+
}[keyof T];
22+
type OmitByValue<T, Value> = {
23+
[P in keyof T as T[P] extends Value ? never : P]: T[P];
24+
};
25+
type StringToBoolean<T> = T extends 'true' | 'false' ? boolean : T;
26+
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] };
27+
28+
// ----------------------------------------------------------------------
29+
30+
export type ClassNameValue = string | null | undefined | ClassNameValue[];
31+
32+
/**
33+
* Definition of the available variants and their options.
34+
* @example
35+
* {
36+
* color: {
37+
* white: "bg-white"
38+
* green: "bg-green-500",
39+
* },
40+
* size: {
41+
* small: "text-xs",
42+
* large: "text-lg"
43+
* }
44+
* }
45+
*/
46+
export type VariantsSchema = Record<string, Record<string, ClassNameValue>>;
47+
48+
export type VariantsConfig<V extends VariantsSchema> = {
49+
base?: ClassNameValue;
50+
variants?: V;
51+
defaultVariants?: keyof V extends never
52+
? Record<string, never>
53+
: Partial<Variants<V>>;
54+
compoundVariants?: keyof V extends never ? never[] : CompoundVariant<V>[];
55+
};
56+
57+
/**
58+
* Rules for class names that are applied for certain variant combinations.
59+
*/
60+
interface CompoundVariant<V extends VariantsSchema> {
61+
variants: Partial<VariantsMulti<V>>;
62+
className: ClassNameValue;
63+
}
64+
65+
type Variants<V extends VariantsSchema> = {
66+
[Variant in keyof V]: StringToBoolean<keyof V[Variant]>;
67+
};
68+
69+
type VariantsMulti<V extends VariantsSchema> = {
70+
[Variant in keyof V]:
71+
| StringToBoolean<keyof V[Variant]>
72+
| StringToBoolean<keyof V[Variant]>[];
73+
};
74+
75+
/**
76+
* Only the boolean variants, i.e. ones that have "true" or "false" as options.
77+
*/
78+
type BooleanVariants<
79+
C extends VariantsConfig<V>,
80+
V extends VariantsSchema = NonNullable<C['variants']>
81+
> = {
82+
[Variant in keyof V as V[Variant] extends { true: any } | { false: any }
83+
? Variant
84+
: never]: V[Variant];
85+
};
86+
87+
/**
88+
* Only the variants for which a default options is set.
89+
*/
90+
type DefaultVariants<
91+
C extends VariantsConfig<V>,
92+
V extends VariantsSchema = NonNullable<C['variants']>
93+
> = {
94+
[Variant in keyof V as Variant extends keyof OmitByValue<
95+
C['defaultVariants'],
96+
undefined
97+
>
98+
? Variant
99+
: never]: V[Variant];
100+
};
101+
102+
/**
103+
* Names of all optional variants, i.e. booleans or ones with default options.
104+
*/
105+
type OptionalVariantNames<
106+
C extends VariantsConfig<V>,
107+
V extends VariantsSchema = NonNullable<C['variants']>
108+
> = keyof BooleanVariants<C, V> | keyof DefaultVariants<C, V>;
109+
110+
export type VariantOptions<
111+
C extends VariantsConfig<V>,
112+
V extends VariantsSchema = NonNullable<C['variants']>
113+
> = keyof V extends never
114+
? {}
115+
: Required<Omit<Variants<V>, OptionalVariantNames<C, V>>> &
116+
Partial<Pick<Variants<V>, OptionalVariantNames<C, V>>>;
117+
118+
/**
119+
* Render prop type.
120+
* @template P Props
121+
* @example
122+
* const children: RenderProp = (props) => <div {...props} />;
123+
*/
124+
export type RenderProp<P = HTMLAttributes<any> & { ref?: Ref<any> }> = (
125+
props: P
126+
) => ReactNode;
127+
128+
// ----------------------------------------------------------------------
129+
130+
export interface VariantFactoryOptions {
131+
onClassesMerged?: (className: string) => string;
132+
}
133+
134+
type VariantsResolverArgs<P> = PickRequiredKeys<P> extends never
135+
? [props?: P]
136+
: [props: P];
137+
138+
export function defineConfig(options?: VariantFactoryOptions) {
139+
const { onClassesMerged } = options ?? {};
140+
141+
function mergeClassNames(...classNames: ClassNameValue[]): string {
142+
// @ts-ignore
143+
const className = classNames.flat(Infinity).filter(Boolean).join(' ');
144+
145+
return onClassesMerged ? onClassesMerged(className) : className;
146+
}
147+
148+
function variants<
149+
C extends VariantsConfig<V>,
150+
V extends VariantsSchema = NonNullable<C['variants']>
151+
>(config: Simplify<C>) {
152+
const { base, variants, compoundVariants, defaultVariants } = config;
153+
154+
if (!('variants' in config) || !config.variants) {
155+
return (props?: { className?: ClassNameValue }) =>
156+
mergeClassNames(base, props?.className);
157+
}
158+
159+
function isBooleanVariant(name: keyof V) {
160+
const variant = (variants as V)?.[name];
161+
return variant && ('false' in variant || 'true' in variant);
162+
}
163+
164+
type ResolveProps = VariantOptions<C, V> & {
165+
className?: ClassNameValue;
166+
};
167+
168+
return function (...[props]: VariantsResolverArgs<ResolveProps>) {
169+
const result = [base];
170+
171+
const getSelectedVariant = (name: keyof V) =>
172+
(props as any)?.[name] ??
173+
defaultVariants?.[name] ??
174+
(isBooleanVariant(name) ? false : undefined);
175+
176+
for (let name in variants) {
177+
const selected = getSelectedVariant(name);
178+
if (selected !== undefined) result.push(variants[name]?.[selected]);
179+
}
180+
181+
for (let { variants, className } of compoundVariants ?? []) {
182+
function isSelectedVariant(name: string) {
183+
const selected = getSelectedVariant(name);
184+
const cvSelector = variants[name];
185+
186+
return Array.isArray(cvSelector)
187+
? cvSelector.includes(selected)
188+
: selected === cvSelector;
189+
}
190+
191+
if (Object.keys(variants).every(isSelectedVariant)) {
192+
result.push(className);
193+
}
194+
}
195+
196+
if (props?.className) {
197+
result.push(props.className);
198+
}
199+
200+
return mergeClassNames(result);
201+
};
202+
}
203+
204+
function variantPropsResolver<
205+
C extends VariantsConfig<V>,
206+
V extends VariantsSchema = NonNullable<C['variants']>
207+
>(config: Simplify<C>) {
208+
const variantsResolver = variants<C, V>(config);
209+
210+
type Props = VariantOptions<C, V> & {
211+
className?: string;
212+
};
213+
214+
return function <P extends Props>(props: P) {
215+
const result = { ...props } as { className: string } & Omit<P, keyof V>;
216+
const onlyVariantProps = { className: result.className } as Props;
217+
218+
if (config.variants) {
219+
for (const variantKey in config.variants) {
220+
if (
221+
hasOwnProperty(config.variants, variantKey) &&
222+
hasOwnProperty(result, variantKey)
223+
) {
224+
(onlyVariantProps as any)[variantKey] = result[variantKey];
225+
delete result[variantKey];
226+
}
227+
}
228+
}
229+
230+
result.className = variantsResolver(onlyVariantProps);
231+
232+
return result;
233+
};
234+
}
235+
236+
function variantComponent<
237+
T extends ElementType,
238+
C extends VariantsConfig<V> & {
239+
withoutRenderProp?: boolean;
240+
},
241+
V extends VariantsSchema = NonNullable<C['variants']>
242+
>(type: T, config: Simplify<C>) {
243+
const { withoutRenderProp, ...variantsConfig } = config;
244+
type OnlyVariantsConfig = Omit<C, 'withoutRenderProp'>;
245+
type VariantOptionsOfConfig = VariantOptions<OnlyVariantsConfig, V>;
246+
type BaseProps = VariantOptionsOfConfig &
247+
Omit<ComponentPropsWithRef<T>, keyof VariantOptionsOfConfig>;
248+
249+
const resolveProps = variantPropsResolver<OnlyVariantsConfig, V>(
250+
variantsConfig
251+
);
252+
253+
if (typeof type !== 'string' || withoutRenderProp) {
254+
return (props: BaseProps) => {
255+
return createElement(type, resolveProps(props));
256+
};
257+
}
258+
259+
const component = (
260+
props: Simplify<
261+
{
262+
/**
263+
* Allows the component to be rendered as a different HTML element or React
264+
* component. The value can be a React element or a function that takes in the
265+
* original component props and gives back a React element with the props
266+
* merged.
267+
*/
268+
render?:
269+
| RenderProp<
270+
{
271+
className: string;
272+
} & Omit<BaseProps, keyof VariantOptionsOfConfig>
273+
>
274+
| ReactElement<any>;
275+
} & Omit<BaseProps, 'render'>
276+
>
277+
) => {
278+
const { render, ...rest } = props;
279+
const mergedRef = useMergeRefs((rest as any).ref, getRefProperty(render));
280+
const resolvedProps = resolveProps(rest as BaseProps);
281+
282+
if (render) {
283+
if (isValidElement<any>(render)) {
284+
const renderProps = { ...render.props, ref: mergedRef };
285+
return cloneElement(render, mergeProps(resolvedProps, renderProps));
286+
} else {
287+
return render(resolvedProps) as ReactElement;
288+
}
289+
}
290+
291+
return createElement(type, resolvedProps);
292+
};
293+
294+
return component;
295+
}
296+
297+
return {
298+
variants,
299+
variantPropsResolver,
300+
variantComponent,
301+
} as const;
302+
}
18303

19304
/**
20305
* No-op function to mark template literals as tailwind strings.

0 commit comments

Comments
 (0)