Skip to content

Instantly share code, notes, and snippets.

@nikitatrifan
Last active October 4, 2024 22:29
Show Gist options
  • Save nikitatrifan/8aaa3054c0355ffe0c173d22991d5e4a to your computer and use it in GitHub Desktop.
Save nikitatrifan/8aaa3054c0355ffe0c173d22991d5e4a to your computer and use it in GitHub Desktop.
createStyled for Restyle to add support for variants and compound variants
import { createStyled } from "./createStyled";
export const Button = createStyled({
as: "button",
styles: {
$$textColor: "$textColors$primary",
$$iconColor: "$iconColors$primary",
$$backgroundColor: "$colors$dark10",
$$backgroundColorOnHover: "$colors$dark20",
$$backgroundColorOnActive: "$colors$dark5",
$$backgroundColorOnSelected: "$$backgroundColorOnHover",
$$borderColorOnFocus: "$borderColors$selected",
fontFamily: "$primary",
display: "flex",
alignItems: "center",
whiteSpace: "pre",
justifyContent: "center",
color: "$$textColor",
fontSize: "$fontSizes$product$link",
lineHeight: "$lineHeight$product$link",
fontWeight: "$fontWeight$product$link",
borderRadius: "$1",
backgroundColor: "$$backgroundColor",
transition: "background 0.15s ease-out",
"&:hover": {
backgroundColor: "$$backgroundColorOnHover",
},
"&:active": {
backgroundColor: "$$backgroundColorOnActive",
},
"&:focus.focus-visible": {
boxShadow: "0 0 0 $space$1 $$borderColorOnFocus",
},
"& svg": {
color: "$$iconColor",
},
},
variants: {
state: {
hovered: {
backgroundColor: "$$backgroundColorOnHover",
},
active: {
backgroundColor: "$$backgroundColorOnActive",
},
focused: {
boxShadow: "0 0 0 $space$1 $$borderColorOnFocus",
},
},
variant: {
primary: {
$$textColor: "$colors$white",
$$iconColor: "$colors$white",
$$backgroundColor: "$colors$dark95",
$$backgroundColorOnHover: "$colors$black",
$$backgroundColorOnActive: "$colors$dark85",
$$borderColorOnFocus: "$borderColors$selected",
},
secondary: {
$$textColor: "$textColors$primary",
$$iconColor: "$iconColors$primary",
$$backgroundColor: "$colors$dark10",
$$backgroundColorOnHover: "$colors$dark20",
$$backgroundColorOnActive: "$colors$dark5",
$$borderColorOnFocus: "$borderColors$selected",
},
branded: {
$$textColor: "$textColors$primary",
$$iconColor: "$iconColors$primary",
$$backgroundColor: "$colors$brand95",
$$backgroundColorOnHover: "$colors$brand85",
$$backgroundColorOnActive: "$colors$brand75",
$$borderColorOnFocus: "$borderColors$selected",
},
ghost: {
$$textColor: "$textColors$secondary",
$$iconColor: "$iconColors$primary",
$$backgroundColor: "$colors$dark0",
$$backgroundColorOnHover: "$colors$dark10",
$$backgroundColorOnActive: "$colors$dark5",
$$backgroundColorOnSelected: "$colors$dark10",
$$borderColorOnFocus: "$borderColors$selected",
},
menu: {
fontSize: "$fontSizes$product$body",
lineHeight: "$lineHeight$product$body",
$$textColor: "$textColors$body",
$$iconColor: "$iconColors$primary",
$$backgroundColor: "$colors$brand0",
$$backgroundColorOnHover: "$colors$brand25",
$$backgroundColorOnActive: "$colors$brand15",
$$borderColorOnFocus: "$borderColors$selected",
},
},
icon: {
left: {
display: "flex",
columnGap: "$space$1",
flexDirection: "row",
justifyContent: "start",
},
right: {
display: "flex",
flexDirection: "row",
columnGap: "$space$1",
justifyContent: "space-between",
},
both: {
display: "flex",
flexDirection: "row",
columnGap: "$space$1",
justifyContent: "space-between",
"& [data-text]": {
flex: 1,
textAlign: "left",
},
},
only: {},
},
size: {
huge: {
padding: "$12 $42",
textTransform: "uppercase",
fontSize: "$fontSizes$studio$button",
lineHeight: "$lineHeight$studio$button",
fontWeight: "$fontWeight$studio$button",
},
large: {
padding: "$6 $8",
},
medium: {
padding: "$3 $8",
},
small: {
padding: "$2 $4",
},
},
disabled: {
true: {
pointerEvents: "none",
cursor: "not-allowed",
},
false: {},
},
selected: {
true: {
backgroundColor: "$$backgroundColorOnSelected",
},
false: {},
},
},
compounds: [
{
variant: "primary",
disabled: "true",
css: {
$$backgroundColor: "$colors$dark30",
$$textColor: "$colors$light95",
$$iconColor: "$iconColors$disabled",
},
},
{
variant: "secondary",
disabled: "false",
css: {
$$backgroundColor: "$colors$dark5",
$$textColor: "$textColors$disabled",
$$iconColor: "$iconColors$disabled",
},
},
{
variant: "ghost",
disabled: "false",
css: {
$$textColor: "$textColors$disabled",
$$iconColor: "$iconColors$disabled",
},
},
{
icon: "left",
size: "medium",
css: {
paddingLeft: "$3",
paddingTop: "$2",
paddingBottom: "$2",
},
},
{
icon: "right",
size: "medium",
css: {
paddingRight: "$3",
paddingTop: "$2",
paddingBottom: "$2",
},
},
{
icon: "both",
size: "medium",
css: {
padding: "$2 $3",
},
},
{
icon: "left",
size: "large",
css: {
paddingLeft: "$3",
paddingTop: "$4",
paddingBottom: "$4",
},
},
{
icon: "right",
size: "large",
css: {
paddingRight: "$3",
paddingTop: "$4",
paddingBottom: "$4",
},
},
{
icon: "both",
size: "large",
css: {
padding: "$4 $3",
},
},
{
icon: "only",
size: "medium",
css: {
padding: "$2 $3",
},
},
{
icon: "only",
size: "small",
css: {
padding: "0",
},
},
],
defaults: {
variant: "secondary",
size: "medium",
},
});
import { css, CSSObject } from "restyle";
import { ComponentPropsWithRef, useMemo, ElementType } from "react";
export const createStyled = <
As extends ElementType,
Styles extends CSSObject,
Variants extends Record<string, Record<string, CSSObject>>,
Compounds extends Array<VariantProps<Variants> & { css: CSSObject }>,
>(args: {
as: As;
styles: Styles;
variants: Variants;
defaults?: VariantProps<Variants>;
compounds?: Compounds;
}) => {
const stylesByVariant = new Map<
string,
Map<string, ReturnType<typeof css>>
>();
for (const variantGroup in args.variants) {
const variants = args.variants[variantGroup];
for (const variantName in variants) {
const variantStyles = variants[variantName];
if (!stylesByVariant.has(variantGroup)) {
stylesByVariant.set(variantGroup, new Map());
}
const variantGroupStyles = stylesByVariant.get(variantGroup)!;
variantGroupStyles.set(variantName, css(variantStyles));
}
}
const compoundsWithStyles = args.compounds?.map(
({ css: cssObject, ...selector }) => {
return { ...selector, styles: css(cssObject) };
},
);
const [defaultStylesClassName, DefaultStyles] = css(args.styles);
const Component = args.as;
type Props = ComponentPropsWithRef<As> & VariantProps<Variants>;
return ({ children, ...props }: Props) => {
if (args.defaults) {
// patch props with defaults, if provided
for (const key in args.defaults) {
const propName = key as keyof VariantProps<Variants>;
const shouldPatch = !(propName in props);
if (shouldPatch) {
// @ts-expect-error as props are read only
props[propName] = args.defaults[propName];
}
}
}
const matchingVariants = new Map<string, ReturnType<typeof css>>();
for (const variantKey of stylesByVariant.keys()) {
if (variantKey in props) {
const variantGroup = stylesByVariant.get(variantKey)!;
const variantProps = variantGroup.get(props[variantKey])!;
matchingVariants.set(variantKey, variantProps);
delete props[variantKey];
}
}
let variantClassNames = "";
let variantStyles = [];
for (const [className, Styled] of matchingVariants.values()) {
variantClassNames += `${className} `;
variantStyles.push(<Styled key={className} />);
}
const compoundVariants = useMemo(() => {
let compoundVariantsClassNames = "";
let compoundVariantStyles = [];
if (compoundsWithStyles) {
for (const compoundVariant of compoundsWithStyles) {
let matchesCompoundVariant = null;
for (const cvk in compoundVariant) {
const compoundVariantKey = cvk as keyof typeof compoundVariant;
if (
matchesCompoundVariant === false ||
["styles", "css"].includes(compoundVariantKey)
) {
continue;
}
if (matchingVariants.has(compoundVariantKey)) {
// a key found in compound variant also exists in matching variants
// let's compare if this is a key-value match as well
matchesCompoundVariant =
compoundVariant[compoundVariantKey] ===
matchingVariants.get(compoundVariantKey);
} else {
matchesCompoundVariant = true;
}
}
if (matchesCompoundVariant) {
// match identified, apply its class name and render style rules
const [className, Styled] = compoundVariant.styles;
compoundVariantsClassNames += `${className} `;
compoundVariantStyles.push(<Styled key={className} />);
}
}
}
return {
styles: compoundVariantStyles,
className: compoundVariantsClassNames,
};
}, [variantClassNames]);
return (
<>
<Component
className={cs([
props.className,
defaultStylesClassName,
variantClassNames,
compoundVariants.className,
])}
{...props}
>
{children}
</Component>
<DefaultStyles />
{variantStyles}
{compoundVariants.styles}
</>
);
};
};
const cs = (classes: Array<string | undefined>) => {
let className = "";
for (const cl of classes) {
if (typeof cl === "string") {
className += `${cl} `;
}
}
return className;
};
type VariantGroups<V> = Extract<keyof V, string>;
type VariantNames<V, Group extends VariantGroups<V>> = Extract<
keyof V[Group],
string | boolean | number
>;
type VariantProps<Variants> = {
[Group in VariantGroups<Variants>]?: VariantNames<Variants, Group>;
};
function Form() {
return (
<Button variant="primary" disabled={!valid}>
Submit
</Button>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment