[WEB-4934] dev: propel button implementation (#7859)
* dev: button component code refactor * dev: propel button component implementation
This commit is contained in:
parent
0ad439fa63
commit
726529044e
199 changed files with 660 additions and 290 deletions
|
|
@ -1,5 +1,22 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Button, EButtonVariant, EButtonSize } from "./button";
|
||||
import { Button } from "./button";
|
||||
import type { TButtonVariant, TButtonSizes } from "./helper";
|
||||
|
||||
const buttonVariants: TButtonVariant[] = [
|
||||
"primary",
|
||||
"accent-primary",
|
||||
"outline-primary",
|
||||
"neutral-primary",
|
||||
"link-primary",
|
||||
"danger",
|
||||
"accent-danger",
|
||||
"outline-danger",
|
||||
"link-danger",
|
||||
"tertiary-danger",
|
||||
"link-neutral",
|
||||
];
|
||||
|
||||
const buttonSizes: TButtonSizes[] = ["sm", "md", "lg", "xl"];
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Components/Button",
|
||||
|
|
@ -11,11 +28,17 @@ const meta: Meta<typeof Button> = {
|
|||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: Object.values(EButtonVariant),
|
||||
options: buttonVariants,
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: Object.values(EButtonSize),
|
||||
options: buttonSizes,
|
||||
},
|
||||
loading: {
|
||||
control: "boolean",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -31,60 +54,116 @@ export const Default: Story = {
|
|||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: EButtonVariant.PRIMARY,
|
||||
variant: "primary",
|
||||
children: "Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
export const AccentPrimary: Story = {
|
||||
args: {
|
||||
variant: EButtonVariant.SECONDARY,
|
||||
children: "Secondary Button",
|
||||
variant: "accent-primary",
|
||||
children: "Accent Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
export const OutlinePrimary: Story = {
|
||||
args: {
|
||||
variant: EButtonVariant.OUTLINE,
|
||||
children: "Outline Button",
|
||||
variant: "outline-primary",
|
||||
children: "Outline Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
export const NeutralPrimary: Story = {
|
||||
args: {
|
||||
variant: EButtonVariant.GHOST,
|
||||
children: "Ghost Button",
|
||||
variant: "neutral-primary",
|
||||
children: "Neutral Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
export const LinkPrimary: Story = {
|
||||
args: {
|
||||
variant: EButtonVariant.DESTRUCTIVE,
|
||||
children: "Destructive Button",
|
||||
variant: "link-primary",
|
||||
children: "Link Primary Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
variant: "danger",
|
||||
children: "Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const AccentDanger: Story = {
|
||||
args: {
|
||||
variant: "accent-danger",
|
||||
children: "Accent Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineDanger: Story = {
|
||||
args: {
|
||||
variant: "outline-danger",
|
||||
children: "Outline Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinkDanger: Story = {
|
||||
args: {
|
||||
variant: "link-danger",
|
||||
children: "Link Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const TertiaryDanger: Story = {
|
||||
args: {
|
||||
variant: "tertiary-danger",
|
||||
children: "Tertiary Danger Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinkNeutral: Story = {
|
||||
args: {
|
||||
variant: "link-neutral",
|
||||
children: "Link Neutral Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: EButtonSize.SM,
|
||||
size: "sm",
|
||||
children: "Small Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
size: EButtonSize.MD,
|
||||
size: "md",
|
||||
children: "Medium Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: EButtonSize.LG,
|
||||
size: "lg",
|
||||
children: "Large Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
size: "xl",
|
||||
children: "Extra Large Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
loading: true,
|
||||
children: "Loading Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
|
|
@ -92,15 +171,74 @@ export const Disabled: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const WithPrependIcon: Story = {
|
||||
args: {
|
||||
prependIcon: (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
),
|
||||
children: "With Prepend Icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAppendIcon: Story = {
|
||||
args: {
|
||||
appendIcon: (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
),
|
||||
children: "With Append Icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button variant={EButtonVariant.PRIMARY}>Primary</Button>
|
||||
<Button variant={EButtonVariant.SECONDARY}>Secondary</Button>
|
||||
<Button variant={EButtonVariant.OUTLINE}>Outline</Button>
|
||||
<Button variant={EButtonVariant.GHOST}>Ghost</Button>
|
||||
<Button variant={EButtonVariant.DESTRUCTIVE}>Destructive</Button>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Primary Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="accent-primary">Accent Primary</Button>
|
||||
<Button variant="outline-primary">Outline Primary</Button>
|
||||
<Button variant="neutral-primary">Neutral Primary</Button>
|
||||
<Button variant="link-primary">Link Primary</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Danger Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="danger">Danger</Button>
|
||||
<Button variant="accent-danger">Accent Danger</Button>
|
||||
<Button variant="outline-danger">Outline Danger</Button>
|
||||
<Button variant="link-danger">Link Danger</Button>
|
||||
<Button variant="tertiary-danger">Tertiary Danger</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Other Variants</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="link-neutral">Link Neutral</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -110,9 +248,25 @@ export const AllSizes: Story = {
|
|||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size={EButtonSize.SM}>Small</Button>
|
||||
<Button size={EButtonSize.MD}>Medium</Button>
|
||||
<Button size={EButtonSize.LG}>Large</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="md">Medium</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="xl">Extra Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Button States</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>Default</Button>
|
||||
<Button loading>Loading</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,73 +1,44 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export enum EButtonVariant {
|
||||
PRIMARY = "primary",
|
||||
SECONDARY = "secondary",
|
||||
OUTLINE = "outline",
|
||||
GHOST = "ghost",
|
||||
DESTRUCTIVE = "destructive",
|
||||
}
|
||||
|
||||
export enum EButtonSize {
|
||||
SM = "sm",
|
||||
MD = "md",
|
||||
LG = "lg",
|
||||
}
|
||||
|
||||
export type TButtonVariant =
|
||||
| EButtonVariant.PRIMARY
|
||||
| EButtonVariant.SECONDARY
|
||||
| EButtonVariant.OUTLINE
|
||||
| EButtonVariant.GHOST
|
||||
| EButtonVariant.DESTRUCTIVE;
|
||||
export type TButtonSize = EButtonSize.SM | EButtonSize.MD | EButtonSize.LG;
|
||||
import { getIconStyling, getButtonStyling, TButtonVariant, TButtonSizes } from "./helper";
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: TButtonVariant;
|
||||
size?: TButtonSize;
|
||||
size?: TButtonSizes;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
appendIcon?: any;
|
||||
prependIcon?: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const buttonVariants = {
|
||||
[EButtonVariant.PRIMARY]: "bg-custom-primary-100 text-white hover:bg-custom-primary-200 focus:bg-custom-primary-200",
|
||||
[EButtonVariant.SECONDARY]:
|
||||
"bg-custom-background-100 text-custom-text-200 border border-custom-border-200 hover:bg-custom-background-90 focus:bg-custom-background-90",
|
||||
[EButtonVariant.OUTLINE]:
|
||||
"border border-custom-primary-100 text-custom-primary-100 bg-transparent hover:bg-custom-primary-100/10 focus:bg-custom-primary-100/20",
|
||||
[EButtonVariant.GHOST]: "text-custom-text-200 hover:bg-custom-background-90 focus:bg-custom-background-90",
|
||||
[EButtonVariant.DESTRUCTIVE]: "bg-red-500 text-white hover:bg-red-600 focus:bg-red-600",
|
||||
};
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||
const {
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
type = "button",
|
||||
loading = false,
|
||||
disabled = false,
|
||||
prependIcon = null,
|
||||
appendIcon = null,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const buttonSizes = {
|
||||
[EButtonSize.SM]: "px-3 py-1.5 text-xs font-medium",
|
||||
[EButtonSize.MD]: "px-4 py-2 text-sm font-medium",
|
||||
[EButtonSize.LG]: "px-6 py-2.5 text-base font-medium",
|
||||
};
|
||||
const buttonStyle = getButtonStyling(variant, size, disabled || loading);
|
||||
const buttonIconStyle = getIconStyling(size);
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = EButtonVariant.PRIMARY, size = EButtonSize.MD, className, children, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"inline-flex items-center justify-center gap-2 rounded-md transition-colors duration-200",
|
||||
"focus:outline-none focus:ring-2 focus:ring-custom-primary-100/20 focus:ring-offset-2",
|
||||
"disabled:opacity-50 disabled:pointer-events-none",
|
||||
// Variant styles
|
||||
buttonVariants[variant],
|
||||
// Size styles
|
||||
buttonSizes[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
return (
|
||||
<button ref={ref} type={type} className={cn(buttonStyle, className)} disabled={disabled || loading} {...rest}>
|
||||
{prependIcon && <div className={buttonIconStyle}>{React.cloneElement(prependIcon, { strokeWidth: 2 })}</div>}
|
||||
{children}
|
||||
{appendIcon && <div className={buttonIconStyle}>{React.cloneElement(appendIcon, { strokeWidth: 2 })}</div>}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
Button.displayName = "Button";
|
||||
Button.displayName = "plane-ui-button";
|
||||
|
||||
export { Button };
|
||||
|
|
|
|||
126
packages/propel/src/button/helper.tsx
Normal file
126
packages/propel/src/button/helper.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
export type TButtonVariant =
|
||||
| "primary"
|
||||
| "accent-primary"
|
||||
| "outline-primary"
|
||||
| "neutral-primary"
|
||||
| "link-primary"
|
||||
| "danger"
|
||||
| "accent-danger"
|
||||
| "outline-danger"
|
||||
| "link-danger"
|
||||
| "tertiary-danger"
|
||||
| "link-neutral";
|
||||
|
||||
export type TButtonSizes = "sm" | "md" | "lg" | "xl";
|
||||
|
||||
export interface IButtonStyling {
|
||||
[key: string]: {
|
||||
default: string;
|
||||
hover: string;
|
||||
pressed: string;
|
||||
disabled: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum buttonSizeStyling {
|
||||
sm = `px-3 py-1.5 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
lg = `px-5 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
xl = `px-5 py-3.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center`,
|
||||
}
|
||||
|
||||
enum buttonIconStyling {
|
||||
sm = "h-3 w-3 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
md = "h-3.5 w-3.5 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
lg = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||
xl = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0 ",
|
||||
}
|
||||
|
||||
export const buttonStyling: IButtonStyling = {
|
||||
primary: {
|
||||
default: `text-white bg-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-200`,
|
||||
pressed: `focus:text-custom-brand-40 focus:bg-custom-primary-200`,
|
||||
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
|
||||
},
|
||||
"accent-primary": {
|
||||
default: `bg-custom-primary-100/20 text-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-100/10 hover:text-custom-primary-200`,
|
||||
pressed: `focus:bg-custom-primary-100/10`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||
},
|
||||
"outline-primary": {
|
||||
default: `text-custom-primary-100 bg-transparent border border-custom-primary-100`,
|
||||
hover: `hover:bg-custom-primary-100/20`,
|
||||
pressed: `focus:text-custom-primary-100 focus:bg-custom-primary-100/30`,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60 !border-custom-primary-60 `,
|
||||
},
|
||||
"neutral-primary": {
|
||||
default: `text-custom-text-200 bg-custom-background-100 border border-custom-border-200`,
|
||||
hover: `hover:bg-custom-background-90`,
|
||||
pressed: `focus:text-custom-text-300 focus:bg-custom-background-90`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
"link-primary": {
|
||||
default: `text-custom-primary-100 bg-custom-background-100`,
|
||||
hover: `hover:text-custom-primary-200`,
|
||||
pressed: `focus:text-custom-primary-80 `,
|
||||
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||
},
|
||||
|
||||
danger: {
|
||||
default: `text-white bg-red-500`,
|
||||
hover: ` hover:bg-red-600`,
|
||||
pressed: `focus:text-red-200 focus:bg-red-600`,
|
||||
disabled: `cursor-not-allowed !bg-red-300`,
|
||||
},
|
||||
"accent-danger": {
|
||||
default: `text-red-500 bg-red-50`,
|
||||
hover: `hover:text-red-600 hover:bg-red-100`,
|
||||
pressed: `focus:text-red-500 focus:bg-red-100`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"outline-danger": {
|
||||
default: `text-red-500 bg-transparent border border-red-500`,
|
||||
hover: `hover:text-red-400 hover:border-red-400`,
|
||||
pressed: `focus:text-red-400 focus:border-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300 !border-red-300`,
|
||||
},
|
||||
"link-danger": {
|
||||
default: `text-red-500 bg-custom-background-100`,
|
||||
hover: `hover:text-red-400`,
|
||||
pressed: `focus:text-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"tertiary-danger": {
|
||||
default: `text-red-500 bg-custom-background-100 border border-red-200`,
|
||||
hover: `hover:bg-red-50 hover:border-red-300`,
|
||||
pressed: `focus:text-red-400`,
|
||||
disabled: `cursor-not-allowed !text-red-300`,
|
||||
},
|
||||
"link-neutral": {
|
||||
default: `text-custom-text-300`,
|
||||
hover: `hover:text-custom-text-200`,
|
||||
pressed: `focus:text-custom-text-100`,
|
||||
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||
},
|
||||
};
|
||||
|
||||
export const getButtonStyling = (variant: TButtonVariant, size: TButtonSizes, disabled: boolean = false): string => {
|
||||
let tempVariant: string = ``;
|
||||
const currentVariant = buttonStyling[variant];
|
||||
|
||||
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
|
||||
currentVariant.pressed
|
||||
}`;
|
||||
|
||||
let tempSize: string = ``;
|
||||
if (size) tempSize = buttonSizeStyling[size];
|
||||
return `${tempVariant} ${tempSize}`;
|
||||
};
|
||||
|
||||
export const getIconStyling = (size: TButtonSizes): string => {
|
||||
let icon: string = ``;
|
||||
if (size) icon = buttonIconStyling[size];
|
||||
return icon;
|
||||
};
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export { Button } from "./button";
|
||||
export * from "./helper";
|
||||
export type { ButtonProps } from "./button";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue