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

1
.npmrc
View File

@@ -1 +0,0 @@
package-lock=false

30
.storybook/constants.ts Normal file
View File

@@ -0,0 +1,30 @@
export const CUSTOM_VIEW_PORTS = {
sm: {
name: "Small",
styles: {
width: "375px",
height: "800px",
},
},
md: {
name: "Medium",
styles: {
width: "768px",
height: "1024px",
},
},
lg: {
name: "Large",
styles: {
width: "1024px",
height: "1366px",
},
},
xl: {
name: "Extra Large",
styles: {
width: "1440px",
height: "900px",
},
},
};

18
.storybook/main.js Normal file
View File

@@ -0,0 +1,18 @@
import tailwindcss from "@tailwindcss/vite";
export default {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
staticDirs: ["../public"],
framework: {
name: "@storybook-astro/framework",
},
core: {
disableTelemetry: true,
enableCrashReports: false,
},
async viteFinal(config) {
config.plugins = config.plugins || [];
config.plugins.push(tailwindcss());
return config;
},
};

1
.storybook/preview.css Normal file
View File

@@ -0,0 +1 @@
@import "../src/app/styles/global.css";

22
.storybook/preview.js Normal file
View File

@@ -0,0 +1,22 @@
import "./preview.css";
import { INITIAL_VIEWPORTS } from "storybook/viewport";
import { CUSTOM_VIEW_PORTS } from "./constants.ts";
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: "darky",
values: [{ name: "darky", value: "#0b121a" }],
},
viewport: {
options: { ...CUSTOM_VIEW_PORTS, ...INITIAL_VIEWPORTS },
},
},
};
export default preview;

View File

@@ -19,4 +19,5 @@ export default defineConfig({
alias, alias,
}, },
}, },
server: {},
}); });

View File

@@ -16,7 +16,9 @@
"lint": "astro check && biome check .", "lint": "astro check && biome check .",
"format": "biome format . --write", "format": "biome format . --write",
"prepare": "husky", "prepare": "husky",
"ci": "astro check && biome ci ." "ci": "astro check && biome ci .",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx,json,jsonc,css,scss}": [ "*.{js,jsx,ts,tsx,json,jsonc,css,scss}": [
@@ -29,14 +31,17 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"@webtui/css": "^0.1.7", "@webtui/css": "^0.1.7",
"astro": "^6.1.8", "astro": "^6.2.1",
"tailwindcss": "^4.2.4" "tailwindcss": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.9",
"@biomejs/biome": "2.4.13", "@biomejs/biome": "2.4.13",
"@storybook-astro/framework": "^1.1.0",
"@storybook/builder-vite": "^10.3.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.4.0", "lint-staged": "^16.4.0",
"typescript": "^6.0.3", "storybook": "^10.3.6",
"@astrojs/check": "^0.9.9" "typescript": "^6.0.3"
} }
} }

3436
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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