add Button component and Storybook configuration
Some checks failed
CI TEST (Vault secrets) / build (push) Failing after 9s

This commit is contained in:
Nikita Pozdniakov
2026-05-04 11:38:50 +03:00
parent 3105d23e42
commit 7d3d87b1e5
11 changed files with 3101 additions and 656 deletions

View File

@@ -0,0 +1 @@
export { default } from "./ui/Button.astro";

View 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>
)
}

View 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",
},
};