feat: add portal UI components and stories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-03-07 15:25:01 +09:00
parent 74ee154669
commit 4c31c448f3
198 changed files with 13136 additions and 0 deletions

View File

@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AnimatedContainer } from "./animated-container";
const meta: Meta<typeof AnimatedContainer> = {
title: "Atoms/AnimatedContainer",
component: AnimatedContainer,
argTypes: {
animation: { control: "select", options: ["fade-up", "fade-scale", "slide-left", "none"] },
stagger: { control: "boolean" },
delay: { control: { type: "number", min: 0, max: 2000, step: 100 } },
},
};
export default meta;
type Story = StoryObj<typeof AnimatedContainer>;
export const FadeUp: Story = {
args: {
animation: "fade-up",
children: (
<div className="bg-card border border-border rounded-xl p-6 shadow-sm">Fade Up Animation</div>
),
},
};
export const FadeScale: Story = {
args: {
animation: "fade-scale",
children: (
<div className="bg-card border border-border rounded-xl p-6 shadow-sm">
Fade Scale Animation
</div>
),
},
};
export const SlideLeft: Story = {
args: {
animation: "slide-left",
children: (
<div className="bg-card border border-border rounded-xl p-6 shadow-sm">
Slide Left Animation
</div>
),
},
};

View File

@ -0,0 +1,71 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Badge } from "./badge";
const meta: Meta<typeof Badge> = {
title: "Atoms/Badge",
component: Badge,
argTypes: {
variant: {
control: "select",
options: ["default", "secondary", "success", "warning", "error", "info", "outline", "ghost"],
},
size: { control: "select", options: ["sm", "default", "lg"] },
dot: { control: "boolean" },
removable: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof Badge>;
export const Default: Story = {
args: { children: "Badge" },
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Badge variant="default">Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="error">Error</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="ghost">Ghost</Badge>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div className="flex gap-2 items-center">
<Badge size="sm">Small</Badge>
<Badge size="default">Default</Badge>
<Badge size="lg">Large</Badge>
</div>
),
};
export const WithDot: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
<Badge variant="success" dot>
Active
</Badge>
<Badge variant="warning" dot>
Pending
</Badge>
<Badge variant="error" dot>
Failed
</Badge>
<Badge variant="info" dot>
Processing
</Badge>
</div>
),
};
export const Removable: Story = {
args: { children: "Removable", removable: true, onRemove: () => alert("Removed!") },
};

View File

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";
import { ArrowRightIcon, PlusIcon } from "@heroicons/react/24/outline";
const meta: Meta<typeof Button> = {
title: "Atoms/Button",
component: Button,
argTypes: {
variant: {
control: "select",
options: [
"default",
"destructive",
"outline",
"secondary",
"ghost",
"subtle",
"link",
"pill",
"pillOutline",
],
},
size: { control: "select", options: ["default", "sm", "lg"] },
loading: { control: "boolean" },
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: { children: "Button" },
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-3 items-center">
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="link">Link</Button>
<Button variant="pill">Pill</Button>
<Button variant="pillOutline">Pill Outline</Button>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div className="flex gap-3 items-center">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
</div>
),
};
export const WithIcons: Story = {
render: () => (
<div className="flex gap-3 items-center">
<Button leftIcon={<PlusIcon className="h-4 w-4" />}>Add Item</Button>
<Button rightIcon={<ArrowRightIcon className="h-4 w-4" />}>Continue</Button>
<Button
leftIcon={<PlusIcon className="h-4 w-4" />}
rightIcon={<ArrowRightIcon className="h-4 w-4" />}
>
Both Icons
</Button>
</div>
),
};
export const Loading: Story = {
args: { children: "Submitting...", loading: true },
};
export const Disabled: Story = {
args: { children: "Disabled", disabled: true },
};

View File

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Checkbox } from "./checkbox";
const meta: Meta<typeof Checkbox> = {
title: "Atoms/Checkbox",
component: Checkbox,
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
export const Default: Story = {
args: { label: "Accept terms and conditions" },
};
export const WithHelperText: Story = {
args: { label: "Send me emails", helperText: "We'll only send important updates" },
};
export const WithError: Story = {
args: { label: "I agree to the terms", error: "You must accept the terms" },
};
export const Checked: Story = {
args: { label: "Already checked", defaultChecked: true },
};
export const AllStates: Story = {
render: () => (
<div className="flex flex-col gap-4">
<Checkbox label="Default" />
<Checkbox label="Checked" defaultChecked />
<Checkbox label="With helper" helperText="Additional information" />
<Checkbox label="With error" error="This is required" />
<Checkbox label="Disabled" disabled />
<Checkbox label="Disabled checked" disabled defaultChecked />
</div>
),
};

View File

@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react";
import { EmptyState, SearchEmptyState } from "./empty-state";
import { InboxIcon } from "@heroicons/react/24/outline";
const meta: Meta<typeof EmptyState> = {
title: "Atoms/EmptyState",
component: EmptyState,
argTypes: {
variant: { control: "select", options: ["default", "compact"] },
},
};
export default meta;
type Story = StoryObj<typeof EmptyState>;
export const Default: Story = {
args: {
icon: <InboxIcon className="h-12 w-12" />,
title: "No invoices yet",
description: "When you receive invoices, they will appear here.",
},
};
export const WithAction: Story = {
args: {
icon: <InboxIcon className="h-12 w-12" />,
title: "No services",
description: "Get started by adding your first service.",
action: { label: "Add Service", onClick: () => {} },
},
};
export const Compact: Story = {
args: {
title: "No results",
description: "Try adjusting your filters.",
variant: "compact",
},
};
export const SearchEmpty: Story = {
render: () => <SearchEmptyState searchTerm="fiber internet" onClearSearch={() => {}} />,
};

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ErrorMessage } from "./error-message";
const meta: Meta<typeof ErrorMessage> = {
title: "Atoms/ErrorMessage",
component: ErrorMessage,
argTypes: {
variant: { control: "select", options: ["default", "inline", "subtle"] },
showIcon: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof ErrorMessage>;
export const Default: Story = {
args: { children: "This field is required" },
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-3">
<ErrorMessage variant="default">Default error message</ErrorMessage>
<ErrorMessage variant="inline">Inline error message</ErrorMessage>
<ErrorMessage variant="subtle">Subtle error message</ErrorMessage>
</div>
),
};
export const WithoutIcon: Story = {
args: { children: "Error without icon", showIcon: false },
};

View File

@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ErrorState } from "./error-state";
const meta: Meta<typeof ErrorState> = {
title: "Atoms/ErrorState",
component: ErrorState,
argTypes: {
variant: { control: "select", options: ["page", "card", "inline"] },
},
};
export default meta;
type Story = StoryObj<typeof ErrorState>;
export const Card: Story = {
args: { variant: "card" },
};
export const Page: Story = {
args: { variant: "page" },
};
export const Inline: Story = {
args: { variant: "inline" },
};
export const WithRetry: Story = {
args: {
variant: "card",
title: "Failed to load data",
message: "Could not connect to the server. Please check your connection.",
onRetry: () => alert("Retrying..."),
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-6 max-w-lg">
<ErrorState
variant="card"
title="Card Error"
message="This is a card error state"
onRetry={() => {}}
/>
<ErrorState variant="inline" title="Inline Error" message="This is an inline error" />
</div>
),
};

View File

@ -0,0 +1,36 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InlineToast } from "./inline-toast";
const meta: Meta<typeof InlineToast> = {
title: "Atoms/InlineToast",
component: InlineToast,
argTypes: {
tone: { control: "select", options: ["info", "success", "warning", "error"] },
visible: { control: "boolean" },
},
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof InlineToast>;
export const Success: Story = {
args: { visible: true, text: "Changes saved successfully!", tone: "success" },
};
export const Error: Story = {
args: { visible: true, text: "Failed to save changes", tone: "error" },
};
export const AllTones: Story = {
render: () => (
<div className="relative h-96">
<div className="absolute bottom-6 right-6 flex flex-col gap-3">
<InlineToast visible text="Info message" tone="info" className="!fixed !relative" />
<InlineToast visible text="Success message" tone="success" className="!fixed !relative" />
<InlineToast visible text="Warning message" tone="warning" className="!fixed !relative" />
<InlineToast visible text="Error message" tone="error" className="!fixed !relative" />
</div>
</div>
),
};

View File

@ -0,0 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./input";
const meta: Meta<typeof Input> = {
title: "Atoms/Input",
component: Input,
};
export default meta;
type Story = StoryObj<typeof Input>;
export const Default: Story = {
args: { placeholder: "Enter text..." },
};
export const WithValue: Story = {
args: { defaultValue: "Hello world" },
};
export const WithError: Story = {
args: { placeholder: "Email", error: "Invalid email address" },
};
export const Disabled: Story = {
args: { placeholder: "Disabled input", disabled: true },
};
export const AllStates: Story = {
render: () => (
<div className="flex flex-col gap-4 w-80">
<Input placeholder="Default" />
<Input placeholder="With value" defaultValue="Some text" />
<Input placeholder="Error state" error="This field is required" />
<Input placeholder="Disabled" disabled />
</div>
),
};

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Label } from "./label";
const meta: Meta<typeof Label> = {
title: "Atoms/Label",
component: Label,
};
export default meta;
type Story = StoryObj<typeof Label>;
export const Default: Story = {
args: { children: "Email address" },
};
export const Required: Story = {
render: () => (
<Label>
Email address <span className="text-danger">*</span>
</Label>
),
};

View File

@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";
import { LoadingOverlay } from "./loading-overlay";
const meta: Meta<typeof LoadingOverlay> = {
title: "Atoms/LoadingOverlay",
component: LoadingOverlay,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof LoadingOverlay>;
export const Default: Story = {
args: {
isVisible: true,
title: "Processing your order...",
subtitle: "This may take a moment",
},
};
export const SimpleMessage: Story = {
args: {
isVisible: true,
title: "Loading...",
},
};

View File

@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Logo } from "./logo";
const meta: Meta<typeof Logo> = {
title: "Atoms/Logo",
component: Logo,
};
export default meta;
type Story = StoryObj<typeof Logo>;
export const Default: Story = {
args: { size: 32 },
};
export const Sizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<Logo size={16} />
<Logo size={24} />
<Logo size={32} />
<Logo size={48} />
<Logo size={64} />
</div>
),
};

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PasswordInput } from "./password-input";
const meta: Meta<typeof PasswordInput> = {
title: "Atoms/PasswordInput",
component: PasswordInput,
};
export default meta;
type Story = StoryObj<typeof PasswordInput>;
export const Default: Story = {
args: { placeholder: "Enter password" },
};
export const WithValue: Story = {
args: { defaultValue: "mysecretpassword" },
};
export const Disabled: Story = {
args: { placeholder: "Disabled", disabled: true },
};

View File

@ -0,0 +1,30 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Skeleton } from "./skeleton";
const meta: Meta<typeof Skeleton> = {
title: "Atoms/Skeleton",
component: Skeleton,
};
export default meta;
type Story = StoryObj<typeof Skeleton>;
export const Default: Story = {
args: { className: "h-4 w-48" },
};
export const CommonPatterns: Story = {
render: () => (
<div className="flex flex-col gap-4 w-80">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-10 w-10 rounded-full" />
<Skeleton className="h-32 w-full rounded-xl" />
</div>
),
};
export const NoAnimation: Story = {
args: { className: "h-4 w-48", animate: false },
};

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Spinner } from "./spinner";
const meta: Meta<typeof Spinner> = {
title: "Atoms/Spinner",
component: Spinner,
argTypes: {
size: { control: "select", options: ["xs", "sm", "md", "lg", "xl"] },
},
};
export default meta;
type Story = StoryObj<typeof Spinner>;
export const Default: Story = {
args: { size: "md" },
};
export const AllSizes: Story = {
render: () => (
<div className="flex gap-4 items-center">
<Spinner size="xs" />
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
<Spinner size="xl" />
</div>
),
};
export const Colored: Story = {
render: () => (
<div className="flex gap-4 items-center">
<Spinner size="lg" className="text-primary" />
<Spinner size="lg" className="text-success" />
<Spinner size="lg" className="text-warning" />
<Spinner size="lg" className="text-danger" />
</div>
),
};

View File

@ -0,0 +1,41 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StatusIndicator } from "./status-indicator";
const meta: Meta<typeof StatusIndicator> = {
title: "Atoms/StatusIndicator",
component: StatusIndicator,
argTypes: {
status: { control: "select", options: ["active", "warning", "error", "inactive", "pending"] },
size: { control: "select", options: ["sm", "md", "lg"] },
pulse: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof StatusIndicator>;
export const Default: Story = {
args: { status: "active", label: "Online" },
};
export const AllStatuses: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusIndicator status="active" label="Active" />
<StatusIndicator status="warning" label="Warning" />
<StatusIndicator status="error" label="Error" />
<StatusIndicator status="inactive" label="Inactive" />
<StatusIndicator status="pending" label="Pending" pulse />
</div>
),
};
export const Sizes: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusIndicator status="active" label="Small" size="sm" />
<StatusIndicator status="active" label="Medium" size="md" />
<StatusIndicator status="active" label="Large" size="lg" />
</div>
),
};

View File

@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StatusPill } from "./status-pill";
import { CheckCircleIcon, ClockIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
const meta: Meta<typeof StatusPill> = {
title: "Atoms/StatusPill",
component: StatusPill,
argTypes: {
variant: { control: "select", options: ["success", "warning", "info", "neutral", "error"] },
size: { control: "select", options: ["sm", "md", "lg"] },
},
};
export default meta;
type Story = StoryObj<typeof StatusPill>;
export const Default: Story = {
args: { label: "Active", variant: "success" },
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-3">
<StatusPill label="Active" variant="success" />
<StatusPill label="Pending" variant="warning" />
<StatusPill label="Processing" variant="info" />
<StatusPill label="Inactive" variant="neutral" />
<StatusPill label="Failed" variant="error" />
</div>
),
};
export const Sizes: Story = {
render: () => (
<div className="flex gap-3 items-center">
<StatusPill label="Small" variant="success" size="sm" />
<StatusPill label="Medium" variant="success" size="md" />
<StatusPill label="Large" variant="success" size="lg" />
</div>
),
};
export const WithIcons: Story = {
render: () => (
<div className="flex gap-3">
<StatusPill
label="Active"
variant="success"
icon={<CheckCircleIcon className="h-3.5 w-3.5" />}
/>
<StatusPill label="Pending" variant="warning" icon={<ClockIcon className="h-3.5 w-3.5" />} />
<StatusPill
label="Error"
variant="error"
icon={<ExclamationTriangleIcon className="h-3.5 w-3.5" />}
/>
</div>
),
};

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StepHeader } from "./step-header";
const meta: Meta<typeof StepHeader> = {
title: "Atoms/StepHeader",
component: StepHeader,
};
export default meta;
type Story = StoryObj<typeof StepHeader>;
export const Default: Story = {
args: {
stepNumber: 1,
title: "Choose Your Plan",
description: "Select the plan that best fits your needs",
},
};
export const MultipleSteps: Story = {
render: () => (
<div className="flex flex-col gap-6">
<StepHeader
stepNumber={1}
title="Choose Your Plan"
description="Select the plan that best fits your needs"
/>
<StepHeader
stepNumber={2}
title="Enter Details"
description="Fill in your personal information"
/>
<StepHeader
stepNumber={3}
title="Review & Confirm"
description="Check everything before submitting"
/>
</div>
),
};

