diff --git a/apps/web/src/components/ui/AvatarUpload.stories.tsx b/apps/web/src/components/ui/AvatarUpload.stories.tsx new file mode 100644 index 000000000..2f403630d --- /dev/null +++ b/apps/web/src/components/ui/AvatarUpload.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { AvatarUpload } from './avatar-upload'; + +const meta = { + title: 'UI/AvatarUpload', + component: AvatarUpload, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg', 'xl'], + }, + disabled: { control: 'boolean' }, + }, + args: { + userId: '123', + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithExistingAvatar: Story = { + args: { + currentAvatarUrl: 'https://github.com/shadcn.png', + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +export const Large: Story = { + args: { + size: 'xl', + }, +}; diff --git a/apps/web/src/components/ui/ConfirmationDialog.stories.tsx b/apps/web/src/components/ui/ConfirmationDialog.stories.tsx new file mode 100644 index 000000000..36b32e6e3 --- /dev/null +++ b/apps/web/src/components/ui/ConfirmationDialog.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ConfirmationDialog } from './confirmation-dialog'; +import { Button } from './button'; +import { useState } from 'react'; + +const meta = { + title: 'UI/ConfirmationDialog', + component: ConfirmationDialog, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'radio', + options: ['default', 'destructive'], + }, + isLoading: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Destructive: Story = { + render: (args) => { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + onConfirm={() => { + alert('Confirmed!'); + setOpen(false); + }} + title="Delete Account" + description="Are you sure? This action cannot be undone." + confirmLabel="Delete" + variant="destructive" + /> + + ); + }, +}; + +export const Standard: Story = { + render: (args) => { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + onConfirm={() => setOpen(false)} + title="Publish Post" + description="Do you want to publish this post now?" + confirmLabel="Publish" + variant="default" + /> + + ); + }, +}; diff --git a/apps/web/src/components/ui/DataList.stories.tsx b/apps/web/src/components/ui/DataList.stories.tsx new file mode 100644 index 000000000..d88b73d58 --- /dev/null +++ b/apps/web/src/components/ui/DataList.stories.tsx @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { DataList } from './DataList'; +import { Card } from './card'; + +interface User { + id: string; + name: string; + email: string; + role: string; +} + +const mockUsers: User[] = [ + { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' }, + { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User' }, + { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'User' }, +]; + +const meta = { + title: 'UI/DataList', + component: DataList, + tags: ['autodocs'], + argTypes: { + loading: { control: 'boolean' }, + error: { control: 'text' }, + emptyMessage: { control: 'text' }, + }, +} satisfies Meta>; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + items: mockUsers, + keyExtractor: (user) => user.id, + renderItem: (user) => ( + +
+

{user.name}

+

{user.email}

+
+ {user.role} +
+ ), + }, +}; + +export const Loading: Story = { + args: { + items: [], + loading: true, + }, +}; + +export const Empty: Story = { + args: { + items: [], + emptyMessage: 'No users found in the system.', + }, +}; + +export const Error: Story = { + args: { + items: [], + error: 'Failed to load users. Please try again later.', + }, +}; diff --git a/apps/web/src/components/ui/ErrorDisplay.stories.tsx b/apps/web/src/components/ui/ErrorDisplay.stories.tsx new file mode 100644 index 000000000..7f4ff986e --- /dev/null +++ b/apps/web/src/components/ui/ErrorDisplay.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ErrorDisplay } from './ErrorDisplay'; + +const meta = { + title: 'UI/ErrorDisplay', + component: ErrorDisplay, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['inline', 'banner', 'modal', 'card'], + }, + severity: { + control: 'select', + options: ['error', 'warning', 'info'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + error: 'An unexpected error occurred.', + severity: 'error', + }, +}; + +export const WarningBanner: Story = { + args: { + error: 'Your subscription is expiring soon.', + severity: 'warning', + variant: 'banner', + title: 'Payment Alert', + }, +}; + +export const WithDetails: Story = { + args: { + error: { + message: 'Failed to fetch user data', + code: 'USER_NOT_FOUND', + status: 404, + details: { userId: '123' } + }, + showDetails: true, + }, +}; + +export const WithActions: Story = { + args: { + error: 'Network connection lost', + onRetry: () => console.log('Retrying...'), + actions: [ + { label: 'Check Status', onClick: () => console.log('Checking status') } + ], + }, +}; diff --git a/apps/web/src/components/ui/FAB.stories.tsx b/apps/web/src/components/ui/FAB.stories.tsx new file mode 100644 index 000000000..86048d6f1 --- /dev/null +++ b/apps/web/src/components/ui/FAB.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FAB } from './FAB'; +import { Plus, Upload, MessageCircle } from 'lucide-react'; + +const meta = { + title: 'UI/FAB', + component: FAB, + tags: ['autodocs'], + argTypes: { + position: { + control: 'select', + options: ['bottom-right', 'bottom-left', 'top-right', 'top-left'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + showLabel: { control: 'boolean' }, + label: { control: 'text' }, + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (Story) => ( +
+ +
+

The FAB will be positioned relative to this container.

+
+
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: , + containerClassName: 'absolute', + }, +}; + +export const WithLabel: Story = { + args: { + children: , + showLabel: true, + label: 'Upload File', + containerClassName: 'absolute', + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + children: , + containerClassName: 'absolute', + }, +}; + +export const TopLeft: Story = { + args: { + position: 'top-left', + children: , + containerClassName: 'absolute', + }, +}; diff --git a/apps/web/src/components/ui/FloatingInput.stories.tsx b/apps/web/src/components/ui/FloatingInput.stories.tsx new file mode 100644 index 000000000..ca5e60374 --- /dev/null +++ b/apps/web/src/components/ui/FloatingInput.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FloatingInput } from './floating-input'; +import { Mail, Lock } from 'lucide-react'; + +const meta = { + title: 'UI/FloatingInput', + component: FloatingInput, + tags: ['autodocs'], + argTypes: { + label: { control: 'text' }, + error: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Email Address', + type: 'email', + }, +}; + +export const WithIcon: Story = { + args: { + label: 'Password', + type: 'password', + icon: , + }, +}; + +export const WithError: Story = { + args: { + label: 'Email Address', + type: 'email', + icon: , + value: 'invalid-email', + error: 'Please enter a valid email address', + }, +}; diff --git a/apps/web/src/components/ui/FocusTrap.stories.tsx b/apps/web/src/components/ui/FocusTrap.stories.tsx new file mode 100644 index 000000000..24446691b --- /dev/null +++ b/apps/web/src/components/ui/FocusTrap.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FocusTrap } from './focus-trap'; +import { Button } from './button'; +import { Input } from './input'; +import { useState } from 'react'; + +const meta = { + title: 'UI/FocusTrap', + component: FocusTrap, + tags: ['autodocs'], + argTypes: { + active: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Interactive: Story = { + render: (args) => { + const [active, setActive] = useState(false); + + return ( +
+ + +
+

{active ? 'Focus Trapped Here' : 'Normal Navigation'}

+ + setActive(false)}> +
+ +
+ + +
+ +
+
+ + {active && ( +

+ Try tabbing through inputs. Focus should stay within this box. Press Escape to exit. +

+ )} +
+ +
+

Elements outside trap (cannot reach freely when active)

+ +
+
+ ); + }, +}; diff --git a/apps/web/src/components/ui/FormField.stories.tsx b/apps/web/src/components/ui/FormField.stories.tsx new file mode 100644 index 000000000..b63b0e307 --- /dev/null +++ b/apps/web/src/components/ui/FormField.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FormField, Input, Textarea, Select } from './FormField'; + +const meta = { + title: 'UI/FormField', + component: FormField, + tags: ['autodocs'], + argTypes: { + label: { control: 'text' }, + error: { control: 'text' }, + helpText: { control: 'text' }, + required: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Username', + children: , + }, +}; + +export const WithError: Story = { + args: { + label: 'Email', + required: true, + error: 'Invalid email address', + children: , + }, +}; + +export const WithHelpText: Story = { + args: { + label: 'Bio', + helpText: 'Tell us a little bit about yourself (max 140 chars)', + children: