Skip to content

Commit 3dfcf9e

Browse files
committed
feat: added logic to generate element styles with variants and base styles
1 parent a24884f commit 3dfcf9e

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

packages/sva/src/index.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { styles } from './';
3+
4+
describe('createVariantStyle', () => {
5+
it('should return base styles when no variants are provided', () => {
6+
const baseStyles = styles({
7+
base: { display: 'flex' },
8+
variants: {},
9+
});
10+
11+
expect(baseStyles()).toEqual({ display: 'flex' });
12+
});
13+
14+
it('should apply variant styles correctly', () => {
15+
const buttonStyles = styles({
16+
base: { display: 'flex' },
17+
variants: {
18+
visual: {
19+
solid: { backgroundColor: '#FC8181', color: 'white' },
20+
outline: { borderWidth: 1, borderColor: '#FC8181' },
21+
},
22+
size: {
23+
sm: { padding: 4, fontSize: 12 },
24+
lg: { padding: 8, fontSize: 24 },
25+
},
26+
},
27+
});
28+
29+
expect(buttonStyles({ visual: 'solid', size: 'sm' })).toEqual({
30+
display: 'flex',
31+
backgroundColor: '#FC8181',
32+
color: 'white',
33+
padding: 4,
34+
fontSize: 12,
35+
});
36+
});
37+
38+
it('should apply default variants when props are not provided', () => {
39+
const buttonStyles = styles({
40+
base: { display: 'flex' },
41+
variants: {
42+
visual: {
43+
solid: { backgroundColor: '#FC8181' },
44+
outline: { borderWidth: 1 },
45+
},
46+
},
47+
defaultVariants: {
48+
visual: 'solid',
49+
},
50+
});
51+
52+
expect(buttonStyles()).toEqual({
53+
display: 'flex',
54+
backgroundColor: '#FC8181',
55+
});
56+
});
57+
58+
it('should apply compound variants correctly', () => {
59+
const buttonStyles = styles({
60+
base: { display: 'flex' },
61+
variants: {
62+
visual: {
63+
solid: { backgroundColor: '#FC8181' },
64+
outline: { borderWidth: 1 },
65+
},
66+
size: {
67+
sm: { padding: 4 },
68+
lg: { padding: 8 },
69+
},
70+
},
71+
compoundVariants: [
72+
{
73+
variants: { visual: 'solid', size: 'lg' },
74+
style: { fontWeight: 'bold' },
75+
},
76+
],
77+
});
78+
79+
expect(buttonStyles({ visual: 'solid', size: 'lg' })).toEqual({
80+
display: 'flex',
81+
backgroundColor: '#FC8181',
82+
padding: 8,
83+
fontWeight: 'bold',
84+
});
85+
});
86+
87+
it('should override default variants with provided props', () => {
88+
const buttonStyles = styles({
89+
base: { display: 'flex' },
90+
variants: {
91+
visual: {
92+
solid: { backgroundColor: '#FC8181' },
93+
outline: { borderWidth: 1 },
94+
},
95+
},
96+
defaultVariants: {
97+
visual: 'solid',
98+
},
99+
});
100+
101+
expect(buttonStyles({ visual: 'outline' })).toEqual({
102+
display: 'flex',
103+
borderWidth: 1,
104+
});
105+
});
106+
107+
it('should handle invalid variant values gracefully', () => {
108+
const buttonStyles = styles({
109+
base: { display: 'flex' },
110+
variants: {
111+
visual: {
112+
solid: { backgroundColor: '#FC8181' },
113+
outline: { borderWidth: 1 },
114+
},
115+
},
116+
});
117+
118+
expect(buttonStyles({ visual: 'nonexistent' })).toEqual({
119+
display: 'flex',
120+
});
121+
});
122+
});

packages/sva/src/index.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* ISC License
3+
* Copyright 2025 Javier Diaz Chamorro
4+
*
5+
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
6+
* granted, provided that the above copyright notice and this permission notice appear in all copies.
7+
*
8+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING
9+
* ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
10+
* DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
11+
* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
12+
* OR PERFORMANCE OF THIS SOFTWARE.
13+
*/
14+
15+
// Example of usage:
16+
// const buttonStyles = style({
17+
// base: {
18+
// display: 'flex',
19+
// },
20+
// variants: {
21+
// visual: {
22+
// solid: { backgroundColor: '#FC8181', color: 'white' },
23+
// outline: { borderWidth: 1, borderColor: '#FC8181' },
24+
// },
25+
// size: {
26+
// sm: { padding: 4, fontSize: 12 },
27+
// lg: { padding: 8, fontSize: 24 },
28+
// },
29+
// },
30+
// // Optional: Add compound variants for specific combinations
31+
// compoundVariants: [
32+
// {
33+
// variants: { visual: 'solid', size: 'lg' },
34+
// style: { fontWeight: 'bold' },
35+
// },
36+
// ],
37+
// default: {
38+
// visual: 'solid',
39+
// size: 'sm',
40+
// }
41+
// });
42+
43+
import { ImageStyle, TextStyle, ViewStyle } from 'react-native';
44+
45+
// Types for variant styles
46+
type StyleObject = ViewStyle | TextStyle | ImageStyle;
47+
48+
// Define the VariantOptions type to ensure type safety in variant definitions
49+
type VariantOptions<V> = {
50+
[P in keyof V]: {
51+
[K in keyof V[P]]: StyleObject;
52+
};
53+
};
54+
55+
type CompoundVariant<V> = {
56+
variants: Partial<{
57+
[P in keyof V]: keyof V[P];
58+
}>;
59+
style: StyleObject;
60+
};
61+
62+
// DefaultVariants type
63+
type DefaultVariants<V> = Partial<{
64+
[P in keyof V]: keyof V[P];
65+
}>;
66+
67+
// VariantStyleConfig type
68+
type VariantStyleConfig<V extends VariantOptions<V>> = {
69+
base?: StyleObject;
70+
variants: V;
71+
compoundVariants?: CompoundVariant<V>[];
72+
defaultVariants?: DefaultVariants<V>;
73+
};
74+
75+
export type OmitUndefined<T> = T extends undefined ? never : T;
76+
77+
/**
78+
* Helper type to make all properties optional if they have a default value
79+
*/
80+
type OptionalIfHasDefault<Props, Defaults> = Omit<Props, keyof Defaults> &
81+
Partial<Pick<Props, Extract<keyof Defaults, keyof Props>>>;
82+
83+
export type VariantProps<Component extends (...args: any) => any> = Omit<
84+
OmitUndefined<Parameters<Component>[0]>,
85+
'style'
86+
>;
87+
88+
/**
89+
* Creates a function that generates styles based on variants
90+
*/
91+
export function styles<V extends VariantOptions<V>>(config: VariantStyleConfig<V>) {
92+
type VariantProps = { [P in keyof V]: keyof V[P] | (string & {}) };
93+
type DefaultProps = NonNullable<typeof config.defaultVariants>;
94+
type Props = OptionalIfHasDefault<VariantProps, DefaultProps>;
95+
96+
return (props?: Props) => {
97+
// Start with base styles
98+
let styles: StyleObject = {
99+
...(config.base || {}),
100+
};
101+
102+
// Merge default variants with provided props
103+
const mergedProps = {
104+
...(config.defaultVariants || {}),
105+
...props,
106+
} as VariantProps;
107+
108+
// Apply variant styles
109+
for (const [propName, value] of Object.entries(mergedProps) as [keyof V, any][]) {
110+
const variantGroup = config.variants[propName];
111+
if (variantGroup) {
112+
const variantValue = value || config.defaultVariants?.[propName];
113+
if (variantValue && variantGroup[variantValue as keyof typeof variantGroup]) {
114+
styles = {
115+
...styles,
116+
...variantGroup[variantValue as keyof typeof variantGroup],
117+
};
118+
}
119+
}
120+
}
121+
122+
// Apply compound variants
123+
if (config.compoundVariants) {
124+
for (const compound of config.compoundVariants) {
125+
if (
126+
Object.entries(compound.variants).every(
127+
([propName, value]: [string, unknown]) => mergedProps[propName as keyof V] === value,
128+
)
129+
) {
130+
styles = {
131+
...styles,
132+
...compound.style,
133+
};
134+
}
135+
}
136+
}
137+
138+
return styles;
139+
};
140+
}

0 commit comments

Comments
 (0)