View File

@ -0,0 +1,25 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { ViewToggle, type ViewMode } from "./view-toggle";
const meta: Meta<typeof ViewToggle> = {
title: "Atoms/ViewToggle",
component: ViewToggle,
};
export default meta;
type Story = StoryObj<typeof ViewToggle>;
export const Default: Story = {
render: function Render() {
const [mode, setMode] = useState<ViewMode>("grid");
return <ViewToggle value={mode} onChange={setMode} />;
},
};
export const ListView: Story = {
render: function Render() {
const [mode, setMode] = useState<ViewMode>("list");
return <ViewToggle value={mode} onChange={setMode} />;
},
};

View File

@ -0,0 +1,79 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AlertBanner } from "./AlertBanner";
const meta: Meta<typeof AlertBanner> = {
title: "Molecules/AlertBanner",
component: AlertBanner,
argTypes: {
variant: { control: "select", options: ["success", "info", "warning", "error"] },
size: { control: "select", options: ["sm", "md"] },
elevated: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof AlertBanner>;
export const Info: Story = {
args: {
variant: "info",
title: "New feature available",
children: "Check out the new dashboard layout.",
},
};
export const Success: Story = {
args: {
variant: "success",
title: "Payment received",
children: "Your payment has been processed successfully.",
},
};
export const Warning: Story = {
args: {
variant: "warning",
title: "Account expiring",
children: "Your subscription expires in 3 days.",
},
};
export const Error: Story = {
args: {
variant: "error",
title: "Payment failed",
children: "Please update your payment method.",
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4 w-[500px]">
<AlertBanner variant="info" title="Info">
Informational message
</AlertBanner>
<AlertBanner variant="success" title="Success">
Operation completed
</AlertBanner>
<AlertBanner variant="warning" title="Warning">
Attention needed
</AlertBanner>
<AlertBanner variant="error" title="Error">
Something went wrong
</AlertBanner>
</div>
),
};
export const Closable: Story = {
args: {
variant: "info",
title: "Dismissible",
children: "Click the X to close.",
onClose: () => alert("Closed!"),
},
};
export const Small: Story = {
args: { variant: "warning", title: "Heads up", size: "sm" },
};

View File

@ -0,0 +1,54 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AnimatedCard } from "./AnimatedCard";
const meta: Meta<typeof AnimatedCard> = {
title: "Molecules/AnimatedCard",
component: AnimatedCard,
argTypes: {
variant: { control: "select", options: ["default", "highlighted", "success", "static"] },
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof AnimatedCard>;
export const Default: Story = {
args: {
children: <div className="p-6">Default animated card content</div>,
},
};
export const AllVariants: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
<AnimatedCard variant="default">
<div className="p-6">Default</div>
</AnimatedCard>
<AnimatedCard variant="highlighted">
<div className="p-6">Highlighted</div>
</AnimatedCard>
<AnimatedCard variant="success">
<div className="p-6">Success</div>
</AnimatedCard>
<AnimatedCard variant="static">
<div className="p-6">Static</div>
</AnimatedCard>
</div>
),
};
export const Interactive: Story = {
args: {
onClick: () => alert("Clicked!"),
children: <div className="p-6">Click me! (interactive card)</div>,
},
};
export const Disabled: Story = {
args: {
disabled: true,
onClick: () => {},
children: <div className="p-6">Disabled card</div>,
},
};

View File

@ -0,0 +1,31 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BackLink } from "./BackLink";
const meta: Meta<typeof BackLink> = {
title: "Molecules/BackLink",
component: BackLink,
argTypes: {
align: { control: "select", options: ["left", "center", "right"] },
},
};
export default meta;
type Story = StoryObj<typeof BackLink>;
export const Default: Story = {
args: { href: "/", label: "Back" },
};
export const CustomLabel: Story = {
args: { href: "/account", label: "Back to Account" },
};
export const Alignments: Story = {
render: () => (
<div className="flex flex-col gap-4 w-96">
<BackLink href="/" label="Left aligned" align="left" />
<BackLink href="/" label="Center aligned" align="center" />
<BackLink href="/" label="Right aligned" align="right" />
</div>
),
};

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ClearFiltersButton } from "./ClearFiltersButton";
const meta: Meta<typeof ClearFiltersButton> = {
title: "Molecules/ClearFiltersButton",
component: ClearFiltersButton,
};
export default meta;
type Story = StoryObj<typeof ClearFiltersButton>;
export const Default: Story = {
args: { onClick: () => alert("Cleared!"), show: true },
};
export const CustomLabel: Story = {
args: { onClick: () => {}, show: true, label: "Reset Filters" },
};
export const Hidden: Story = {
args: { onClick: () => {}, show: false },
};

View File

@ -0,0 +1,82 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DataTable } from "./DataTable";
import { InboxIcon } from "@heroicons/react/24/outline";
interface SampleRow {
id: number;
name: string;
email: string;
status: string;
amount: string;
}
const sampleData: SampleRow[] = [
{ id: 1, name: "John Doe", email: "john@example.com", status: "Active", amount: "¥12,000" },
{ id: 2, name: "Jane Smith", email: "jane@example.com", status: "Pending", amount: "¥8,500" },
{ id: 3, name: "Bob Wilson", email: "bob@example.com", status: "Active", amount: "¥15,200" },
{ id: 4, name: "Alice Brown", email: "alice@example.com", status: "Inactive", amount: "¥3,100" },
{
id: 5,
name: "Charlie Davis",
email: "charlie@example.com",
status: "Active",
amount: "¥9,800",
},
];
const columns = [
{ key: "name", header: "Name", render: (r: SampleRow) => r.name, primary: true },
{ key: "email", header: "Email", render: (r: SampleRow) => r.email },
{
key: "status",
header: "Status",
render: (r: SampleRow) => (
<span
className={
r.status === "Active"
? "text-success"
: r.status === "Pending"
? "text-warning"
: "text-muted-foreground"
}
>
{r.status}
</span>
),
},
{ key: "amount", header: "Amount", render: (r: SampleRow) => r.amount },
];
const meta: Meta<typeof DataTable<SampleRow>> = {
title: "Molecules/DataTable",
component: DataTable,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof DataTable<SampleRow>>;
export const Default: Story = {
args: { data: sampleData, columns, forceTableView: true },
};
export const Clickable: Story = {
args: {
data: sampleData,
columns,
onRowClick: item => alert(`Clicked: ${item.name}`),
forceTableView: true,
},
};
export const Empty: Story = {
args: {
data: [],
columns,
emptyState: {
icon: <InboxIcon className="h-12 w-12" />,
title: "No records found",
description: "Try adjusting your search or filters.",
},
},
};

View File

@ -0,0 +1,54 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DetailHeader } from "./DetailHeader";
import { DocumentTextIcon, WifiIcon } from "@heroicons/react/24/outline";
import { Button } from "../../atoms/button";
const meta: Meta<typeof DetailHeader> = {
title: "Molecules/DetailHeader",
component: DetailHeader,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof DetailHeader>;
export const Default: Story = {
args: {
title: "Order #12345",
subtitle: "Placed on March 1, 2026",
status: { label: "Active", variant: "success" },
},
};
export const WithIcon: Story = {
args: {
title: "Internet Plan - Fiber 1Gbps",
subtitle: "Subscription #SUB-789",
leftIcon: <WifiIcon className="h-8 w-8 text-primary" />,
status: { label: "Active", variant: "success" },
},
};
export const WithActions: Story = {
args: {
title: "Invoice #INV-2026-001",
subtitle: "Due: March 15, 2026",
leftIcon: <DocumentTextIcon className="h-8 w-8 text-info" />,
status: { label: "Pending", variant: "warning" },
actions: <Button size="sm">Pay Now</Button>,
},
};
export const WithMeta: Story = {
args: {
title: "Support Ticket #4567",
status: { label: "Open", variant: "info" },
meta: (
<div className="flex gap-4 text-sm text-muted-foreground">
<span>Priority: High</span>
<span>Category: Billing</span>
<span>Created: 2 hours ago</span>
</div>
),
},
};

View File

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DetailStatsGrid } from "./DetailStatsGrid";
import {
CalendarIcon,
CurrencyYenIcon,
DocumentTextIcon,
ClockIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof DetailStatsGrid> = {
title: "Molecules/DetailStatsGrid",
component: DetailStatsGrid,
argTypes: {
columns: { control: "select", options: [2, 3, 4] },
},
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof DetailStatsGrid>;
export const FourColumns: Story = {
args: {
columns: 4,
items: [
{ icon: <CalendarIcon className="h-5 w-5" />, label: "Start Date", value: "Jan 15, 2026" },
{ icon: <CurrencyYenIcon className="h-5 w-5" />, label: "Monthly Cost", value: "¥4,800" },
{ icon: <DocumentTextIcon className="h-5 w-5" />, label: "Contract", value: "24 months" },
{ icon: <ClockIcon className="h-5 w-5" />, label: "Next Billing", value: "Apr 1, 2026" },
],
},
};
export const ThreeColumns: Story = {
args: {
columns: 3,
items: [
{ icon: <CalendarIcon className="h-5 w-5" />, label: "Created", value: "Mar 1, 2026" },
{ icon: <CurrencyYenIcon className="h-5 w-5" />, label: "Total", value: "¥32,400" },
{ icon: <ClockIcon className="h-5 w-5" />, label: "Status", value: "Processing" },
],
},
};
export const TwoColumns: Story = {
args: {
columns: 2,
items: [
{ label: "Plan", value: "Fiber 1Gbps" },
{ label: "Speed", value: "Up to 1Gbps" },
],
},
};

View File

@ -0,0 +1,50 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { FilterDropdown } from "./FilterDropdown";
const meta: Meta<typeof FilterDropdown> = {
title: "Molecules/FilterDropdown",
component: FilterDropdown,
};
export default meta;
type Story = StoryObj<typeof FilterDropdown>;
export const Default: Story = {
render: function Render() {
const [value, setValue] = useState("all");
return (
<FilterDropdown
value={value}
onChange={setValue}
options={[
{ value: "all", label: "All Statuses" },
{ value: "active", label: "Active" },
{ value: "pending", label: "Pending" },
{ value: "cancelled", label: "Cancelled" },
]}
label="Filter by status"
/>
);
},
};
export const CustomWidth: Story = {
render: function Render() {
const [value, setValue] = useState("all");
return (
<FilterDropdown
value={value}
onChange={setValue}
options={[
{ value: "all", label: "All Categories" },
{ value: "billing", label: "Billing" },
{ value: "technical", label: "Technical" },
{ value: "general", label: "General" },
]}
label="Filter by category"
width="w-48"
/>
);
},
};

View File

@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FormField } from "./FormField";
const meta: Meta<typeof FormField> = {
title: "Molecules/FormField",
component: FormField,
};
export default meta;
type Story = StoryObj<typeof FormField>;
export const Default: Story = {
args: { label: "Email", placeholder: "you@example.com" },
};
export const Required: Story = {
args: { label: "Full Name", placeholder: "John Doe", required: true },
};
export const WithError: Story = {
args: {
label: "Email",
placeholder: "you@example.com",
error: "Invalid email address",
required: true,
},
};
export const WithHelperText: Story = {
args: { label: "Phone", placeholder: "+81 90-1234-5678", helperText: "Include country code" },
};
export const FormExample: Story = {
render: () => (
<div className="flex flex-col gap-4 w-80">
<FormField label="First Name" placeholder="John" required />
<FormField label="Last Name" placeholder="Doe" required />
<FormField label="Email" placeholder="you@example.com" type="email" required />
<FormField label="Phone" placeholder="+81 90-1234-5678" helperText="Optional" />
<FormField label="Address" error="Address is required" required />
</div>
),
};

View File

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";
import { LoadingCard } from "./loading-card";
import { LoadingTable } from "./loading-table";
import { LoadingStats } from "./loading-stats";
const meta: Meta = {
title: "Molecules/LoadingSkeletons",
parameters: { layout: "padded" },
};
export default meta;
export const Card: StoryObj = {
render: () => (
<div className="w-[400px]">
<LoadingCard />
</div>
),
};
export const MultipleCards: StoryObj = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
<LoadingCard />
<LoadingCard />
</div>
),
};
export const Table: StoryObj = {
render: () => (
<div className="w-[700px]">
<LoadingTable rows={5} columns={4} />
</div>
),
};
export const TableSmall: StoryObj = {
render: () => (
<div className="w-[500px]">
<LoadingTable rows={3} columns={3} />
</div>
),
};
export const Stats: StoryObj = {
render: () => <LoadingStats count={4} />,
};
export const StatsThree: StoryObj = {
render: () => <LoadingStats count={3} />,
};

View File

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MetricCard, MetricCardSkeleton } from "./MetricCard";
import {
CurrencyYenIcon,
UsersIcon,
DocumentTextIcon,
WifiIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof MetricCard> = {
title: "Molecules/MetricCard",
component: MetricCard,
argTypes: {
tone: {
control: "select",
options: ["primary", "success", "warning", "danger", "info", "neutral"],
},
},
};
export default meta;
type Story = StoryObj<typeof MetricCard>;
export const Default: Story = {
args: {
icon: <CurrencyYenIcon className="h-5 w-5" />,
label: "Total Revenue",
value: "¥1,234,567",
tone: "primary",
},
};
export const WithTrend: Story = {
args: {
icon: <UsersIcon className="h-5 w-5" />,
label: "Active Users",
value: "2,847",
tone: "success",
trend: { value: "+12.5%", positive: true },
},
};
export const AllTones: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
<MetricCard
icon={<CurrencyYenIcon className="h-5 w-5" />}
label="Revenue"
value="¥1.2M"
tone="primary"
/>
<MetricCard
icon={<UsersIcon className="h-5 w-5" />}
label="Users"
value="2,847"
tone="success"
trend={{ value: "+5%", positive: true }}
/>
<MetricCard
icon={<DocumentTextIcon className="h-5 w-5" />}
label="Pending"
value="23"
tone="warning"
/>
<MetricCard
icon={<WifiIcon className="h-5 w-5" />}
label="Downtime"
value="2hrs"
tone="danger"
trend={{ value: "+0.5%", positive: false }}
/>
</div>
),
};
export const LoadingSkeleton: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
<MetricCardSkeleton />
<MetricCardSkeleton />
</div>
),
};

