feat: add portal UI components and stories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
74ee154669
commit
4c31c448f3
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
71
apps/portal/src/components/atoms/badge.stories.tsx
Normal file
71
apps/portal/src/components/atoms/badge.stories.tsx
Normal 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!") },
|
||||
};
|
||||
83
apps/portal/src/components/atoms/button.stories.tsx
Normal file
83
apps/portal/src/components/atoms/button.stories.tsx
Normal 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 },
|
||||
};
|
||||
39
apps/portal/src/components/atoms/checkbox.stories.tsx
Normal file
39
apps/portal/src/components/atoms/checkbox.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
43
apps/portal/src/components/atoms/empty-state.stories.tsx
Normal file
43
apps/portal/src/components/atoms/empty-state.stories.tsx
Normal 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={() => {}} />,
|
||||
};
|
||||
32
apps/portal/src/components/atoms/error-message.stories.tsx
Normal file
32
apps/portal/src/components/atoms/error-message.stories.tsx
Normal 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 },
|
||||
};
|
||||
48
apps/portal/src/components/atoms/error-state.stories.tsx
Normal file
48
apps/portal/src/components/atoms/error-state.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
36
apps/portal/src/components/atoms/inline-toast.stories.tsx
Normal file
36
apps/portal/src/components/atoms/inline-toast.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
37
apps/portal/src/components/atoms/input.stories.tsx
Normal file
37
apps/portal/src/components/atoms/input.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
22
apps/portal/src/components/atoms/label.stories.tsx
Normal file
22
apps/portal/src/components/atoms/label.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
26
apps/portal/src/components/atoms/loading-overlay.stories.tsx
Normal file
26
apps/portal/src/components/atoms/loading-overlay.stories.tsx
Normal 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...",
|
||||
},
|
||||
};
|
||||
26
apps/portal/src/components/atoms/logo.stories.tsx
Normal file
26
apps/portal/src/components/atoms/logo.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
22
apps/portal/src/components/atoms/password-input.stories.tsx
Normal file
22
apps/portal/src/components/atoms/password-input.stories.tsx
Normal 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 },
|
||||
};
|
||||
30
apps/portal/src/components/atoms/skeleton.stories.tsx
Normal file
30
apps/portal/src/components/atoms/skeleton.stories.tsx
Normal 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 },
|
||||
};
|
||||
40
apps/portal/src/components/atoms/spinner.stories.tsx
Normal file
40
apps/portal/src/components/atoms/spinner.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
59
apps/portal/src/components/atoms/status-pill.stories.tsx
Normal file
59
apps/portal/src/components/atoms/status-pill.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
40
apps/portal/src/components/atoms/step-header.stories.tsx
Normal file
40
apps/portal/src/components/atoms/step-header.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
25
apps/portal/src/components/atoms/view-toggle.stories.tsx
Normal file
25
apps/portal/src/components/atoms/view-toggle.stories.tsx
Normal 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} />;
|
||||
},
|
||||
};
|
||||
@ -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" },
|
||||
};
|
||||
@ -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>,
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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 },
|
||||
};
|
||||
@ -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.",
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -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"
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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} />,
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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 },
|
||||
};
|
||||
@ -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} />;
|
||||
},
|
||||
};
|
||||
@ -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: () => {} },
|
||||
};
|
||||
@ -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 })),
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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 },
|
||||
};
|
||||
@ -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>,
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -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>,
|
||||
},
|
||||
};
|
||||
62
apps/portal/src/components/ui/input-otp.stories.tsx
Normal file
62
apps/portal/src/components/ui/input-otp.stories.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -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.",
|
||||
},
|
||||
};
|
||||
@ -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!",
|
||||
},
|
||||
};
|
||||
@ -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: "",
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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">⚠️</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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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: () => {},
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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: {} } },
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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.",
|
||||
},
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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 = {};
|
||||
@ -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 = {};
|
||||
@ -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 = {};
|
||||
@ -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 = {};
|
||||
@ -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 = {};
|
||||
@ -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 },
|
||||
};
|
||||
@ -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'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
Loading…
x
Reference in New Issue
Block a user