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:
30
.storybook/constants.ts
Normal file
30
.storybook/constants.ts
Normal 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
18
.storybook/main.js
Normal 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
1
.storybook/preview.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "../src/app/styles/global.css";
|
||||||
22
.storybook/preview.js
Normal file
22
.storybook/preview.js
Normal 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;
|
||||||
@@ -19,4 +19,5 @@ export default defineConfig({
|
|||||||
alias,
|
alias,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {},
|
||||||
});
|
});
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -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
3436
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
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