View File

@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";
import { OtpExpiryDisplay } from "./OtpExpiryDisplay";
const meta: Meta<typeof OtpExpiryDisplay> = {
title: "Molecules/OtpExpiryDisplay",
component: OtpExpiryDisplay,
};
export default meta;
type Story = StoryObj<typeof OtpExpiryDisplay>;
export const CountingDown: Story = {
args: { timeRemaining: 245, isExpired: false },
};
export const AlmostExpired: Story = {
args: { timeRemaining: 15, isExpired: false },
};
export const Expired: Story = {
args: { timeRemaining: 0, isExpired: true },
};
export const NoTimer: Story = {
args: { timeRemaining: null, isExpired: false },
};

View File

@ -0,0 +1,46 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { OtpInput } from "./OtpInput";
const meta: Meta<typeof OtpInput> = {
title: "Molecules/OtpInput",
component: OtpInput,
};
export default meta;
type Story = StoryObj<typeof OtpInput>;
export const Default: Story = {
render: function Render() {
const [value, setValue] = useState("");
return <OtpInput value={value} onChange={setValue} autoFocus={false} />;
},
};
export const WithError: Story = {
render: function Render() {
const [value, setValue] = useState("123456");
return (
<OtpInput
value={value}
onChange={setValue}
error="Invalid code. Please try again."
autoFocus={false}
/>
);
},
};
export const Disabled: Story = {
render: function Render() {
const [value, setValue] = useState("123");
return <OtpInput value={value} onChange={setValue} disabled autoFocus={false} />;
},
};
export const FourDigit: Story = {
render: function Render() {
const [value, setValue] = useState("");
return <OtpInput value={value} onChange={setValue} length={4} autoFocus={false} />;
},
};

View File

@ -0,0 +1,39 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { PaginationBar } from "./PaginationBar";
const meta: Meta<typeof PaginationBar> = {
title: "Molecules/PaginationBar",
component: PaginationBar,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof PaginationBar>;
export const Default: Story = {
render: function Render() {
const [page, setPage] = useState(1);
return (
<div className="w-[600px]">
<PaginationBar currentPage={page} pageSize={10} totalItems={47} onPageChange={setPage} />
</div>
);
},
};
export const FirstPage: Story = {
args: { currentPage: 1, pageSize: 10, totalItems: 100, onPageChange: () => {} },
};
export const MiddlePage: Story = {
args: { currentPage: 5, pageSize: 10, totalItems: 100, onPageChange: () => {} },
};
export const LastPage: Story = {
args: { currentPage: 10, pageSize: 10, totalItems: 100, onPageChange: () => {} },
};
export const SinglePage: Story = {
args: { currentPage: 1, pageSize: 10, totalItems: 5, onPageChange: () => {} },
};

View File

@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ProgressSteps } from "./ProgressSteps";
const meta: Meta<typeof ProgressSteps> = {
title: "Molecules/ProgressSteps",
component: ProgressSteps,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof ProgressSteps>;
const steps = [
{ number: 1, title: "Plan", completed: false },
{ number: 2, title: "Details", completed: false },
{ number: 3, title: "Review", completed: false },
{ number: 4, title: "Payment", completed: false },
];
export const AtStart: Story = {
args: {
steps,
currentStep: 1,
},
};
export const InProgress: Story = {
args: {
steps: [
{ number: 1, title: "Plan", completed: true },
{ number: 2, title: "Details", completed: true },
{ number: 3, title: "Review", completed: false },
{ number: 4, title: "Payment", completed: false },
],
currentStep: 3,
},
};
export const AllComplete: Story = {
args: {
steps: steps.map(s => ({ ...s, completed: true })),
},
};

View File

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react";
// RouteLoading depends on PageLayout which is a template - show it as a simple skeleton
const meta: Meta = {
title: "Molecules/RouteLoading",
parameters: { layout: "fullscreen" },
};
export default meta;
export const Default: StoryObj = {
render: () => (
<div className="bg-background min-h-[400px]">
<div className="bg-muted/40 border-b border-border/40 h-16 flex items-center px-8">
<div className="h-6 w-36 bg-muted rounded animate-pulse" />
</div>
<div className="p-8 space-y-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-muted animate-pulse" />
<div className="space-y-2">
<div className="h-6 w-48 bg-muted rounded animate-pulse" />
<div className="h-4 w-64 bg-muted rounded animate-pulse" />
</div>
</div>
{[1, 2, 3].map(i => (
<div key={i} className="bg-card border border-border rounded-lg p-4 shadow-sm">
<div className="h-4 w-1/2 bg-muted rounded animate-pulse mb-2" />
<div className="h-3 w-3/4 bg-muted rounded animate-pulse" />
</div>
))}
</div>
</div>
),
};

View File

@ -0,0 +1,70 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { SearchFilterBar } from "./SearchFilterBar";
const meta: Meta<typeof SearchFilterBar> = {
title: "Molecules/SearchFilterBar",
component: SearchFilterBar,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof SearchFilterBar>;
export const SearchOnly: Story = {
render: function Render() {
const [search, setSearch] = useState("");
return (
<div className="w-[600px]">
<SearchFilterBar
searchValue={search}
onSearchChange={setSearch}
searchPlaceholder="Search orders..."
/>
</div>
);
},
};
export const WithFilter: Story = {
render: function Render() {
const [search, setSearch] = useState("");
const [filter, setFilter] = useState("all");
return (
<div className="w-[600px]">
<SearchFilterBar
searchValue={search}
onSearchChange={setSearch}
searchPlaceholder="Search invoices..."
filterValue={filter}
onFilterChange={setFilter}
filterOptions={[
{ value: "all", label: "All" },
{ value: "paid", label: "Paid" },
{ value: "unpaid", label: "Unpaid" },
{ value: "overdue", label: "Overdue" },
]}
filterLabel="Status"
/>
</div>
);
},
};
export const WithActiveFilters: Story = {
render: function Render() {
const [search, setSearch] = useState("fiber");
return (
<div className="w-[600px]">
<SearchFilterBar
searchValue={search}
onSearchChange={setSearch}
activeFilters={[
{ label: "Status: Active", onRemove: () => {} },
{ label: "Type: Internet", onRemove: () => {} },
]}
/>
</div>
);
},
};

View File

@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SectionCard } from "./SectionCard";
import { CreditCardIcon, UserIcon } from "@heroicons/react/24/outline";
import { Button } from "../../atoms/button";
const meta: Meta<typeof SectionCard> = {
title: "Molecules/SectionCard",
component: SectionCard,
argTypes: {
tone: {
control: "select",
options: ["primary", "success", "info", "warning", "danger", "neutral"],
},
},
};
export default meta;
type Story = StoryObj<typeof SectionCard>;
export const Default: Story = {
args: {
icon: <CreditCardIcon className="h-5 w-5" />,
title: "Payment Methods",
subtitle: "Manage your payment options",
children: (
<p className="text-sm text-muted-foreground">Your saved payment methods will appear here.</p>
),
},
};
export const WithActions: Story = {
args: {
icon: <UserIcon className="h-5 w-5" />,
title: "Account Details",
subtitle: "Your personal information",
tone: "info",
actions: (
<Button size="sm" variant="outline">
Edit
</Button>
),
children: (
<div className="space-y-2 text-sm">
<p>
<span className="text-muted-foreground">Name:</span> John Doe
</p>
<p>
<span className="text-muted-foreground">Email:</span> john@example.com
</p>
</div>
),
},
};
export const AllTones: Story = {
render: () => (
<div className="flex flex-col gap-4 w-[500px]">
<SectionCard icon={<CreditCardIcon className="h-5 w-5" />} title="Primary" tone="primary">
Content
</SectionCard>
<SectionCard icon={<CreditCardIcon className="h-5 w-5" />} title="Success" tone="success">
Content
</SectionCard>
<SectionCard icon={<CreditCardIcon className="h-5 w-5" />} title="Warning" tone="warning">
Content
</SectionCard>
<SectionCard icon={<CreditCardIcon className="h-5 w-5" />} title="Danger" tone="danger">
Content
</SectionCard>
</div>
),
};

View File

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SectionHeader } from "./SectionHeader";
import { Button } from "../../atoms/button";
const meta: Meta<typeof SectionHeader> = {
title: "Molecules/SectionHeader",
component: SectionHeader,
};
export default meta;
type Story = StoryObj<typeof SectionHeader>;
export const Default: Story = {
args: { title: "Recent Orders" },
};
export const WithAction: Story = {
render: () => (
<div className="w-[500px]">
<SectionHeader title="Subscriptions">
<Button size="sm" variant="outline">
View All
</Button>
</SectionHeader>
</div>
),
};

View File

@ -0,0 +1,151 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ServiceCard } from "./ServiceCard";
import {
WifiIcon,
DevicePhoneMobileIcon,
ShieldCheckIcon,
ServerIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof ServiceCard> = {
title: "Molecules/ServiceCard",
component: ServiceCard,
argTypes: {
variant: {
control: "select",
options: ["default", "featured", "minimal", "bento-sm", "bento-md", "bento-lg"],
},
accentColor: {
control: "select",
options: ["blue", "green", "purple", "orange", "cyan", "pink", "amber", "rose"],
},
},
};
export default meta;
type Story = StoryObj<typeof ServiceCard>;
export const Default: Story = {
args: {
icon: <WifiIcon className="h-6 w-6" />,
title: "Internet",
description: "High-speed fiber internet for your home or office",
price: "¥3,200/mo",
accentColor: "blue",
},
};
export const Featured: Story = {
args: {
variant: "featured",
icon: <DevicePhoneMobileIcon className="h-6 w-6" />,
title: "SIM & eSIM",
description: "Mobile data plans with flexible options",
highlight: "1st month free",
},
};
export const Minimal: Story = {
args: {
variant: "minimal",
icon: <ShieldCheckIcon className="h-6 w-6" />,
title: "VPN",
},
};
export const WithBadge: Story = {
args: {
icon: <WifiIcon className="h-6 w-6" />,
title: "Fiber Internet",
description: "Ultra-fast connection up to 10Gbps",
price: "¥4,800/mo",
badge: "Popular",
accentColor: "blue",
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-6 w-[400px]">
<ServiceCard
icon={<WifiIcon className="h-6 w-6" />}
title="Default"
description="Standard card"
price="¥3,200/mo"
accentColor="blue"
/>
<ServiceCard
variant="featured"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
title="Featured"
description="Premium styling"
highlight="New"
/>
<ServiceCard
variant="minimal"
icon={<ShieldCheckIcon className="h-6 w-6" />}
title="Minimal"
/>
</div>
),
};
export const BentoGrid: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
<div className="col-span-2">
<ServiceCard
variant="bento-lg"
icon={<WifiIcon className="h-7 w-7" />}
title="Internet"
description="High-speed fiber for home and office"
accentColor="blue"
/>
</div>
<ServiceCard
variant="bento-md"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
title="Mobile"
description="SIM & eSIM plans"
accentColor="green"
/>
<ServiceCard
variant="bento-md"
icon={<ShieldCheckIcon className="h-6 w-6" />}
title="VPN"
description="Secure browsing"
accentColor="purple"
/>
<ServiceCard
variant="bento-sm"
icon={<ServerIcon className="h-5 w-5" />}
title="Hosting"
accentColor="orange"
/>
<ServiceCard
variant="bento-sm"
icon={<WifiIcon className="h-5 w-5" />}
title="WiFi Router"
accentColor="cyan"
/>
</div>
),
};
export const AccentColors: Story = {
render: () => (
<div className="grid grid-cols-2 gap-4 w-[600px]">
{(["blue", "green", "purple", "orange", "cyan", "pink", "amber", "rose"] as const).map(
color => (
<ServiceCard
key={color}
icon={<WifiIcon className="h-6 w-6" />}
title={color.charAt(0).toUpperCase() + color.slice(1)}
description={`${color} accent`}
accentColor={color}
/>
)
)}
</div>
),
};

View File

@ -0,0 +1,64 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StatusBadge } from "./status-badge";
import {
CheckCircleIcon,
ClockIcon,
XCircleIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import type { StatusConfigMap } from "./types";
const orderStatusConfig: StatusConfigMap = {
active: {
variant: "success",
icon: <CheckCircleIcon className="h-3.5 w-3.5" />,
label: "Active",
},
pending: { variant: "warning", icon: <ClockIcon className="h-3.5 w-3.5" />, label: "Pending" },
cancelled: {
variant: "error",
icon: <XCircleIcon className="h-3.5 w-3.5" />,
label: "Cancelled",
},
suspended: {
variant: "warning",
icon: <ExclamationTriangleIcon className="h-3.5 w-3.5" />,
label: "Suspended",
},
};
const meta: Meta<typeof StatusBadge> = {
title: "Molecules/StatusBadge",
component: StatusBadge,
};
export default meta;
type Story = StoryObj<typeof StatusBadge>;
export const Active: Story = {
args: { status: "Active", configMap: orderStatusConfig },
};
export const AllStatuses: Story = {
render: () => (
<div className="flex flex-wrap gap-3">
<StatusBadge status="Active" configMap={orderStatusConfig} />
<StatusBadge status="Pending" configMap={orderStatusConfig} />
<StatusBadge status="Cancelled" configMap={orderStatusConfig} />
<StatusBadge status="Suspended" configMap={orderStatusConfig} />
</div>
),
};
export const WithoutIcons: Story = {
render: () => (
<div className="flex flex-wrap gap-3">
<StatusBadge status="Active" configMap={orderStatusConfig} showIcon={false} />
<StatusBadge status="Pending" configMap={orderStatusConfig} showIcon={false} />
</div>
),
};
export const UnknownStatus: Story = {
args: { status: "Unknown", configMap: orderStatusConfig },
};

View File

@ -0,0 +1,74 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SubCard } from "./SubCard";
import { CreditCardIcon, CogIcon } from "@heroicons/react/24/outline";
import { Button } from "../../atoms/button";
const meta: Meta<typeof SubCard> = {
title: "Molecules/SubCard",
component: SubCard,
};
export default meta;
type Story = StoryObj<typeof SubCard>;
export const Default: Story = {
args: {
title: "Payment Information",
children: (
<p className="text-sm text-muted-foreground">Your payment details will appear here.</p>
),
},
};
export const WithIcon: Story = {
args: {
title: "Billing Details",
icon: <CreditCardIcon className="h-5 w-5" />,
children: (
<div className="space-y-2 text-sm">
<p>
<span className="text-muted-foreground">Card:</span> **** **** **** 4242
</p>
<p>
<span className="text-muted-foreground">Expires:</span> 12/2027
</p>
</div>
),
},
};
export const WithHeaderRight: Story = {
args: {
title: "Settings",
icon: <CogIcon className="h-5 w-5" />,
right: (
<Button size="sm" variant="outline">
Edit
</Button>
),
children: <p className="text-sm text-muted-foreground">Manage your preferences.</p>,
},
};
export const WithFooter: Story = {
args: {
title: "Subscription",
children: <p className="text-sm">Fiber Internet 1Gbps - Active</p>,
footer: (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Next billing: April 1</span>
<Button size="sm" variant="outline">
Manage
</Button>
</div>
),
},
};
export const Interactive: Story = {
args: {
interactive: true,
title: "Click me",
children: <p className="text-sm text-muted-foreground">This card has hover effects.</p>,
},
};

