docs(frontend): new Button component + stories (#10192)

### Changes 🏗️

![Screenshot 2025-06-19 at 13 38
21](https://github.com/user-attachments/assets/8c3f8d27-6f4d-4d95-8d78-0b160ce51091)

- Adds a new `<Button>` component that mirrors 1:1 what we have in the
design system
- Documented the new component via stories
- Re-arranged the stories in the Storybook sidebar to show the legacy
ones at the end

Once this is merged, we can start updating buttons on the app to only
use this one, so we have a consistent UX 💆🏽

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run Storybook locally
  - [x] Button stories look good ( _in all variants_ )
This commit is contained in:
Ubbe
2025-06-19 14:43:23 +04:00
committed by GitHub
parent d0beebcbff
commit fc4d0d4bb8
14 changed files with 635 additions and 25 deletions

View File

@@ -1,7 +1,12 @@
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: [
"../src/components/overview.stories.@(js|jsx|mjs|ts|tsx)",
"../src/components/tokens/**/*.stories.@(js|jsx|mjs|ts|tsx)",
"../src/components/atoms/**/*.stories.@(js|jsx|mjs|ts|tsx)",
"../src/components/agptui/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-a11y",
"@storybook/addon-onboarding",

View File

@@ -1,18 +1,17 @@
import React from "react";
import { initialize, mswLoader } from "msw-storybook-addon";
import "../src/app/globals.css";
import "../src/components/styles/fonts.css";
import {
Controls,
Description,
Primary,
Source,
Stories,
Subtitle,
Title,
} from "@storybook/addon-docs/blocks";
import { theme } from "./theme";
import { Preview } from "@storybook/nextjs";
import { initialize, mswLoader } from "msw-storybook-addon";
import React from "react";
import "../src/app/globals.css";
import "../src/components/styles/fonts.css";
import { theme } from "./theme";
// Initialize MSW
initialize();

View File

@@ -0,0 +1,526 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { Play, Plus } from "lucide-react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Atoms/Button",
tags: ["autodocs"],
component: Button,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Button component with multiple variants and sizes based on our design system. Built on top of shadcn/ui button with custom styling.",
},
},
},
argTypes: {
variant: {
control: "select",
options: [
"primary",
"secondary",
"destructive",
"outline",
"ghost",
"loading",
],
description: "Button style variant",
},
size: {
control: "select",
options: ["small", "large", "icon"],
description: "Button size",
},
loading: {
control: "boolean",
description: "Show loading spinner and disable button",
},
disabled: {
control: "boolean",
description: "Disable the button",
},
children: {
control: "text",
description: "Button content",
},
},
args: {
children: "Button",
variant: "primary",
size: "large",
loading: false,
disabled: false,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Basic variants
export const Primary: Story = {
args: {
variant: "primary",
children: "Primary Button",
},
};
export const Secondary: Story = {
args: {
variant: "secondary",
children: "Secondary Button",
},
};
export const Destructive: Story = {
args: {
variant: "destructive",
children: "Delete",
},
};
export const Outline: Story = {
args: {
variant: "outline",
children: "Outline Button",
},
};
export const Ghost: Story = {
args: {
variant: "ghost",
children: "Ghost Button",
},
};
export const Link: Story = {
args: {
variant: "link",
children: "Add to library",
},
};
// Loading states
export const Loading: Story = {
args: {
loading: true,
children: "Processing...",
},
};
export const LoadingSecondary: Story = {
args: {
variant: "secondary",
loading: true,
children: "Loading...",
},
};
// Sizes
export const SmallButtons: Story = {
render: renderSmallButtons,
};
export const LargeButtons: Story = {
render: renderLargeButtons,
};
// With icons
export const WithLeftIcon: Story = {
args: {
variant: "primary",
leftIcon: <Play className="h-4 w-4" />,
children: "Play",
},
};
export const WithRightIcon: Story = {
args: {
variant: "outline",
rightIcon: <Plus className="h-4 w-4" />,
children: "Add Item",
},
};
export const IconOnly: Story = {
args: {
variant: "icon",
size: "icon",
children: <Plus className="h-4 w-4" />,
"aria-label": "Add item",
},
};
// States
export const Disabled: Story = {
render: renderDisabledButtons,
};
// Complete showcase matching Figma design
export const AllVariants: Story = {
render: renderAllVariants,
};
// Render functions as function declarations
function renderSmallButtons() {
return (
<div className="flex flex-wrap gap-4">
<Button variant="primary" size="small">
Primary
</Button>
<Button variant="secondary" size="small">
Secondary
</Button>
<Button variant="destructive" size="small">
Delete
</Button>
<Button variant="outline" size="small">
Outline
</Button>
<Button variant="ghost" size="small">
Ghost
</Button>
</div>
);
}
function renderLargeButtons() {
return (
<div className="flex flex-wrap gap-4">
<Button variant="primary" size="large">
Primary
</Button>
<Button variant="secondary" size="large">
Secondary
</Button>
<Button variant="destructive" size="large">
Delete
</Button>
<Button variant="outline" size="large">
Outline
</Button>
<Button variant="ghost" size="large">
Ghost
</Button>
</div>
);
}
function renderDisabledButtons() {
return (
<div className="flex flex-wrap gap-4">
<Button variant="primary" disabled>
Primary Disabled
</Button>
<Button variant="secondary" disabled>
Secondary Disabled
</Button>
<Button variant="destructive" disabled>
Destructive Disabled
</Button>
<Button variant="outline" disabled>
Outline Disabled
</Button>
<Button variant="ghost" disabled>
Ghost Disabled
</Button>
</div>
);
}
function renderAllVariants() {
return (
<div className="space-y-12 p-8">
{/* Large buttons section */}
<div className="space-y-8">
<h2 className="text-3xl font-semibold text-neutral-900">
Large buttons
</h2>
<div className="flex flex-wrap gap-20">
{/* Primary */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Primary
</div>
<div className="flex flex-col gap-8">
<Button variant="primary" size="large">
Save
</Button>
<Button variant="primary" size="large" loading>
Loading
</Button>
<Button variant="primary" size="large" disabled>
Disabled
</Button>
<Button
variant="primary"
size="large"
leftIcon={<Play className="h-5 w-5" />}
>
Play
</Button>
</div>
</div>
{/* Secondary */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Secondary
</div>
<div className="flex flex-col gap-8">
<Button variant="secondary" size="large">
Save
</Button>
<Button variant="secondary" size="large" loading>
Loading
</Button>
<Button variant="secondary" size="large" disabled>
Disabled
</Button>
<Button
variant="secondary"
size="large"
leftIcon={<Play className="h-5 w-5" />}
>
Play
</Button>
</div>
</div>
{/* Destructive */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Destructive
</div>
<div className="flex flex-col gap-8">
<Button variant="destructive" size="large">
Save
</Button>
<Button variant="destructive" size="large" loading>
Loading
</Button>
<Button variant="destructive" size="large" disabled>
Disabled
</Button>
<Button
variant="destructive"
size="large"
leftIcon={<Play className="h-5 w-5" />}
>
Play
</Button>
</div>
</div>
{/* Outline */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Outline
</div>
<div className="flex flex-col gap-8">
<Button variant="outline" size="large">
Save
</Button>
<Button variant="outline" size="large" loading>
Loading
</Button>
<Button variant="outline" size="large" disabled>
Disabled
</Button>
<Button
variant="outline"
size="large"
leftIcon={<Play className="h-5 w-5" />}
>
Play
</Button>
</div>
</div>
{/* Ghost */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Save
</div>
<div className="flex flex-col gap-8">
<Button variant="ghost" size="large">
Text
</Button>
<Button variant="ghost" size="large" loading>
Loading
</Button>
<Button variant="ghost" size="large" disabled>
Disabled
</Button>
<Button
variant="ghost"
size="large"
leftIcon={<Play className="h-5 w-5" />}
>
Play
</Button>
</div>
</div>
</div>
</div>
{/* Small buttons section */}
<div className="space-y-8">
<h2 className="text-3xl font-semibold text-neutral-900">
Small buttons
</h2>
<div className="flex flex-wrap gap-20">
{/* Primary Small */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Primary
</div>
<div className="flex flex-col gap-8">
<Button variant="primary" size="small">
Save
</Button>
<Button variant="primary" size="small" loading>
Loading
</Button>
<Button variant="primary" size="small" disabled>
Disabled
</Button>
<Button
variant="primary"
size="small"
leftIcon={<Play className="h-4 w-4" />}
>
Play
</Button>
</div>
</div>
{/* Secondary Small */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Secondary
</div>
<div className="flex flex-col gap-8">
<Button variant="secondary" size="small">
Save
</Button>
<Button variant="secondary" size="small" disabled>
Disabled
</Button>
<Button
variant="secondary"
size="small"
leftIcon={<Play className="h-4 w-4" />}
>
Play
</Button>
</div>
</div>
{/* Destructive Small */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Destructive
</div>
<div className="flex flex-col gap-8">
<Button variant="destructive" size="small">
Save
</Button>
<Button variant="destructive" size="small" disabled>
Disabled
</Button>
<Button
variant="destructive"
size="small"
leftIcon={<Play className="h-4 w-4" />}
>
Play
</Button>
</div>
</div>
{/* Outline Small */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Outline
</div>
<div className="flex flex-col gap-8">
<Button variant="outline" size="small">
Save
</Button>
<Button variant="outline" size="small" disabled>
Disabled
</Button>
<Button
variant="outline"
size="small"
leftIcon={<Play className="h-4 w-4" />}
>
Play
</Button>
</div>
</div>
{/* Ghost Small */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-neutral-900">
Ghost
</div>
<div className="flex flex-col gap-8">
<Button variant="ghost" size="small">
Save
</Button>
<Button variant="ghost" size="small" disabled>
Disabled
</Button>
<Button
variant="ghost"
size="small"
leftIcon={<Play className="h-4 w-4" />}
>
Play
</Button>
</div>
</div>
</div>
</div>
{/* Other button types */}
<div className="space-y-8">
<h2 className="text-3xl font-semibold text-neutral-900">
Other button types
</h2>
<div className="flex gap-20">
{/* Link */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-zinc-800">
Link
</div>
<div className="flex flex-col gap-8">
<Button variant="link">Add to library</Button>
</div>
</div>
{/* Icon */}
<div className="flex flex-col gap-5">
<div className="font-['Geist'] text-base font-medium text-zinc-800">
Icon
</div>
<div className="flex flex-col gap-8">
<Button variant="icon" size="icon">
<Plus className="h-4 w-4" />
</Button>
<Button variant="primary" size="icon" className="bg-zinc-700">
<Plus className="h-4 w-4" />
</Button>
<Button variant="icon" size="icon" disabled>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,81 @@
import { cn } from "@/lib/utils";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { cva, type VariantProps } from "class-variance-authority";
import React from "react";
// Extended button variants based on our design system
const extendedButtonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-['Geist'] leading-snug border",
{
variants: {
variant: {
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
link: "bg-transparent border-transparent text-black hover:underline rounded-none p-0 h-auto min-w-auto disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "min-w-20 px-4 py-3 text-sm gap-2",
icon: "p-3",
},
},
defaultVariants: {
variant: "primary",
size: "large",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof extendedButtonVariants> {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
leftIcon,
rightIcon,
children,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled;
return (
<button
className={cn(
extendedButtonVariants({ variant, size, className }),
loading && "pointer-events-none",
)}
disabled={isDisabled}
{...props}
>
{loading && (
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</button>
);
}
Button.displayName = "Button";
export { Button, extendedButtonVariants };

View File

@@ -1,9 +1,9 @@
import { StoryCode } from "@/stories/helpers/StoryCode";
import { StoryCode } from "@/components/tokens/helpers/StoryCode";
import type { Meta, StoryObj } from "@storybook/nextjs";
import { Text, textVariants } from "./Text";
const meta: Meta<typeof Text> = {
title: "Design System/Atoms/Text",
title: "Atoms/Text",
component: Text,
tags: ["autodocs"],
parameters: {

View File

@@ -456,7 +456,6 @@ const meta: Meta<typeof OverviewComponent> = {
component: OverviewComponent,
parameters: {
layout: "fullscreen",
controls: { disable: true },
},
};

View File

@@ -1,10 +1,10 @@
import { Text } from "@/components/atoms/Text/Text";
import type { Meta } from "@storybook/nextjs";
import { Text } from "@/components/_new/Text/Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
import { SquareArrowOutUpRight } from "lucide-react";
import { StoryCode } from "./helpers/StoryCode";
const meta: Meta = {
title: "Design System/ Tokens /Border Radius",
title: "Tokens /Border Radius",
parameters: {
layout: "fullscreen",
controls: { disable: true },

View File

@@ -1,10 +1,10 @@
import { Text } from "@/components/_new/Text/Text";
import { Text } from "@/components/atoms/Text/Text";
import { colors } from "@/components/styles/colors";
import { StoryCode } from "@/stories/helpers/StoryCode";
import type { Meta } from "@storybook/nextjs";
import { StoryCode } from "./helpers/StoryCode";
const meta: Meta = {
title: "Design System/ Tokens /Colors",
title: "Tokens /Colors",
parameters: {
layout: "fullscreen",
controls: { disable: true },

View File

@@ -1,5 +1,4 @@
import { Text } from "@/components/_new/Text/Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
import { Text } from "@/components/atoms/Text/Text";
import {
Alien,
ArrowClockwise,
@@ -40,9 +39,10 @@ import {
} from "@phosphor-icons/react";
import type { Meta } from "@storybook/nextjs";
import { SquareArrowOutUpRight } from "lucide-react";
import { StoryCode } from "./helpers/StoryCode";
const meta: Meta = {
title: "Design System/ Tokens /Icons",
title: "Tokens /Icons",
parameters: {
layout: "fullscreen",
controls: { disable: true },

View File

@@ -1,10 +1,10 @@
import { Text } from "@/components/atoms/Text/Text";
import type { Meta } from "@storybook/nextjs";
import { Text } from "@/components/_new/Text/Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
import { SquareArrowOutUpRight } from "lucide-react";
import { StoryCode } from "./helpers/StoryCode";
const meta: Meta = {
title: "Design System/ Tokens /Spacing",
title: "Tokens /Spacing",
parameters: {
layout: "fullscreen",
controls: { disable: true },

View File

@@ -1,9 +1,9 @@
import { Text } from "@/components/atoms/Text/Text";
import type { Meta } from "@storybook/nextjs";
import { Text } from "@/components/_new/Text/Text";
import { StoryCode } from "@/stories/helpers/StoryCode";
import { StoryCode } from "./helpers/StoryCode";
const meta: Meta<typeof Text> = {
title: "Design System/ Tokens /Typography",
title: "Tokens /Typography",
component: Text,
parameters: {
layout: "fullscreen",