add Button component and Storybook configuration
Some checks failed
CI TEST (Vault secrets) / build (push) Failing after 9s
Some checks failed
CI TEST (Vault secrets) / build (push) Failing after 9s
This commit is contained in:
1
src/shared/ui/Button/index.ts
Normal file
1
src/shared/ui/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./ui/Button.astro";
|
||||
118
src/shared/ui/Button/ui/Button.astro
Normal file
118
src/shared/ui/Button/ui/Button.astro
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
import type { HTMLAttributes } from "astro/types";
|
||||
|
||||
type Variant = "primary" | "secondary" | "ghost" | "danger";
|
||||
type Size = "sm" | "md" | "lg";
|
||||
|
||||
type CommonProps = {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
type ButtonAsButtonProps = CommonProps &
|
||||
Omit<HTMLAttributes<"button">, "class" | "disabled"> & {
|
||||
as?: "button";
|
||||
type?: "button" | "submit" | "reset";
|
||||
href?: never;
|
||||
};
|
||||
|
||||
type ButtonAsLinkProps = CommonProps &
|
||||
Omit<HTMLAttributes<"a">, "class" | "href"> & {
|
||||
as: "a";
|
||||
href: string;
|
||||
type?: never;
|
||||
};
|
||||
|
||||
type Props = ButtonAsButtonProps | ButtonAsLinkProps;
|
||||
|
||||
const {
|
||||
as = "button",
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
fullWidth = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
class: className = "",
|
||||
...attrs
|
||||
} = Astro.props as Props;
|
||||
|
||||
const isLink = as === "a";
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
const baseClass =
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md border font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-black disabled:pointer-events-none disabled:opacity-50";
|
||||
|
||||
const variantClassMap: Record<Variant, string> = {
|
||||
primary:
|
||||
"border-cyan-400 bg-cyan-400 text-black hover:border-cyan-300 hover:bg-cyan-300",
|
||||
secondary:
|
||||
"border-zinc-700 bg-zinc-900 text-white hover:border-zinc-600 hover:bg-zinc-800",
|
||||
ghost:
|
||||
"border-transparent bg-transparent text-zinc-200 hover:bg-white/5 hover:text-white",
|
||||
danger:
|
||||
"border-red-500 bg-red-500 text-white hover:border-red-400 hover:bg-red-400",
|
||||
};
|
||||
|
||||
const sizeClassMap: Record<Size, string> = {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-11 px-5 text-base",
|
||||
};
|
||||
|
||||
const widthClass = fullWidth ? "w-full" : "";
|
||||
const loadingClass = loading ? "cursor-wait" : "";
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
variantClassMap[variant],
|
||||
sizeClassMap[size],
|
||||
widthClass,
|
||||
loadingClass,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
---
|
||||
|
||||
{
|
||||
isLink ? (
|
||||
<a
|
||||
{...(attrs as Omit<ButtonAsLinkProps, keyof CommonProps | "as" | "href" | "type">)}
|
||||
href={(Astro.props as ButtonAsLinkProps).href}
|
||||
class={classes}
|
||||
aria-disabled={isDisabled ? "true" : undefined}
|
||||
data-loading={loading ? "true" : undefined}
|
||||
tabindex={isDisabled ? -1 : undefined}
|
||||
>
|
||||
{loading && (
|
||||
<span aria-hidden="true" class="inline-block size-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
|
||||
)}
|
||||
<slot name="leftIcon" />
|
||||
<span class:list={["inline-flex items-center", loading && "opacity-80"]}>
|
||||
<slot />
|
||||
</span>
|
||||
<slot name="rightIcon" />
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
{...(attrs as Omit<ButtonAsButtonProps, keyof CommonProps | "as" | "href" | "type">)}
|
||||
type={(Astro.props as ButtonAsButtonProps).type ?? "button"}
|
||||
class={classes}
|
||||
disabled={isDisabled}
|
||||
data-loading={loading ? "true" : undefined}
|
||||
>
|
||||
{loading && (
|
||||
<span aria-hidden="true" class="inline-block size-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
|
||||
)}
|
||||
<slot name="leftIcon" />
|
||||
<span class:list={["inline-flex items-center", loading && "opacity-80"]}>
|
||||
<slot />
|
||||
</span>
|
||||
<slot name="rightIcon" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
116
src/shared/ui/Button/ui/Button.stories.tsx
Normal file
116
src/shared/ui/Button/ui/Button.stories.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import Button from "./Button.astro";
|
||||
|
||||
export default {
|
||||
title: "shared/ui/Button",
|
||||
component: Button,
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
fullWidth: false,
|
||||
loading: false,
|
||||
disabled: false,
|
||||
},
|
||||
argTypes: {
|
||||
as: {
|
||||
control: "radio",
|
||||
options: ["button", "a"],
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["primary", "secondary", "ghost", "danger"],
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["sm", "md", "lg"],
|
||||
},
|
||||
fullWidth: {
|
||||
control: "boolean",
|
||||
},
|
||||
loading: {
|
||||
control: "boolean",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["button", "submit", "reset"],
|
||||
},
|
||||
href: {
|
||||
control: "text",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Primary = {
|
||||
args: {
|
||||
children: "Primary button",
|
||||
variant: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary = {
|
||||
args: {
|
||||
children: "Secondary button",
|
||||
variant: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Ghost = {
|
||||
args: {
|
||||
children: "Ghost button",
|
||||
variant: "ghost",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger = {
|
||||
args: {
|
||||
children: "Delete",
|
||||
variant: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading = {
|
||||
args: {
|
||||
children: "Loading...",
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
children: "Disabled",
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const AsLink = {
|
||||
args: {
|
||||
as: "a",
|
||||
href: "/demo",
|
||||
children: "Open demo",
|
||||
variant: "ghost",
|
||||
},
|
||||
};
|
||||
|
||||
export const FullWidth = {
|
||||
args: {
|
||||
children: "Continue",
|
||||
fullWidth: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = {
|
||||
args: {
|
||||
children: "Small",
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large = {
|
||||
args: {
|
||||
children: "Large",
|
||||
size: "lg",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user