View File

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SummaryStats } from "./SummaryStats";
import {
DocumentTextIcon,
CurrencyYenIcon,
ClockIcon,
CheckCircleIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof SummaryStats> = {
title: "Molecules/SummaryStats",
component: SummaryStats,
argTypes: {
variant: { control: "select", options: ["inline", "cards"] },
},
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof SummaryStats>;
const items = [
{
icon: <DocumentTextIcon className="h-5 w-5" />,
label: "Total Orders",
value: 24,
tone: "primary" as const,
},
{
icon: <CurrencyYenIcon className="h-5 w-5" />,
label: "Revenue",
value: "¥1.2M",
tone: "success" as const,
},
{ icon: <ClockIcon className="h-5 w-5" />, label: "Pending", value: 3, tone: "warning" as const },
];
export const Inline: Story = {
args: { items, variant: "inline" },
};
export const Cards: Story = {
args: { items, variant: "cards" },
};
export const WithTones: Story = {
args: {
variant: "cards",
items: [
{
icon: <CheckCircleIcon className="h-5 w-5" />,
label: "Active",
value: 12,
tone: "success",
},
{ icon: <ClockIcon className="h-5 w-5" />, label: "Pending", value: 3, tone: "warning" },
{
icon: <CurrencyYenIcon className="h-5 w-5" />,
label: "Total Spent",
value: "¥89,400",
tone: "info",
},
],
},
};

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react";
import { GlobalErrorFallback, PageErrorFallback } from "./error-fallbacks";
const meta: Meta = {
title: "Molecules/ErrorFallbacks",
parameters: { layout: "fullscreen" },
};
export default meta;
export const GlobalError: StoryObj = {
render: () => <GlobalErrorFallback />,
};
export const PageError: StoryObj = {
render: () => (
<div className="bg-background p-8">
<PageErrorFallback />
</div>
),
};

View File

@ -0,0 +1,13 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SiteFooter } from "./SiteFooter";
const meta: Meta<typeof SiteFooter> = {
title: "Organisms/SiteFooter",
component: SiteFooter,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof SiteFooter>;
export const Default: Story = {};

View File

@ -0,0 +1,87 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AuthLayout } from "./AuthLayout";
const meta: Meta<typeof AuthLayout> = {
title: "Templates/AuthLayout",
component: AuthLayout,
parameters: { layout: "fullscreen" },
argTypes: {
wide: { control: "boolean" },
showBackButton: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof AuthLayout>;
export const Login: Story = {
args: {
title: "Welcome back",
subtitle: "Sign in to your account to continue",
children: (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">Email</label>
<input
className="w-full h-11 px-4 rounded-lg border border-border bg-card text-sm"
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">Password</label>
<input
type="password"
className="w-full h-11 px-4 rounded-lg border border-border bg-card text-sm"
placeholder="Enter password"
/>
</div>
<button className="w-full h-11 rounded-lg bg-primary text-primary-foreground font-medium text-sm">
Sign in
</button>
</div>
),
},
};
export const SignUp: Story = {
args: {
title: "Create your account",
subtitle: "Get started with Assist Solutions services",
wide: true,
showBackButton: true,
children: (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
First Name
</label>
<input
className="w-full h-11 px-4 rounded-lg border border-border bg-card text-sm"
placeholder="John"
/>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Last Name
</label>
<input
className="w-full h-11 px-4 rounded-lg border border-border bg-card text-sm"
placeholder="Doe"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">Email</label>
<input
className="w-full h-11 px-4 rounded-lg border border-border bg-card text-sm"
placeholder="you@example.com"
/>
</div>
<button className="w-full h-11 rounded-lg bg-primary text-primary-foreground font-medium text-sm">
Create Account
</button>
</div>
),
},
};

View File

@ -0,0 +1,71 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PageLayout } from "./PageLayout";
import { Button } from "../../atoms/button";
import { StatusPill } from "../../atoms/status-pill";
const meta: Meta<typeof PageLayout> = {
title: "Templates/PageLayout",
component: PageLayout,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof PageLayout>;
export const Default: Story = {
args: {
title: "Dashboard",
description: "Overview of your account",
children: (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
{["Services", "Invoices", "Support"].map(title => (
<div key={title} className="bg-card border border-border rounded-xl p-6 shadow-sm">
<h3 className="font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground mt-1">
Manage your {title.toLowerCase()}
</p>
</div>
))}
</div>
</div>
),
},
};
export const WithActions: Story = {
args: {
title: "Subscriptions",
description: "Manage your active services",
actions: <Button size="sm">Add Service</Button>,
children: <div className="bg-card border border-border rounded-xl p-6">Content area</div>,
},
};
export const WithBackLink: Story = {
args: {
title: "Order #12345",
backLink: { label: "Orders", href: "/orders" },
statusPill: <StatusPill label="Active" variant="success" />,
children: (
<div className="bg-card border border-border rounded-xl p-6">Order details content</div>
),
},
};
export const Loading: Story = {
args: {
title: "Loading Page",
loading: true,
children: <div>This won't be shown</div>,
},
};
export const Error: Story = {
args: {
title: "Error Page",
error: "Failed to load data from the server",
onRetry: () => alert("Retrying..."),
children: <div>This won't be shown</div>,
},
};

View File

@ -0,0 +1,62 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "./input-otp";
import { REGEXP_ONLY_DIGITS } from "input-otp";
const meta: Meta = {
title: "UI/InputOTP",
parameters: { layout: "centered" },
};
export default meta;
export const SixDigit: StoryObj = {
render: function Render() {
const [value, setValue] = useState("");
return (
<InputOTP maxLength={6} value={value} onChange={setValue} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map(i => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>
);
},
};
export const WithSeparator: StoryObj = {
render: function Render() {
const [value, setValue] = useState("");
return (
<InputOTP maxLength={6} value={value} onChange={setValue} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
);
},
};
export const FourDigit: StoryObj = {
render: function Render() {
const [value, setValue] = useState("");
return (
<InputOTP maxLength={4} value={value} onChange={setValue} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
{[0, 1, 2, 3].map(i => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>
);
},
};

View File

@ -0,0 +1,68 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AddressCard } from "./AddressCard";
/**
* AddressCard uses AddressForm internally (for editing mode).
* The non-editing stories render without AddressForm so they work standalone.
* Editing stories may need the AddressForm module available.
*/
const mockAddress = {
address1: "Sunshine Mansion 201",
address2: "Nishi-Shinjuku 1-5-3",
city: "Shinjuku-ku",
state: "Tokyo",
postcode: "160-0023",
country: "JP",
countryCode: "JP",
phoneNumber: "+81 90-1234-5678",
phoneCountryCode: "81",
};
const emptyAddress = {
address1: "",
address2: "",
city: "",
state: "",
postcode: "",
country: "",
countryCode: "",
phoneNumber: "",
phoneCountryCode: "",
};
const meta: Meta<typeof AddressCard> = {
title: "Features/Account/AddressCard",
component: AddressCard,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof AddressCard>;
export const Default: Story = {
args: {
address: mockAddress,
isEditing: false,
isSaving: false,
error: null,
onEdit: () => {},
onCancel: () => {},
onSave: () => {},
onAddressChange: () => {},
},
};
export const EmptyAddress: Story = {
args: {
...Default.args,
address: emptyAddress,
},
};
export const WithError: Story = {
args: {
...Default.args,
isEditing: true,
error: "Failed to save address. Please try again.",
},
};

View File

@ -0,0 +1,69 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PasswordChangeCard } from "./PasswordChangeCard";
const meta: Meta<typeof PasswordChangeCard> = {
title: "Features/Account/PasswordChangeCard",
component: PasswordChangeCard,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof PasswordChangeCard>;
export const Default: Story = {
args: {
isChanging: false,
error: null,
success: null,
form: { currentPassword: "", newPassword: "", confirmPassword: "" },
setForm: () => {},
onSubmit: () => {},
},
};
export const Filled: Story = {
args: {
...Default.args,
form: {
currentPassword: "oldPassword123",
newPassword: "NewSecure!Pass1",
confirmPassword: "NewSecure!Pass1",
},
},
};
export const PasswordMismatch: Story = {
args: {
...Default.args,
form: {
currentPassword: "oldPassword123",
newPassword: "NewSecure!Pass1",
confirmPassword: "Different!Pass2",
},
},
};
export const Changing: Story = {
args: {
...Default.args,
isChanging: true,
form: {
currentPassword: "oldPassword123",
newPassword: "NewSecure!Pass1",
confirmPassword: "NewSecure!Pass1",
},
},
};
export const WithError: Story = {
args: {
...Default.args,
error: "Current password is incorrect.",
},
};
export const WithSuccess: Story = {
args: {
...Default.args,
success: "Password changed successfully!",
},
};

View File

@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PersonalInfoCard } from "./PersonalInfoCard";
const meta: Meta<typeof PersonalInfoCard> = {
title: "Features/Account/PersonalInfoCard",
component: PersonalInfoCard,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof PersonalInfoCard>;
const mockData = {
firstname: "Taro",
lastname: "Yamada",
email: "taro.yamada@example.com",
phonenumber: "+81 90-1234-5678",
sfNumber: "SF-001234",
dateOfBirth: "1990-05-15",
gender: "Male",
};
export const Default: Story = {
args: {
data: mockData,
editEmail: mockData.email,
editPhoneNumber: mockData.phonenumber ?? "",
isEditing: false,
isSaving: false,
onEdit: () => {},
onCancel: () => {},
onChange: () => {},
onSave: () => {},
},
};
export const Editing: Story = {
args: {
...Default.args,
isEditing: true,
},
};
export const Saving: Story = {
args: {
...Default.args,
isEditing: true,
isSaving: true,
},
};
export const MissingFields: Story = {
args: {
...Default.args,
data: {
firstname: null,
lastname: null,
email: "user@example.com",
phonenumber: null,
sfNumber: null,
dateOfBirth: null,
gender: null,
},
editEmail: "user@example.com",
editPhoneNumber: "",
},
};

View File

@ -0,0 +1,119 @@
import type { Meta, StoryObj } from "@storybook/react";
import { createRef } from "react";
import { VerificationCard } from "./VerificationCard";
/**
* VerificationCard requires complex query/upload objects.
* We mock them inline with static data to avoid importing hooks.
*/
const noopFileUpload = {
canUpload: false,
file: null,
isSubmitting: false,
isError: false,
error: null,
inputRef: createRef<HTMLInputElement>(),
handleFileChange: () => {},
clearFile: () => {},
submit: () => {},
};
const uploadableFileUpload = {
...noopFileUpload,
canUpload: true,
};
function makeQuery(overrides: Record<string, unknown> = {}) {
return {
data: undefined,
error: null,
isLoading: false,
isError: false,
isSuccess: false,
isPending: false,
isFetching: false,
isRefetching: false,
status: "success" as const,
fetchStatus: "idle" as const,
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isInitialLoading: false,
isLoadingError: false,
isPlaceholderData: false,
isRefetchError: false,
isStale: false,
refetch: async () => ({}) as never,
promise: Promise.resolve({} as never),
...overrides,
} as never;
}
const meta: Meta<typeof VerificationCard> = {
title: "Features/Account/VerificationCard",
component: VerificationCard,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof VerificationCard>;
export const Loading: Story = {
args: {
verificationQuery: makeQuery({ isLoading: true, data: undefined }),
fileUpload: noopFileUpload as never,
},
};
export const Verified: Story = {
args: {
verificationQuery: makeQuery({
data: {
status: "verified",
submittedAt: "2025-01-10T12:00:00Z",
reviewedAt: "2025-01-12T09:30:00Z",
reviewerNotes: null,
},
}),
fileUpload: noopFileUpload as never,
},
};
export const Pending: Story = {
args: {
verificationQuery: makeQuery({
data: {
status: "pending",
submittedAt: "2025-02-01T08:00:00Z",
reviewedAt: null,
reviewerNotes: null,
},
}),
fileUpload: noopFileUpload as never,
},
};
export const Rejected: Story = {
args: {
verificationQuery: makeQuery({
data: {
status: "rejected",
submittedAt: "2025-01-15T10:00:00Z",
reviewedAt: "2025-01-17T14:00:00Z",
reviewerNotes: "The photo is too blurry. Please upload a clearer image.",
},
}),
fileUpload: uploadableFileUpload as never,
},
};
export const NotSubmitted: Story = {
args: {
verificationQuery: makeQuery({ data: undefined }),
fileUpload: uploadableFileUpload as never,
},
};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ProfileLoadingSkeleton } from "./ProfileLoadingSkeleton";
const meta: Meta<typeof ProfileLoadingSkeleton> = {
title: "Features/Account/ProfileLoadingSkeleton",
component: ProfileLoadingSkeleton,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof ProfileLoadingSkeleton>;
export const Default: Story = {};

View File

@ -0,0 +1,95 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AddressStepJapan } from "./AddressStepJapan";
/**
* AddressStepJapan is an integration adapter that wraps JapanAddressForm.
* It expects a form interface with values, errors, touched, setValue, and setTouchedField.
* We mock this form interface with static data.
*/
const emptyForm = {
values: {
address: {
address1: "",
address2: "",
city: "",
state: "",
postcode: "",
country: "JP",
countryCode: "JP",
},
},
errors: {} as Record<string, string | undefined>,
touched: {} as Record<string, boolean | undefined>,
setValue: () => {},
setTouchedField: () => {},
};
const filledForm = {
values: {
address: {
address1: "Sunshine Mansion 201",
address2: "Nishi-Shinjuku",
city: "Shinjuku-ku",
state: "Tokyo",
postcode: "160-0023",
country: "JP",
countryCode: "JP",
},
},
errors: {} as Record<string, string | undefined>,
touched: {} as Record<string, boolean | undefined>,
setValue: () => {},
setTouchedField: () => {},
};
const formWithErrors = {
values: {
address: {
address1: "",
address2: "",
city: "",
state: "",
postcode: "",
country: "JP",
countryCode: "JP",
},
},
errors: {
"address.postcode": "Postal code is required",
"address.state": "Prefecture is required",
} as Record<string, string | undefined>,
touched: {
address: true,
"address.postcode": true,
"address.state": true,
} as Record<string, boolean | undefined>,
setValue: () => {},
setTouchedField: () => {},
};
const meta: Meta<typeof AddressStepJapan> = {
title: "Features/Address/AddressStepJapan",
component: AddressStepJapan,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof AddressStepJapan>;
export const Empty: Story = {
args: {
form: emptyForm,
},
};
export const WithExistingAddress: Story = {
args: {
form: filledForm,
},
};
export const WithValidationErrors: Story = {
args: {
form: formWithErrors,
},
};

View File

@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AnimatedSection } from "./AnimatedSection";
const meta: Meta<typeof AnimatedSection> = {
title: "Features/Address/AnimatedSection",
component: AnimatedSection,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof AnimatedSection>;
export const Visible: Story = {
args: {
show: true,
delay: 0,
children: (
<div className="p-4 border border-border rounded-lg bg-card">
<p className="text-foreground">This content is visible with animation.</p>
</div>
),
},
};
export const Hidden: Story = {
args: {
show: false,
delay: 0,
children: (
<div className="p-4 border border-border rounded-lg bg-card">
<p className="text-foreground">This content is hidden.</p>
</div>
),
},
};
export const WithDelay: Story = {
args: {
show: true,
delay: 300,
children: (
<div className="p-4 border border-border rounded-lg bg-card">
<p className="text-foreground">This content appears with a 300ms delay.</p>
</div>
),
},
};

View File

@ -0,0 +1,36 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BilingualValue } from "./BilingualValue";
const meta: Meta<typeof BilingualValue> = {
title: "Features/Address/BilingualValue",
component: BilingualValue,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof BilingualValue>;
export const Verified: Story = {
args: {
romaji: "Shinjuku-ku",
japanese: "新宿区",
placeholder: "—",
verified: true,
},
};
export const VerifiedWithoutJapanese: Story = {
args: {
romaji: "Shinjuku-ku",
placeholder: "—",
verified: true,
},
};
export const NotVerified: Story = {
args: {
romaji: "",
japanese: "",
placeholder: "Awaiting verification",
verified: false,
},
};

View File

@ -0,0 +1,87 @@
import type { Meta, StoryObj } from "@storybook/react";
import { JapanAddressForm } from "./JapanAddressForm";
/**
* JapanAddressForm uses useJapanAddressForm internally.
* Stories provide initial values; interactive behavior depends on the hook
* being functional in the Storybook environment.
*/
const meta: Meta<typeof JapanAddressForm> = {
title: "Features/Address/JapanAddressForm",
component: JapanAddressForm,
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof JapanAddressForm>;
export const Empty: Story = {
args: {
onChange: () => {},
disabled: false,
},
};
export const WithInitialValues: Story = {
args: {
initialValues: {
postcode: "160-0023",
prefecture: "Tokyo",
prefectureJa: "東京都",
city: "Shinjuku-ku",
cityJa: "新宿区",
town: "Nishi-Shinjuku",
townJa: "西新宿",
streetAddress: "1-5-3",
buildingName: "Sunshine Mansion",
roomNumber: "201",
residenceType: "apartment" as const,
},
onChange: () => {},
disabled: false,
},
};
export const WithErrors: Story = {
args: {
initialValues: {
postcode: "",
prefecture: "",
prefectureJa: "",
city: "",
cityJa: "",
town: "",
townJa: "",
streetAddress: "",
buildingName: "",
roomNumber: "",
residenceType: undefined,
},
errors: {
postcode: "Postal code is required",
},
touched: {
postcode: true,
},
onChange: () => {},
disabled: false,
},
};
export const Disabled: Story = {
args: {
...WithInitialValues.args,
disabled: true,
},
};
export const WithCustomCompletion: Story = {
args: {
...WithInitialValues.args,
completionContent: (
<div className="rounded-xl bg-blue-50 border border-blue-200 p-4 text-blue-800 text-sm">
Custom completion message: Your address has been saved.
</div>
),
},
};

View File

@ -0,0 +1,45 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ProgressIndicator } from "./ProgressIndicator";
const meta: Meta<typeof ProgressIndicator> = {
title: "Features/Address/ProgressIndicator",
component: ProgressIndicator,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof ProgressIndicator>;
export const FirstStep: Story = {
args: {
currentStep: 0,
totalSteps: 4,
},
};
export const MiddleStep: Story = {
args: {
currentStep: 2,
totalSteps: 4,
},
};
export const LastStep: Story = {
args: {
currentStep: 3,
totalSteps: 4,
},
};
export const AllComplete: Story = {
args: {
currentStep: 4,
totalSteps: 4,
},
};
export const ManySteps: Story = {
args: {
currentStep: 3,
totalSteps: 7,
},
};

View File

@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ZipCodeInput } from "./ZipCodeInput";
/**
* ZipCodeInput uses an internal useZipCodeLookup hook.
* Stories provide static props; the lookup will not fire in isolation
* unless the hook's API endpoint is available. Visual appearance is
* still rendered correctly from the props.
*/
const meta: Meta<typeof ZipCodeInput> = {
title: "Features/Address/ZipCodeInput",
component: ZipCodeInput,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof ZipCodeInput>;
export const Empty: Story = {
args: {
value: "",
onChange: () => {},
required: true,
label: "Postal Code",
helperText: "Format: XXX-XXXX",
autoLookup: false,
},
};
export const WithValue: Story = {
args: {
...Empty.args,
value: "160-0023",
},
};
export const WithError: Story = {
args: {
...Empty.args,
value: "123",
error: "Please enter a valid 7-digit ZIP code.",
},
};
export const Disabled: Story = {
args: {
...Empty.args,
value: "160-0023",
disabled: true,
},
};
export const CustomLabel: Story = {
args: {
...Empty.args,
label: "ZIP Code (Japan)",
helperText: "Enter your Japanese postal code",
},
};

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MarketingCheckbox } from "./MarketingCheckbox";
const meta: Meta<typeof MarketingCheckbox> = {
title: "Features/Auth/MarketingCheckbox",
component: MarketingCheckbox,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof MarketingCheckbox>;
export const Unchecked: Story = {
args: {
checked: false,
onChange: () => {},
},
};
export const Checked: Story = {
args: {
checked: true,
onChange: () => {},
},
};
export const Disabled: Story = {
args: {
checked: false,
onChange: () => {},
disabled: true,
},
};

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PasswordMatchIndicator } from "./PasswordMatchIndicator";
const meta: Meta<typeof PasswordMatchIndicator> = {
title: "Features/Auth/PasswordMatchIndicator",
component: PasswordMatchIndicator,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof PasswordMatchIndicator>;
export const Matching: Story = {
args: {
passwordsMatch: true,
},
};
export const NotMatching: Story = {
args: {
passwordsMatch: false,
},
};

View File

@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PasswordRequirements } from "./PasswordRequirements";
const meta: Meta<typeof PasswordRequirements> = {
title: "Features/Auth/PasswordRequirements",
component: PasswordRequirements,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof PasswordRequirements>;
export const AllMet: Story = {
args: {
checks: {
minLength: true,
hasUppercase: true,
hasLowercase: true,
hasNumber: true,
},
},
};
export const NoneMet: Story = {
args: {
checks: {
minLength: false,
hasUppercase: false,
hasLowercase: false,
hasNumber: false,
},
},
};
export const PartiallyMet: Story = {
args: {
checks: {
minLength: true,
hasUppercase: true,
hasLowercase: false,
hasNumber: false,
},
},
};
export const HintMode: Story = {
args: {
checks: {
minLength: false,
hasUppercase: false,
hasLowercase: false,
hasNumber: false,
},
showHint: true,
},
};

View File

@ -0,0 +1,93 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { PasswordSection } from "./PasswordSection";
const meta: Meta<typeof PasswordSection> = {
title: "Features/Auth/PasswordSection",
component: PasswordSection,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[400px] space-y-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof PasswordSection>;
export const Empty: Story = {
args: {
password: "",
confirmPassword: "",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const WithPassword: Story = {
args: {
password: "MyPass123",
confirmPassword: "",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const PasswordsMatch: Story = {
args: {
password: "MyPass123",
confirmPassword: "MyPass123",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const PasswordsMismatch: Story = {
args: {
password: "MyPass123",
confirmPassword: "MyPass456",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const WithErrors: Story = {
args: {
password: "short",
confirmPassword: "",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {
password: "Password does not meet requirements",
confirmPassword: "Please confirm your password",
},
clearError: () => {},
loading: false,
},
};
export const Loading: Story = {
args: {
password: "MyPass123",
confirmPassword: "MyPass123",
onPasswordChange: () => {},
onConfirmPasswordChange: () => {},
errors: {},
clearError: () => {},
loading: true,
},
};

View File

@ -0,0 +1,76 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Button } from "@/components/atoms/button";
/**
* SessionTimeoutWarning is a container component that relies on auth session
* stores and hooks internally. It renders nothing unless the session is about
* to expire. Instead, we render the visual TimeoutDialog inline to showcase
* the UI.
*/
function TimeoutDialogPreview({
timeLeft = 5,
onExtend = () => {},
onLogout = () => {},
}: {
timeLeft?: number;
onExtend?: () => void;
onLogout?: () => void;
}) {
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
role="presentation"
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="session-timeout-title"
aria-describedby="session-timeout-description"
tabIndex={-1}
className="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl outline-none"
>
<div className="flex items-center gap-2 mb-4">
<span className="text-yellow-500 text-xl">&#9888;&#65039;</span>
<h2 id="session-timeout-title" className="text-lg font-semibold">
Session Expiring Soon
</h2>
</div>
<p id="session-timeout-description" className="text-gray-600 mb-6">
Your session will expire in{" "}
<strong>
{timeLeft} minute{timeLeft === 1 ? "" : "s"}
</strong>
. Would you like to extend your session?
</p>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={onLogout}>
Logout Now
</Button>
<Button onClick={onExtend}>Extend Session</Button>
</div>
</div>
</div>
);
}
const meta: Meta<typeof TimeoutDialogPreview> = {
title: "Features/Auth/SessionTimeoutWarning",
component: TimeoutDialogPreview,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof TimeoutDialogPreview>;
export const FiveMinutesLeft: Story = {
args: {
timeLeft: 5,
},
};
export const OneMinuteLeft: Story = {
args: {
timeLeft: 1,
},
};

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TermsCheckbox } from "./TermsCheckbox";
const meta: Meta<typeof TermsCheckbox> = {
title: "Features/Auth/TermsCheckbox",
component: TermsCheckbox,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof TermsCheckbox>;
export const Unchecked: Story = {
args: {
checked: false,
onChange: () => {},
},
};
export const Checked: Story = {
args: {
checked: true,
onChange: () => {},
},
};
export const WithError: Story = {
args: {
checked: false,
onChange: () => {},
error: "You must accept the terms to continue",
},
};
export const Disabled: Story = {
args: {
checked: false,
onChange: () => {},
disabled: true,
},
};

View File

@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BillingStatusBadge } from "./BillingStatusBadge";
const meta: Meta<typeof BillingStatusBadge> = {
title: "Features/Billing/BillingStatusBadge",
component: BillingStatusBadge,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof BillingStatusBadge>;
export const Paid: Story = {
args: {
status: "Paid",
},
};
export const Unpaid: Story = {
args: {
status: "Unpaid",
},
};
export const Overdue: Story = {
args: {
status: "Overdue",
},
};
export const Cancelled: Story = {
args: {
status: "Cancelled",
},
};
export const Draft: Story = {
args: {
status: "Draft",
},
};
export const Refunded: Story = {
args: {
status: "Refunded",
},
};
export const Collections: Story = {
args: {
status: "Collections",
},
};
export const PaymentPending: Story = {
args: {
status: "Payment Pending",
},
};
export const WithoutIcon: Story = {
args: {
status: "Paid",
showIcon: false,
},
};
export const CustomLabel: Story = {
args: {
status: "Paid",
children: "Payment Complete",
},
};

View File

@ -0,0 +1,85 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BillingSummary } from "./BillingSummary";
import type { BillingSummary as BillingSummaryType } from "@customer-portal/domain/billing";
const mockSummary: BillingSummaryType = {
totalOutstanding: 249.98,
totalOverdue: 79.99,
totalPaid: 1520.0,
currency: "EUR",
invoiceCount: {
total: 24,
unpaid: 3,
overdue: 1,
paid: 20,
},
};
const meta: Meta<typeof BillingSummary> = {
title: "Features/Billing/BillingSummary",
component: BillingSummary,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[450px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof BillingSummary>;
export const Default: Story = {
args: {
summary: mockSummary,
},
};
export const Compact: Story = {
args: {
summary: mockSummary,
compact: true,
},
};
export const Loading: Story = {
args: {
summary: mockSummary,
loading: true,
},
};
export const AllPaid: Story = {
args: {
summary: {
totalOutstanding: 0,
totalOverdue: 0,
totalPaid: 3200.0,
currency: "EUR",
invoiceCount: {
total: 15,
unpaid: 0,
overdue: 0,
paid: 15,
},
},
},
};
export const HighOutstanding: Story = {
args: {
summary: {
totalOutstanding: 1500.0,
totalOverdue: 800.0,
totalPaid: 200.0,
currency: "USD",
invoiceCount: {
total: 10,
unpaid: 5,
overdue: 3,
paid: 2,
},
},
},
};

View File

@ -0,0 +1,79 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceItems } from "./InvoiceItems";
import type { InvoiceItem } from "@customer-portal/domain/billing";
const mockItems: InvoiceItem[] = [
{
id: 1,
description: "Premium Mobile Plan - Monthly",
amount: 49.99,
quantity: 1,
type: "subscription",
serviceId: 101,
},
{
id: 2,
description: "SIM Card Activation Fee",
amount: 9.99,
quantity: 1,
type: "one-time",
},
{
id: 3,
description: "Data Add-on 10GB",
amount: 14.99,
quantity: 2,
type: "subscription",
serviceId: 102,
},
];
const meta: Meta<typeof InvoiceItems> = {
title: "Features/Billing/InvoiceItems",
component: InvoiceItems,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[700px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceItems>;
export const Default: Story = {
args: {
items: mockItems,
currency: "EUR",
},
};
export const LinkedItemsOnly: Story = {
args: {
items: mockItems.filter(item => item.serviceId),
currency: "EUR",
},
};
export const OneTimeItemsOnly: Story = {
args: {
items: mockItems.filter(item => !item.serviceId),
currency: "EUR",
},
};
export const Empty: Story = {
args: {
items: [],
currency: "EUR",
},
};
export const SingleItem: Story = {
args: {
items: [mockItems[0]],
currency: "USD",
},
};

View File

@ -0,0 +1,98 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceSummaryBar } from "./InvoiceSummaryBar";
import type { Invoice } from "@customer-portal/domain/billing";
const baseInvoice: Invoice = {
id: 1001,
number: "INV-2026-001",
status: "Paid",
currency: "EUR",
total: 129.99,
subtotal: 109.99,
tax: 20.0,
issuedAt: "2026-02-15T00:00:00Z",
dueDate: "2026-03-15T00:00:00Z",
paidDate: "2026-02-28T00:00:00Z",
description: "Monthly subscription",
};
const meta: Meta<typeof InvoiceSummaryBar> = {
title: "Features/Billing/InvoiceSummaryBar",
component: InvoiceSummaryBar,
parameters: { layout: "padded" },
decorators: [
Story => (
<div className="bg-card rounded-2xl border border-border overflow-hidden">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceSummaryBar>;
export const Paid: Story = {
args: {
invoice: baseInvoice,
onDownload: () => {},
},
};
export const Unpaid: Story = {
args: {
invoice: {
...baseInvoice,
status: "Unpaid",
paidDate: undefined,
dueDate: "2026-03-20T00:00:00Z",
},
onDownload: () => {},
onPay: () => {},
},
};
export const Overdue: Story = {
args: {
invoice: {
...baseInvoice,
status: "Overdue",
paidDate: undefined,
daysOverdue: 12,
dueDate: "2026-02-23T00:00:00Z",
},
onDownload: () => {},
onPay: () => {},
},
};
export const Cancelled: Story = {
args: {
invoice: {
...baseInvoice,
status: "Cancelled",
paidDate: undefined,
},
onDownload: () => {},
},
};
export const DownloadLoading: Story = {
args: {
invoice: baseInvoice,
onDownload: () => {},
loadingDownload: true,
},
};
export const PaymentLoading: Story = {
args: {
invoice: {
...baseInvoice,
status: "Unpaid",
paidDate: undefined,
},
onDownload: () => {},
onPay: () => {},
loadingPayment: true,
},
};

View File

@ -0,0 +1,41 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceTotals } from "./InvoiceTotals";
const meta: Meta<typeof InvoiceTotals> = {
title: "Features/Billing/InvoiceTotals",
component: InvoiceTotals,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[400px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceTotals>;
export const Default: Story = {
args: {
subtotal: 99.99,
tax: 10.0,
total: 109.99,
},
};
export const NoTax: Story = {
args: {
subtotal: 49.99,
tax: 0,
total: 49.99,
},
};
export const LargeAmounts: Story = {
args: {
subtotal: 12500.0,
tax: 2500.0,
total: 15000.0,
},
};

View File

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceItemRow } from "./InvoiceItemRow";
const meta: Meta<typeof InvoiceItemRow> = {
title: "Features/Billing/InvoiceItemRow",
component: InvoiceItemRow,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[600px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceItemRow>;
export const OneTimeItem: Story = {
args: {
id: 1,
description: "SIM Card Activation Fee",
amount: 9.99,
},
};
export const LinkedToService: Story = {
args: {
id: 2,
description: "Premium Mobile Plan - Monthly",
amount: 49.99,
serviceId: 101,
},
};
export const WithQuantity: Story = {
args: {
id: 3,
description: "Data Add-on 5GB",
amount: 29.98,
quantity: 2,
serviceId: 55,
},
};
export const LargeAmount: Story = {
args: {
id: 4,
description: "Enterprise Plan - Annual License",
amount: 2499.99,
},
};

View File

@ -0,0 +1,114 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceTable } from "./InvoiceTable";
import type { Invoice } from "@customer-portal/domain/billing";
const mockInvoices: Invoice[] = [
{
id: 1,
number: "INV-2026-001",
status: "Paid",
currency: "EUR",
total: 129.99,
subtotal: 109.99,
tax: 20.0,
issuedAt: "2026-01-15T00:00:00Z",
dueDate: "2026-02-15T00:00:00Z",
paidDate: "2026-01-30T00:00:00Z",
description: "Premium Mobile Plan - January",
},
{
id: 2,
number: "INV-2026-002",
status: "Unpaid",
currency: "EUR",
total: 49.99,
subtotal: 41.99,
tax: 8.0,
issuedAt: "2026-02-15T00:00:00Z",
dueDate: "2026-03-15T00:00:00Z",
description: "Standard Plan - February",
},
{
id: 3,
number: "INV-2026-003",
status: "Overdue",
currency: "EUR",
total: 79.99,
subtotal: 67.22,
tax: 12.77,
issuedAt: "2026-01-01T00:00:00Z",
dueDate: "2026-02-01T00:00:00Z",
daysOverdue: 34,
description: "Data Add-on Package",
},
{
id: 4,
number: "INV-2026-004",
status: "Cancelled",
currency: "EUR",
total: 19.99,
subtotal: 16.8,
tax: 3.19,
issuedAt: "2026-01-10T00:00:00Z",
description: "SIM Activation - Cancelled",
},
];
const meta: Meta<typeof InvoiceTable> = {
title: "Features/Billing/InvoiceTable",
component: InvoiceTable,
parameters: { layout: "padded" },
decorators: [
Story => (
<div className="w-full max-w-[900px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceTable>;
export const Default: Story = {
args: {
invoices: mockInvoices,
onInvoiceClick: () => {},
},
};
export const WithoutActions: Story = {
args: {
invoices: mockInvoices,
showActions: false,
onInvoiceClick: () => {},
},
};
export const Compact: Story = {
args: {
invoices: mockInvoices,
compact: true,
onInvoiceClick: () => {},
},
};
export const Loading: Story = {
args: {
invoices: [],
loading: true,
},
};
export const Empty: Story = {
args: {
invoices: [],
onInvoiceClick: () => {},
},
};
export const SingleInvoice: Story = {
args: {
invoices: [mockInvoices[0]],
onInvoiceClick: () => {},
},
};

View File

@ -0,0 +1,33 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InvoiceListSkeleton } from "./invoice-list-skeleton";
const meta: Meta<typeof InvoiceListSkeleton> = {
title: "Features/Billing/InvoiceListSkeleton",
component: InvoiceListSkeleton,
parameters: { layout: "centered" },
decorators: [
Story => (
<div className="w-[600px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof InvoiceListSkeleton>;
export const Default: Story = {
args: {},
};
export const ThreeRows: Story = {
args: {
rows: 3,
},
};
export const TenRows: Story = {
args: {
rows: 10,
},
};

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CheckoutErrorFallback } from "./CheckoutErrorFallback";
const meta: Meta<typeof CheckoutErrorFallback> = {
title: "Features/Checkout/CheckoutErrorFallback",
component: CheckoutErrorFallback,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 640 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof CheckoutErrorFallback>;
export const Default: Story = {
args: {
shopHref: "/account/services",
},
};

View File

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CheckoutShell } from "./CheckoutShell";
const meta: Meta<typeof CheckoutShell> = {
title: "Features/Checkout/CheckoutShell",
component: CheckoutShell,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof CheckoutShell>;
export const Default: Story = {
args: {
children: (
<div className="text-center py-20">
<h1 className="text-2xl font-bold">Checkout Content Area</h1>
<p className="text-muted-foreground mt-2">This area would contain the checkout form.</p>
</div>
),
},
};

View File

@ -0,0 +1,135 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CheckoutStatusBanners } from "./CheckoutStatusBanners";
import { fn } from "@storybook/test";
const meta: Meta<typeof CheckoutStatusBanners> = {
title: "Features/Checkout/CheckoutStatusBanners",
component: CheckoutStatusBanners,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 640 }}>
<Story />
</div>
),
],
args: {
activeInternetWarning: null,
eligibility: {
isLoading: false,
isError: false,
isPending: false,
isNotRequested: false,
isIneligible: false,
notes: null,
requestedAt: null,
refetch: fn(),
},
eligibilityRequest: {
isPending: false,
mutate: fn(),
},
hasServiceAddress: true,
addressLabel: "123 Tokyo Street, Shibuya-ku, Tokyo",
userAddress: {
address1: "123 Tokyo Street",
city: "Tokyo",
state: "Shibuya-ku",
postcode: "150-0001",
country: "JP",
},
planSku: "FIBER-100",
},
};
export default meta;
type Story = StoryObj<typeof CheckoutStatusBanners>;
export const NoWarnings: Story = {};
export const ActiveInternetWarning: Story = {
args: {
activeInternetWarning:
"You already have an active internet subscription. Adding a new one may result in duplicate billing.",
},
};
export const EligibilityLoading: Story = {
args: {
eligibility: {
isLoading: true,
isError: false,
isPending: false,
isNotRequested: false,
isIneligible: false,
refetch: fn(),
},
},
};
export const EligibilityError: Story = {
args: {
eligibility: {
isLoading: false,
isError: true,
isPending: false,
isNotRequested: false,
isIneligible: false,
refetch: fn(),
},
},
};
export const EligibilityPending: Story = {
args: {
eligibility: {
isLoading: false,
isError: false,
isPending: true,
isNotRequested: false,
isIneligible: false,
refetch: fn(),
},
},
};
export const EligibilityNotRequested: Story = {
args: {
eligibility: {
isLoading: false,
isError: false,
isPending: false,
isNotRequested: true,
isIneligible: false,
refetch: fn(),
},
},
};
export const EligibilityNotRequestedNoAddress: Story = {
args: {
hasServiceAddress: false,
eligibility: {
isLoading: false,
isError: false,
isPending: false,
isNotRequested: true,
isIneligible: false,
refetch: fn(),
},
},
};
export const Ineligible: Story = {
args: {
eligibility: {
isLoading: false,
isError: false,
isPending: false,
isNotRequested: false,
isIneligible: true,
notes: "Service is not available in your area due to infrastructure limitations.",
requestedAt: "2026-03-01T10:00:00Z",
refetch: fn(),
},
},
};

View File

@ -0,0 +1,28 @@
import type { Meta, StoryObj } from "@storybook/react";
import { OrderConfirmation } from "./OrderConfirmation";
const meta: Meta<typeof OrderConfirmation> = {
title: "Features/Checkout/OrderConfirmation",
component: OrderConfirmation,
parameters: {
layout: "centered",
nextjs: { navigation: { searchParams: { orderId: "ORD-20260307-001" } } },
},
decorators: [
Story => (
<div style={{ width: 640 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof OrderConfirmation>;
export const Default: Story = {};
export const WithoutOrderId: Story = {
parameters: {
nextjs: { navigation: { searchParams: {} } },
},
};

View File

@ -0,0 +1,88 @@
import type { Meta, StoryObj } from "@storybook/react";
import { IdentityVerificationSection } from "./IdentityVerificationSection";
import { fn } from "@storybook/test";
const formatDateTime = (iso?: string | null) => (iso ? new Date(iso).toLocaleString() : null);
const meta: Meta<typeof IdentityVerificationSection> = {
title: "Features/Checkout/IdentityVerificationSection",
component: IdentityVerificationSection,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 560 }}>
<Story />
</div>
),
],
args: {
isLoading: false,
isError: false,
onRefetch: fn(),
onSubmitFile: fn(),
isSubmitting: false,
submitError: null,
formatDateTime,
},
};
export default meta;
type Story = StoryObj<typeof IdentityVerificationSection>;
export const Verified: Story = {
args: {
status: "verified",
data: {
submittedAt: "2026-02-20T14:30:00Z",
reviewedAt: "2026-02-21T09:15:00Z",
reviewerNotes: null,
},
},
};
export const Pending: Story = {
args: {
status: "pending",
data: {
submittedAt: "2026-03-06T10:00:00Z",
reviewedAt: null,
reviewerNotes: null,
},
},
};
export const NotSubmitted: Story = {
args: {
status: "not_submitted",
data: undefined,
},
};
export const Rejected: Story = {
args: {
status: "rejected",
data: {
reviewerNotes: "The document is expired. Please upload a valid residence card.",
},
},
};
export const Loading: Story = {
args: {
isLoading: true,
status: undefined,
},
};
export const ErrorState: Story = {
args: {
isError: true,
status: undefined,
},
};
export const Submitting: Story = {
args: {
status: "not_submitted",
isSubmitting: true,
},
};

View File

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from "@storybook/react";
import { OrderSubmitSection } from "./OrderSubmitSection";
import { fn } from "@storybook/test";
const meta: Meta<typeof OrderSubmitSection> = {
title: "Features/Checkout/OrderSubmitSection",
component: OrderSubmitSection,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 640 }}>
<Story />
</div>
),
],
args: {
onSubmit: fn(),
onBack: fn(),
},
};
export default meta;
type Story = StoryObj<typeof OrderSubmitSection>;
export const Default: Story = {
args: {
pricing: { monthlyTotal: 5980, oneTimeTotal: 0 },
submitError: null,
isSubmitting: false,
canSubmit: true,
},
};
export const WithOneTimeFee: Story = {
args: {
pricing: { monthlyTotal: 5980, oneTimeTotal: 3300 },
submitError: null,
isSubmitting: false,
canSubmit: true,
},
};
export const Submitting: Story = {
args: {
pricing: { monthlyTotal: 5980, oneTimeTotal: 0 },
submitError: null,
isSubmitting: true,
canSubmit: true,
},
};
export const CannotSubmit: Story = {
args: {
pricing: { monthlyTotal: 5980, oneTimeTotal: 0 },
submitError: null,
isSubmitting: false,
canSubmit: false,
},
};
export const WithError: Story = {
args: {
pricing: { monthlyTotal: 5980, oneTimeTotal: 0 },
submitError: "An unexpected error occurred. Please try again or contact support.",
isSubmitting: false,
canSubmit: true,
},
};

View File

@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PaymentMethodSection } from "./PaymentMethodSection";
import { fn } from "@storybook/test";
const meta: Meta<typeof PaymentMethodSection> = {
title: "Features/Checkout/PaymentMethodSection",
component: PaymentMethodSection,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 560 }}>
<Story />
</div>
),
],
args: {
onManagePayment: fn(),
onRefresh: fn(),
isOpeningPortal: false,
},
};
export default meta;
type Story = StoryObj<typeof PaymentMethodSection>;
export const WithPaymentMethod: Story = {
args: {
isLoading: false,
isError: false,
hasPaymentMethod: true,
paymentMethodDisplay: {
title: "Visa ending in 4242",
subtitle: "Expires 12/2028",
},
},
};
export const NoPaymentMethod: Story = {
args: {
isLoading: false,
isError: false,
hasPaymentMethod: false,
paymentMethodDisplay: null,
},
};
export const Loading: Story = {
args: {
isLoading: true,
isError: false,
hasPaymentMethod: false,
paymentMethodDisplay: null,
},
};
export const Error: Story = {
args: {
isLoading: false,
isError: true,
hasPaymentMethod: false,
paymentMethodDisplay: null,
},
};
export const OpeningPortal: Story = {
args: {
isLoading: false,
isError: false,
hasPaymentMethod: true,
paymentMethodDisplay: {
title: "Visa ending in 4242",
subtitle: "Expires 12/2028",
},
isOpeningPortal: true,
},
};

View File

@ -0,0 +1,54 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ResidenceCardUploadInput } from "./ResidenceCardUploadInput";
import { fn } from "@storybook/test";
const meta: Meta<typeof ResidenceCardUploadInput> = {
title: "Features/Checkout/ResidenceCardUploadInput",
component: ResidenceCardUploadInput,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 480 }}>
<Story />
</div>
),
],
args: {
onSubmit: fn(),
isPending: false,
isError: false,
error: null,
},
};
export default meta;
type Story = StoryObj<typeof ResidenceCardUploadInput>;
export const Default: Story = {};
export const WithDescription: Story = {
args: {
description: "Upload a JPG, PNG, or PDF (max 5MB).",
submitLabel: "Submit for review",
},
};
export const Uploading: Story = {
args: {
isPending: true,
submitLabel: "Submit for review",
},
};
export const WithError: Story = {
args: {
isError: true,
error: new Error("File size exceeds the 5MB limit. Please choose a smaller file."),
},
};
export const CustomSubmitLabel: Story = {
args: {
submitLabel: "Submit replacement",
description: "Replacing the file restarts the verification process.",
},
};

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AccountStatusCard } from "./AccountStatusCard";
const meta: Meta<typeof AccountStatusCard> = {
title: "Features/Dashboard/AccountStatusCard",
component: AccountStatusCard,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 400 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof AccountStatusCard>;
export const Default: Story = {};

View File

@ -0,0 +1,98 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ActivityTimeline } from "./ActivityTimeline";
import { fn } from "@storybook/test";
const today = new Date().toISOString();
const yesterday = new Date(Date.now() - 86400000).toISOString();
const twoDaysAgo = new Date(Date.now() - 2 * 86400000).toISOString();
const meta: Meta<typeof ActivityTimeline> = {
title: "Features/Dashboard/ActivityTimeline",
component: ActivityTimeline,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 560 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof ActivityTimeline>;
export const Default: Story = {
args: {
activities: [
{
id: "act-1",
type: "invoice_created",
title: "Invoice #1042 created",
description: "Monthly internet service",
date: today,
},
{
id: "act-2",
type: "invoice_paid",
title: "Invoice #1041 paid",
description: "Payment of 5,980 JPY received",
date: today,
},
{
id: "act-3",
type: "service_activated",
title: "Fiber 100Mbps activated",
description: "Internet service is now active",
date: yesterday,
},
{
id: "act-4",
type: "case_created",
title: "Support case opened",
description: "Connection issues reported",
date: twoDaysAgo,
},
{
id: "act-5",
type: "case_closed",
title: "Support case resolved",
description: "Connection issue has been fixed",
date: twoDaysAgo,
},
],
onItemClick: fn(),
},
};
export const Empty: Story = {
args: {
activities: [],
},
};
export const SingleActivity: Story = {
args: {
activities: [
{
id: "act-1",
type: "invoice_paid",
title: "Invoice #1041 paid",
description: "Payment of 5,980 JPY received",
date: today,
},
],
},
};
export const LimitedItems: Story = {
args: {
activities: Array.from({ length: 20 }, (_, i) => ({
id: `act-${i}`,
type: "invoice_created" as const,
title: `Invoice #${1000 + i} created`,
description: "Monthly service charge",
date: new Date(Date.now() - i * 86400000).toISOString(),
})),
maxItems: 5,
},
};

View File

@ -0,0 +1,110 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DashboardActivityItem } from "./DashboardActivityItem";
import { fn } from "@storybook/test";
const meta: Meta<typeof DashboardActivityItem> = {
title: "Features/Dashboard/DashboardActivityItem",
component: DashboardActivityItem,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 480 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof DashboardActivityItem>;
export const InvoiceCreated: Story = {
args: {
activity: {
id: "act-1",
type: "invoice_created",
title: "Invoice #1042 created",
description: "Monthly internet service - March 2026",
date: new Date().toISOString(),
},
showConnector: true,
},
};
export const InvoicePaid: Story = {
args: {
activity: {
id: "act-2",
type: "invoice_paid",
title: "Invoice #1041 paid",
description: "Payment of 5,980 JPY received",
date: new Date().toISOString(),
},
showConnector: true,
},
};
export const ServiceActivated: Story = {
args: {
activity: {
id: "act-3",
type: "service_activated",
title: "Fiber 100Mbps activated",
description: "Internet service is now active",
date: new Date().toISOString(),
},
showConnector: false,
},
};
export const CaseCreated: Story = {
args: {
activity: {
id: "act-4",
type: "case_created",
title: "Support case opened",
description: "Connection issues reported",
date: new Date().toISOString(),
},
showConnector: true,
},
};
export const CaseClosed: Story = {
args: {
activity: {
id: "act-5",
type: "case_closed",
title: "Support case resolved",
description: "Connection issue has been fixed",
date: new Date().toISOString(),
},
showConnector: false,
},
};
export const Clickable: Story = {
args: {
activity: {
id: "act-6",
type: "invoice_created",
title: "Invoice #1042 created",
description: "Click to view invoice details",
date: new Date().toISOString(),
},
onClick: fn(),
showConnector: false,
},
};
export const WithoutConnector: Story = {
args: {
activity: {
id: "act-7",
type: "invoice_paid",
title: "Invoice #1040 paid",
description: "Payment received",
date: new Date().toISOString(),
},
showConnector: false,
},
};

View File

@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { QuickAction } from "./QuickAction";
import {
CreditCardIcon,
Squares2X2Icon,
ChatBubbleLeftRightIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof QuickAction> = {
title: "Features/Dashboard/QuickAction",
component: QuickAction,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 400 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof QuickAction>;
export const Default: Story = {
args: {
href: "/account/billing",
title: "Pay Invoice",
description: "View and pay your outstanding invoices",
icon: CreditCardIcon,
iconColor: "text-primary",
bgColor: "bg-primary/10",
},
};
export const Services: Story = {
args: {
href: "/account/services",
title: "Browse Services",
description: "Explore available internet and SIM plans",
icon: Squares2X2Icon,
iconColor: "text-info",
bgColor: "bg-info/10",
},
};
export const Support: Story = {
args: {
href: "/account/support",
title: "Get Support",
description: "Open a support case or check existing tickets",
icon: ChatBubbleLeftRightIcon,
iconColor: "text-warning",
bgColor: "bg-warning/10",
},
};

View File

@ -0,0 +1,73 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StatCard } from "./StatCard";
import {
Squares2X2Icon,
DocumentTextIcon,
ChatBubbleLeftRightIcon,
CurrencyYenIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof StatCard> = {
title: "Features/Dashboard/StatCard",
component: StatCard,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 320 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof StatCard>;
export const Default: Story = {
args: {
title: "Active Services",
value: 3,
icon: Squares2X2Icon,
href: "/account/services",
tone: "primary",
},
};
export const UnpaidInvoices: Story = {
args: {
title: "Unpaid Invoices",
value: 2,
icon: DocumentTextIcon,
href: "/account/billing",
tone: "warning",
},
};
export const OpenCases: Story = {
args: {
title: "Open Cases",
value: 0,
icon: ChatBubbleLeftRightIcon,
href: "/account/support",
tone: "info",
},
};
export const TotalSpent: Story = {
args: {
title: "Total Spent",
value: "17,940 JPY",
icon: CurrencyYenIcon,
href: "/account/billing",
tone: "success",
},
};
export const Neutral: Story = {
args: {
title: "Recent Orders",
value: 1,
icon: DocumentTextIcon,
href: "/account/orders",
tone: "neutral",
},
};

View File

@ -0,0 +1,100 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TaskCard } from "./TaskCard";
import { fn } from "@storybook/test";
import {
ExclamationCircleIcon,
CreditCardIcon,
ClockIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
const meta: Meta<typeof TaskCard> = {
title: "Features/Dashboard/TaskCard",
component: TaskCard,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 560 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof TaskCard>;
export const Critical: Story = {
args: {
id: "task-1",
icon: ExclamationCircleIcon,
title: "Invoice #1042 overdue",
description:
"Payment of 5,980 JPY was due on March 1, 2026. Pay now to avoid service interruption.",
actionLabel: "Pay now",
detailHref: "/account/billing",
onAction: fn(),
tone: "critical",
},
};
export const Warning: Story = {
args: {
id: "task-2",
icon: CreditCardIcon,
title: "Add a payment method",
description: "A payment method is required before you can place orders.",
actionLabel: "Add payment",
onAction: fn(),
tone: "warning",
},
};
export const Info: Story = {
args: {
id: "task-3",
icon: ClockIcon,
title: "Order being processed",
description: "Your internet service order is being reviewed by our team.",
actionLabel: "View order",
detailHref: "/account/orders/ORD-001",
tone: "info",
},
};
export const Neutral: Story = {
args: {
id: "task-4",
icon: SparklesIcon,
title: "Complete your profile",
description: "Add your address and verify your identity to unlock all features.",
actionLabel: "Get started",
detailHref: "/account/settings",
tone: "neutral",
},
};
export const Loading: Story = {
args: {
id: "task-5",
icon: CreditCardIcon,
title: "Add a payment method",
description: "A payment method is required before you can place orders.",
actionLabel: "Add payment",
onAction: fn(),
tone: "warning",
isLoading: true,
loadingText: "Opening...",
},
};
export const WithoutDetailLink: Story = {
args: {
id: "task-6",
icon: CreditCardIcon,
title: "Update payment method",
description: "Your card on file has expired. Please update it.",
actionLabel: "Update",
onAction: fn(),
tone: "warning",
},
};

View File

@ -0,0 +1,142 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
ExclamationCircleIcon,
CreditCardIcon,
ClockIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
import { TaskCard, TaskCardSkeleton } from "./TaskCard";
import type { TaskTone } from "./TaskCard";
/**
* TaskList uses internal hooks (useCreateInvoiceSsoLink, useCreatePaymentMethodsSsoLink)
* that require provider context. Instead of mocking those hooks, we compose stories
* from the underlying TaskCard and TaskCardSkeleton components directly, which
* faithfully represents what TaskList renders.
*/
interface MockTask {
id: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
title: string;
description: string;
actionLabel: string;
detailHref?: string;
tone: TaskTone;
}
function TaskListPreview({ tasks, isLoading }: { tasks: MockTask[]; isLoading?: boolean }) {
if (isLoading) {
return (
<div className="space-y-4">
<TaskCardSkeleton />
<TaskCardSkeleton />
</div>
);
}
if (tasks.length === 0) {
return (
<div className="text-center py-12 text-muted-foreground">
All caught up! No tasks to show.
</div>
);
}
return (
<div className="space-y-4">
{tasks.map(task => (
<TaskCard
key={task.id}
id={task.id}
icon={task.icon}
title={task.title}
description={task.description}
actionLabel={task.actionLabel}
detailHref={task.detailHref}
tone={task.tone}
/>
))}
</div>
);
}
const meta: Meta<typeof TaskListPreview> = {
title: "Features/Dashboard/TaskList",
component: TaskListPreview,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ width: 600 }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof TaskListPreview>;
export const WithTasks: Story = {
args: {
isLoading: false,
tasks: [
{
id: "task-1",
title: "Invoice #1042 overdue",
description: "Payment of 5,980 JPY was due on March 1, 2026.",
actionLabel: "Pay now",
detailHref: "/account/billing",
tone: "critical",
icon: ExclamationCircleIcon,
},
{
id: "task-2",
title: "Add a payment method",
description: "A payment method is required before you can place orders.",
actionLabel: "Add payment",
tone: "warning",
icon: CreditCardIcon,
},
{
id: "task-3",
title: "Order being processed",
description: "Your internet service order is being reviewed.",
actionLabel: "View order",
detailHref: "/account/orders/ORD-001",
tone: "info",
icon: ClockIcon,
},
],
},
};
export const Empty: Story = {
args: {
isLoading: false,
tasks: [],
},
};
export const Loading: Story = {
args: {
isLoading: true,
tasks: [],
},
};
export const SingleTask: Story = {
args: {
isLoading: false,
tasks: [
{
id: "task-1",
title: "Complete your profile",
description: "Add your address and verify your identity to unlock all features.",
actionLabel: "Get started",
detailHref: "/account/settings",
tone: "neutral",
icon: SparklesIcon,
},
],
},
};

View File

@ -0,0 +1,167 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Input, Label } from "@/components/atoms";
/**
* Preview wrapper that recreates the NewCustomerFields visual output
* without importing the real component (which depends on JapanAddressForm).
*/
interface AccountFormErrors {
firstName?: string;
lastName?: string;
address?: string;
[key: string]: string | undefined;
}
interface NewCustomerFieldsPreviewProps {
firstName: string;
lastName: string;
onFirstNameChange: (value: string) => void;
onLastNameChange: (value: string) => void;
onAddressChange: (data: unknown, isComplete: boolean) => void;
errors: AccountFormErrors;
clearError: (field: string) => void;
loading: boolean;
}
function NewCustomerFieldsPreview({
firstName,
lastName,
onFirstNameChange,
onLastNameChange,
errors,
clearError,
loading,
}: NewCustomerFieldsPreviewProps) {
return (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">
First Name <span className="text-danger">*</span>
</Label>
<Input
id="firstName"
value={firstName}
onChange={e => {
onFirstNameChange(e.target.value);
clearError("firstName");
}}
placeholder="Taro"
disabled={loading}
error={errors.firstName}
/>
{errors.firstName && <p className="text-sm text-danger">{errors.firstName}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="lastName">
Last Name <span className="text-danger">*</span>
</Label>
<Input
id="lastName"
value={lastName}
onChange={e => {
onLastNameChange(e.target.value);
clearError("lastName");
}}
placeholder="Yamada"
disabled={loading}
error={errors.lastName}
/>
{errors.lastName && <p className="text-sm text-danger">{errors.lastName}</p>}
</div>
</div>
<div className="space-y-2">
<Label>
Address <span className="text-danger">*</span>
</Label>
<div
style={{
padding: "12px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
color: loading ? "#9ca3af" : "#374151",
fontSize: "14px",
}}
>
[Japan Address Form Placeholder]
</div>
{errors.address && <p className="text-sm text-danger">{errors.address}</p>}
</div>
</>
);
}
const meta: Meta<typeof NewCustomerFieldsPreview> = {
title: "Features/GetStarted/CompleteAccount/NewCustomerFields",
component: NewCustomerFieldsPreview,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ maxWidth: 480, width: "100%" }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof NewCustomerFieldsPreview>;
export const Default: Story = {
args: {
firstName: "",
lastName: "",
onFirstNameChange: fn(),
onLastNameChange: fn(),
onAddressChange: fn(),
errors: {},
clearError: fn(),
loading: false,
},
};
export const Filled: Story = {
args: {
firstName: "Taro",
lastName: "Yamada",
onFirstNameChange: fn(),
onLastNameChange: fn(),
onAddressChange: fn(),
errors: {},
clearError: fn(),
loading: false,
},
};
export const WithErrors: Story = {
args: {
firstName: "",
lastName: "",
onFirstNameChange: fn(),
onLastNameChange: fn(),
onAddressChange: fn(),
errors: {
firstName: "First name is required",
lastName: "Last name is required",
address: "Please enter a valid address",
},
clearError: fn(),
loading: false,
},
};
export const Loading: Story = {
args: {
firstName: "Taro",
lastName: "Yamada",
onFirstNameChange: fn(),
onLastNameChange: fn(),
onAddressChange: fn(),
errors: {},
clearError: fn(),
loading: true,
},
};

View File

@ -0,0 +1,234 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Check, X } from "lucide-react";
import { PasswordInput, Label, ErrorMessage } from "@/components/atoms";
/**
* Preview wrapper that recreates the PasswordSection visual output
* without importing the real component (which depends on usePasswordValidation).
*/
interface PasswordChecks {
minLength: boolean;
hasUppercase: boolean;
hasLowercase: boolean;
hasNumber: boolean;
}
function RequirementCheck({ met, label }: { met: boolean; label: string }) {
return (
<div className="flex items-center gap-1">
{met ? (
<Check className="h-3 w-3 text-success" />
) : (
<X className="h-3 w-3 text-muted-foreground" />
)}
<span className={met ? "text-success" : "text-muted-foreground"}>{label}</span>
</div>
);
}
function PasswordRequirementsPreview({
checks,
showHint = false,
}: {
checks: PasswordChecks;
showHint?: boolean;
}) {
if (showHint) {
return (
<p className="text-xs text-muted-foreground">
At least 8 characters with uppercase, lowercase, and numbers
</p>
);
}
return (
<div className="grid grid-cols-2 gap-1 text-xs">
<RequirementCheck met={checks.minLength} label="8+ characters" />
<RequirementCheck met={checks.hasUppercase} label="Uppercase letter" />
<RequirementCheck met={checks.hasLowercase} label="Lowercase letter" />
<RequirementCheck met={checks.hasNumber} label="Number" />
</div>
);
}
function PasswordMatchIndicatorPreview({ passwordsMatch }: { passwordsMatch: boolean }) {
if (passwordsMatch) {
return (
<div className="flex items-center gap-1 text-xs">
<Check className="h-3 w-3 text-success" />
<span className="text-success">Passwords match</span>
</div>
);
}
return (
<div className="flex items-center gap-1 text-xs">
<X className="h-3 w-3 text-danger" />
<span className="text-danger">Passwords do not match</span>
</div>
);
}
interface PasswordSectionPreviewProps {
password: string;
confirmPassword: string;
onPasswordChange: (value: string) => void;
onConfirmPasswordChange: (value: string) => void;
errors: {
password?: string | undefined;
confirmPassword?: string | undefined;
};
clearError: (field: "password" | "confirmPassword") => void;
loading: boolean;
/** Static checks to display (since we cannot call the real hook) */
checks: PasswordChecks;
}
function PasswordSectionPreview({
password,
confirmPassword,
onPasswordChange,
onConfirmPasswordChange,
errors,
clearError,
loading,
checks,
}: PasswordSectionPreviewProps) {
const showPasswordMatch = confirmPassword.length > 0 && !errors.confirmPassword;
const passwordsMatch = password === confirmPassword;
return (
<>
<div className="space-y-2">
<Label htmlFor="password">
Password <span className="text-danger">*</span>
</Label>
<PasswordInput
id="password"
value={password}
onChange={e => {
onPasswordChange(e.target.value);
clearError("password");
}}
placeholder="Create a strong password"
disabled={loading}
error={errors.password}
autoComplete="new-password"
/>
<ErrorMessage>{errors.password}</ErrorMessage>
<PasswordRequirementsPreview checks={checks} showHint={password.length === 0} />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">
Confirm Password <span className="text-danger">*</span>
</Label>
<PasswordInput
id="confirmPassword"
value={confirmPassword}
onChange={e => {
onConfirmPasswordChange(e.target.value);
clearError("confirmPassword");
}}
placeholder="Confirm your password"
disabled={loading}
error={errors.confirmPassword}
autoComplete="new-password"
/>
<ErrorMessage>{errors.confirmPassword}</ErrorMessage>
{showPasswordMatch && <PasswordMatchIndicatorPreview passwordsMatch={passwordsMatch} />}
</div>
</>
);
}
const allChecksPassing: PasswordChecks = {
minLength: true,
hasUppercase: true,
hasLowercase: true,
hasNumber: true,
};
const allChecksFailing: PasswordChecks = {
minLength: false,
hasUppercase: false,
hasLowercase: false,
hasNumber: false,
};
const meta: Meta<typeof PasswordSectionPreview> = {
title: "Features/GetStarted/CompleteAccount/PasswordSection",
component: PasswordSectionPreview,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ maxWidth: 480, width: "100%" }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof PasswordSectionPreview>;
export const Default: Story = {
args: {
password: "",
confirmPassword: "",
onPasswordChange: fn(),
onConfirmPasswordChange: fn(),
errors: {},
clearError: fn(),
loading: false,
checks: allChecksFailing,
},
};
export const FilledMatching: Story = {
args: {
password: "StrongP@ss1",
confirmPassword: "StrongP@ss1",
onPasswordChange: fn(),
onConfirmPasswordChange: fn(),
errors: {},
clearError: fn(),
loading: false,
checks: allChecksPassing,
},
};
export const WithErrors: Story = {
args: {
password: "weak",
confirmPassword: "different",
onPasswordChange: fn(),
onConfirmPasswordChange: fn(),
errors: {
password: "Password does not meet requirements",
confirmPassword: "Passwords do not match",
},
clearError: fn(),
loading: false,
checks: {
minLength: false,
hasUppercase: false,
hasLowercase: true,
hasNumber: false,
},
},
};
export const Loading: Story = {
args: {
password: "StrongP@ss1",
confirmPassword: "StrongP@ss1",
onPasswordChange: fn(),
onConfirmPasswordChange: fn(),
errors: {},
clearError: fn(),
loading: true,
checks: allChecksPassing,
},
};

View File

@ -0,0 +1,77 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PersonalInfoFields } from "./PersonalInfoFields";
const meta: Meta<typeof PersonalInfoFields> = {
title: "Features/GetStarted/CompleteAccount/PersonalInfoFields",
component: PersonalInfoFields,
parameters: { layout: "centered" },
decorators: [
Story => (
<div style={{ maxWidth: 480, width: "100%" }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof PersonalInfoFields>;
export const Default: Story = {
args: {
phone: "",
dateOfBirth: "",
gender: "",
onPhoneChange: () => {},
onDateOfBirthChange: () => {},
onGenderChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const Filled: Story = {
args: {
phone: "090-1234-5678",
dateOfBirth: "1990-05-15",
gender: "male",
onPhoneChange: () => {},
onDateOfBirthChange: () => {},
onGenderChange: () => {},
errors: {},
clearError: () => {},
loading: false,
},
};
export const WithErrors: Story = {
args: {
phone: "123",
dateOfBirth: "",
gender: "",
onPhoneChange: () => {},
onDateOfBirthChange: () => {},
onGenderChange: () => {},
errors: {
phone: "Please enter a valid phone number",
dateOfBirth: "Date of birth is required",
gender: "Please select a gender",
},
clearError: () => {},
loading: false,
},
};
export const Loading: Story = {
args: {
phone: "090-1234-5678",
dateOfBirth: "1990-05-15",
gender: "female",
onPhoneChange: () => {},
onDateOfBirthChange: () => {},
onGenderChange: () => {},
errors: {},
clearError: () => {},
loading: true,
},
};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CTABanner } from "./CTABanner";
const meta: Meta<typeof CTABanner> = {
title: "Features/LandingPage/CTABanner",
component: CTABanner,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof CTABanner>;
export const Default: Story = {};

View File

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Chapter } from "./Chapter";
const meta: Meta<typeof Chapter> = {
title: "Features/LandingPage/Chapter",
component: Chapter,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof Chapter>;
export const Default: Story = {
args: {
children: (
<div className="p-8">
<h2 className="text-2xl font-bold">Chapter Content</h2>
<p className="mt-2 text-muted-foreground">
This is an example section wrapped in a Chapter component.
</p>
</div>
),
},
};
export const WithCustomClass: Story = {
args: {
children: (
<div className="p-8">
<h2 className="text-2xl font-bold">Styled Chapter</h2>
<p className="mt-2 text-muted-foreground">This chapter has a custom background class.</p>
</div>
),
className: "bg-muted rounded-xl",
},
};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ContactSection } from "./ContactSection";
const meta: Meta<typeof ContactSection> = {
title: "Features/LandingPage/ContactSection",
component: ContactSection,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof ContactSection>;
export const Default: Story = {};

View File

@ -0,0 +1,18 @@
import { useRef } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { HeroSection } from "./HeroSection";
function HeroSectionWrapper() {
const ctaRef = useRef<HTMLDivElement>(null);
return <HeroSection heroCTARef={ctaRef} />;
}
const meta: Meta<typeof HeroSection> = {
title: "Features/LandingPage/HeroSection",
component: HeroSectionWrapper,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof HeroSectionWrapper>;
export const Default: Story = {};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ServicesCarousel } from "./ServicesCarousel";
const meta: Meta<typeof ServicesCarousel> = {
title: "Features/LandingPage/ServicesCarousel",
component: ServicesCarousel,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof ServicesCarousel>;
export const Default: Story = {};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SupportDownloadsSection } from "./SupportDownloadsSection";
const meta: Meta<typeof SupportDownloadsSection> = {
title: "Features/LandingPage/SupportDownloadsSection",
component: SupportDownloadsSection,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof SupportDownloadsSection>;
export const Default: Story = {};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TrustStrip } from "./TrustStrip";
const meta: Meta<typeof TrustStrip> = {
title: "Features/LandingPage/TrustStrip",
component: TrustStrip,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof TrustStrip>;
export const Default: Story = {};

View File

@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { WhyUsSection } from "./WhyUsSection";
const meta: Meta<typeof WhyUsSection> = {
title: "Features/LandingPage/WhyUsSection",
component: WhyUsSection,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof WhyUsSection>;
export const Default: Story = {};

View File

@ -0,0 +1,58 @@
import type { Meta, StoryObj } from "@storybook/react";
/**
* NotificationBell is tightly coupled to useUnreadNotificationCount hook.
* This story recreates the visual bell icon with badge using static data.
*/
function NotificationBellPreview({ unreadCount = 2 }: { unreadCount?: number }) {
return (
<div className="relative">
<button
type="button"
className="relative p-2.5 rounded-xl transition-all duration-200 text-muted-foreground hover:text-foreground hover:bg-muted/60"
aria-label={unreadCount > 0 ? `Notifications (${unreadCount} unread)` : "Notifications"}
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
{unreadCount > 0 && (
<span className="absolute top-1.5 right-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold text-primary-foreground">
{unreadCount > 9 ? "9+" : unreadCount}
</span>
)}
</button>
</div>
);
}
const meta: Meta<typeof NotificationBellPreview> = {
title: "Features/Notifications/NotificationBell",
component: NotificationBellPreview,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj<typeof NotificationBellPreview>;
export const WithUnread: Story = {
args: { unreadCount: 2 },
};
export const NoUnread: Story = {
args: { unreadCount: 0 },
};
export const ManyUnread: Story = {
args: { unreadCount: 15 },
};

View File

@ -0,0 +1,133 @@
import type { Meta, StoryObj } from "@storybook/react";
import { NotificationItem } from "./NotificationItem";
/**
* NotificationDropdown is tightly coupled to hooks (useNotifications, useMarkNotificationAsRead, etc.).
* This story recreates the visual layout with static data to avoid needing jest.mock in Vite/Storybook.
*/
const mockNotifications = [
{
id: "n-001",
userId: "u-001",
type: "ORDER_ACTIVATED" as const,
title: "Service activated",
message: "Your internet service is now active and ready to use.",
actionUrl: "/account/services",
actionLabel: "View Service",
source: "PORTAL" as const,
sourceId: null,
read: false,
readAt: null,
dismissed: false,
createdAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
expiresAt: "2026-12-31T23:59:59Z",
},
{
id: "n-002",
userId: "u-001",
type: "ORDER_APPROVED" as const,
title: "Order approved",
message: "Your order has been approved and is being processed.",
actionUrl: "/account/orders",
actionLabel: "View Order",
source: "SALESFORCE" as const,
sourceId: null,
read: true,
readAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
dismissed: false,
createdAt: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
expiresAt: "2026-12-31T23:59:59Z",
},
{
id: "n-003",
userId: "u-001",
type: "VERIFICATION_REJECTED" as const,
title: "ID verification requires attention",
message: "We couldn't verify your ID. Please review the feedback and resubmit.",
actionUrl: "/account/settings/verification",
actionLabel: "Resubmit",
source: "PORTAL" as const,
sourceId: null,
read: false,
readAt: null,
dismissed: false,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
expiresAt: "2026-12-31T23:59:59Z",
},
];
function NotificationDropdownPreview({
position: _position = "below",
}: {
position?: "below" | "right";
}) {
return (
<div className="w-80 sm:w-96 bg-popover border border-border rounded-xl shadow-lg overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold text-foreground">Notifications</h3>
<button
type="button"
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
Mark all read
</button>
</div>
{/* Notification list */}
<div className="max-h-96 overflow-y-auto divide-y divide-border/50">
{mockNotifications.map(notification => (
<NotificationItem
key={notification.id}
notification={notification}
onMarkAsRead={() => {}}
onDismiss={() => {}}
/>
))}
</div>
{/* Footer */}
<div className="px-4 py-3 border-t border-border">
<span className="block text-center text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
View all notifications
</span>
</div>
</div>
);
}
const meta: Meta = {
title: "Features/Notifications/NotificationDropdown",
component: NotificationDropdownPreview,
parameters: { layout: "centered" },
};
export default meta;
type Story = StoryObj;
export const Default: Story = {};
export const Empty: Story = {
render: () => (
<div className="w-80 sm:w-96 bg-popover border border-border rounded-xl shadow-lg overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold text-foreground">Notifications</h3>
</div>
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
<p className="text-sm text-muted-foreground">No notifications yet</p>
<p className="text-xs text-muted-foreground/70 mt-1">
We&apos;ll notify you when something important happens
</p>
</div>
</div>
),
};

Some files were not shown because too many files have changed in this diff Show More