Replies: 2 comments
-
I was thinking about this a little more and thought of something that maybe is crazy. It would allow easy switching of styles even at runtime with a prop or context. For example <Button styleName="new-york">test</Button> Looking at the cli code I believe we could write a transform to update these style files if new styles are added/removed. button.tsx import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import styles from "./button.styles"
function buttonVariants(style?: keyof typeof styles) {
if (style && style in styles) {
return styles[style].variants
}
return Object.values(styles)[0].variants
}
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof styles.default.variants> {
asChild?: boolean
styleName?: keyof typeof styles
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, styleName, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants(styleName)({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants } button.styles.ts import { cva } from "class-variance-authority"
const styles = {
default: {
variants: cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
componentStyle: {
default: "",
"new-york": "",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
),
},
"new-york": {
variants: cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
),
},
}
export default styles |
Beta Was this translation helpful? Give feedback.
-
Ok, one more idea. This one would even support people who want css modules as a style. A little bit bigger of a change, but adds a lot more flexibility. ui/shadButton.tsx import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import styles from "@/registry/styles/shadButtonStyles";
function getStyle(style?: keyof typeof styles) {
if (style && style in styles) {
return styles[style];
}
return Object.values(styles)[0];
}
function buttonVariants(style?: keyof typeof styles) {
return getStyle(style).getStyling ?? (() => {});
}
interface StyleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null;
size?: "sm" | "default" | "lg" | "icon" | null;
}
export interface ShadButtonProps extends StyleProps {
asChild?: boolean;
styleName?: keyof typeof styles;
}
export interface ShadButtonStyle {
getStyling?: (props: StyleProps) => { style?: React.CSSProperties; className: string };
getProps?: (props: StyleProps) => any;
}
const ShadButton = React.forwardRef<HTMLButtonElement, ShadButtonProps>(
({ asChild = false, styleName, style, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const componentStyle = getStyle(styleName);
return <Comp ref={ref} {...componentStyle.getStyling?.({ className, style, ...props })} {...componentStyle.getProps?.(props)} />;
}
);
ShadButton.displayName = "ShadButton";
export { buttonVariants, ShadButton }; /styles/shadButtonStyles.ts import shadcn_default from "./default/shadButton";
import module from "./module/shadButton";
import shadcn_new_york from "./new-york/shadButton";
const style = {
default: shadcn_default,
"new-york": shadcn_new_york,
module,
};
export default style; styles/default/shadButton.ts import { cn } from "@/lib/utils";
import { ShadButtonStyle } from "@/registry/ui/shadButton";
import { cva } from "class-variance-authority";
const variants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
const style: ShadButtonStyle = {
getStyling({ variant, size, className, ...props }) {
return {
className: cn(variants({ variant, size, className })),
...props,
};
},
};
export default style; styles/new-york/shadButton.tsx import { cn } from "@/lib/utils";
import { ShadButtonStyle } from "@/registry/ui/shadButton";
import { cva } from "class-variance-authority";
const variants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
const style: ShadButtonStyle = {
getStyling({ variant, size, className, ...props }) {
return {
className: cn(variants({ variant, size, className })),
...props,
};
},
};
export default style; styles/module/shadButton.ts import { ShadButtonStyle } from "@/registry/ui/shadButton";
import moduleStyles from "./shadButton.module.css";
const style: ShadButtonStyle = {
getStyling({ className }) {
return {
className: `${moduleStyles.Button} ${className ? className : ""}`,
};
},
getProps({ variant, size, className, ...props }) {
return {
"data-size": size,
"data-variant": variant,
...props,
};
},
};
export default style; /styles/module/shadButton.module.css .Button {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
outline: none;
background-color: hsl(var(--primary));
color: red;
border: 0.0375rem solid hsl(var(--border) / 0.8);
transition: background-color 0.2s ease;
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
max-width: 20rem;
}
.Button:hover {
cursor: pointer;
background-color: hsl(var(--primary) / 0.8);
transition: background-color 0.2s ease;
}
.Button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px hsl(var(--foreground) / 0.50);
}
.Button:disabled {
pointer-events: none;
opacity: 0.5;
}
.Button:disabled:focus-visible {
outline: none;
box-shadow: none;
}
.Button[data-variant='destructive'] {
background-color: hsl(var(--destructive));
color: hsl(var(--destructive-foreground));
transition: background-color 0.2s ease;
}
.Button[data-variant='destructive']:hover {
background-color: hsl(var(--destructive) / 0.9);
}
.Button[data-variant='outline'] {
border: 1px solid hsl(var(--border));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: background-color 0.2s ease, color 0.2s ease;
}
.Button[data-variant='outline']:hover {
cursor: pointer;
background-color: hsl(var(--foreground) / 0.05);
color: hsl(var(--accent-foreground));
}
.Button[data-variant='secondary'] {
background-color: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
transition: background-color 0.2s ease;
}
.Button[data-variant='secondary']:hover {
background-color: hsl(var(--secondary) / 0.9);
}
.Button[data-variant='ghost'] {
background-color: transparent;
border: 1px solid transparent;
color: hsl(var(--foreground));
display: flex;
align-items: center;
}
.Button[data-variant='ghost']:hover {
background-color: hsl(var(--foreground) / 0.1);
}
.Button[data-variant='link'] {
color: hsl(var(--foreground));
background-color: transparent;
text-decoration: none;
transition: text-decoration 0.2s ease;
border: none;
padding: 0;
height: max-content;
width: max-content;
}
.Button[data-variant='link']:hover {
color: hsl(var(--primary));
text-decoration-color: hsl(var(--foreground));
}
.Button[data-variant='icon'] {
background-color: hsl(var(--foreground));
border: none;
color: hsl(var(--background));
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
width: 2rem;
padding: 0;
border-radius: 9999px;
}
.Button[data-variant='icon']:hover {
background-color: hsl(var(--accent) / 0.8);
}
/* sizing */
.Button[data-size='sm'] {
height: 2.25rem;
border-radius: 0.375rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.Button[data-size='lg'] {
height: 2.75rem;
border-radius: 0.375rem;
padding-left: 2rem;
padding-right: 2rem;
}
.Button[data-size='icon'] {
height: 2.5rem;
width: 2.5rem;
} |
Beta Was this translation helpful? Give feedback.
-
I was looking at styles and it seems like there is quite a bit of duplicate code for mostly some small changes for the 2 styles. (Originally I started looking because I wanted to set up my own registry for a few components and it looks like I would have to also have the default and new york style even though I'm not currently planning to support 2 styles to make the cli work).
Have you considered extracting out the style bits into a separate file for each component? Then you wouldn't have to have separate directories for styles? It also seems like that would be beneficial with cli for updates or changing styles. If a bug fix was needed in a component it wouldn't overwrite the user's style customizations. The cli could also independently download new styles for a component and just replace the style file.
I'll try to give a quick example of what I'm talking about.
button.tsx
button.style.ts
When the cli is downloading a component it could pick the right style file in the registry and just write it out to this style file.
Beta Was this translation helpful? Give feedback.
All reactions