diff --git a/src/components/OrderList/OrderList.stories.tsx b/src/components/OrderList/OrderList.stories.tsx index 0226665e..00a2a0c9 100644 --- a/src/components/OrderList/OrderList.stories.tsx +++ b/src/components/OrderList/OrderList.stories.tsx @@ -7,6 +7,7 @@ import { defaultOrderTabs, } from './OrderList'; import { Button } from '../Button/Button'; +import { Badge } from '../Badge/Badge'; const meta: Meta = { title: 'Components/OrderList', @@ -14,12 +15,131 @@ const meta: Meta = { tags: ['autodocs'], parameters: { layout: 'padded', + docs: { + description: { + component: + 'A tabbed list view for orders with filtering, search, and customizable rendering. Supports loading states, empty states, and custom actions.', + }, + canvas: { + sourceState: 'shown', + }, + }, + }, + args: { + showSearch: true, + isLoading: false, + emptyMessage: 'No orders found', + searchPlaceholder: 'Search orders...', + }, + argTypes: { + orders: { + description: 'Array of order items to display', + control: false, + }, + activeTab: { + description: 'Currently selected tab ID', + control: 'text', + }, + tabs: { + description: + 'Array of tab configurations with id, label, count, and statuses', + control: false, + }, + onTabChange: { + description: 'Callback when tab selection changes', + action: 'tab changed', + }, + renderOrder: { + description: 'Render function for each order item', + control: false, + }, + getOrderStatus: { + description: 'Function to extract status from order item for filtering', + control: false, + }, + isLoading: { + description: 'Show loading spinner', + control: 'boolean', + }, + emptyMessage: { + description: 'Message to display when no orders', + control: 'text', + }, + emptyIcon: { + description: 'Custom icon for empty state', + control: false, + }, + searchPlaceholder: { + description: 'Placeholder text for search input', + control: 'text', + }, + searchValue: { + description: 'Controlled search input value', + control: 'text', + }, + onSearchChange: { + description: 'Callback when search value changes', + action: 'search changed', + }, + showSearch: { + description: 'Show the search input', + control: 'boolean', + }, + actions: { + description: 'Slot for additional action buttons', + control: false, + }, + className: { + description: 'Additional CSS class names', + control: 'text', + }, }, }; export default meta; type Story = StoryObj; +// Helper to render an order item +function renderOrderItem(order: SampleOrder) { + const statusVariants: Record< + OrderStatus, + 'default' | 'secondary' | 'success' | 'warning' | 'danger' + > = { + pending: 'warning', + active: 'default', + scheduled: 'default', + 'in-progress': 'default', + completed: 'success', + rejected: 'danger', + invoiced: 'success', + cancelled: 'secondary', + }; + + return ( +
+
+
+
+ + {order.orderNumber} + + + {order.status.replace('-', ' ')} + +
+

{order.employeeName}

+

+ {order.services.join(' • ')} +

+
+ + {order.createdAt.toLocaleDateString()} + +
+
+ ); +} + // Sample order type interface SampleOrder { id: string; @@ -90,46 +210,40 @@ const sampleOrders: SampleOrder[] = [ }, ]; -// Simple order item renderer +// Simple order item renderer using Badge component function OrderItem({ order }: { order: SampleOrder }) { - const statusColors: Record = { - pending: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300', - active: 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300', - scheduled: - 'bg-purple-100 text-purple-800 dark:bg-purple-900/50 dark:text-purple-300', - 'in-progress': - 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/50 dark:text-cyan-300', - completed: - 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300', - rejected: 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300', - invoiced: - 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/50 dark:text-emerald-300', - cancelled: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', + const statusVariants: Record< + OrderStatus, + 'default' | 'secondary' | 'success' | 'warning' | 'danger' + > = { + pending: 'warning', + active: 'default', + scheduled: 'default', + 'in-progress': 'default', + completed: 'success', + rejected: 'danger', + invoiced: 'success', + cancelled: 'secondary', }; return ( -
+
- + {order.orderNumber} - + {order.status.replace('-', ' ')} - +
-

- {order.employeeName} -

-

+

{order.employeeName}

+

{order.services.join(' • ')}

- + {order.createdAt.toLocaleDateString()}
@@ -143,11 +257,15 @@ function InteractiveDemo({ tabs = defaultOrderTabs, showSearch = false, showActions = false, + isLoading = false, + emptyMessage, }: { orders?: SampleOrder[]; tabs?: OrderListTab[]; showSearch?: boolean; showActions?: boolean; + isLoading?: boolean; + emptyMessage?: string; }) { const [activeTab, setActiveTab] = useState('all'); const [searchValue, setSearchValue] = useState(''); @@ -161,7 +279,7 @@ function InteractiveDemo({ : orders; return ( -
+
} getOrderStatus={(order) => order.status} + isLoading={isLoading} showSearch={showSearch} searchValue={searchValue} onSearchChange={setSearchValue} + emptyMessage={emptyMessage} actions={ showActions ? ( - ) : undefined } @@ -197,20 +304,107 @@ function InteractiveDemo({ ); } +/** + * Playground wrapper component for proper hook usage + */ +function PlaygroundDemo({ + showSearch, + isLoading, + emptyMessage, + searchPlaceholder, +}: { + showSearch?: boolean; + isLoading?: boolean; + emptyMessage?: string; + searchPlaceholder?: string; +}) { + const [activeTab, setActiveTab] = useState('all'); + const [searchValue, setSearchValue] = useState(''); + + const filteredOrders = searchValue + ? sampleOrders.filter( + (o) => + o.employeeName.toLowerCase().includes(searchValue.toLowerCase()) || + o.orderNumber.toLowerCase().includes(searchValue.toLowerCase()) + ) + : sampleOrders; + + return ( +
+ order.status} + isLoading={isLoading} + showSearch={showSearch} + searchValue={searchValue} + onSearchChange={setSearchValue} + searchPlaceholder={searchPlaceholder} + emptyMessage={emptyMessage} + actions={ + + } + /> +
+ ); +} + export const Default: Story = { - render: () => , + render: (args) => ( + + ), + parameters: { + docs: { + description: { + story: + 'Interactive demo - use controls to toggle showSearch, isLoading, and edit messages.', + }, + }, + }, }; export const WithSearch: Story = { render: () => , + parameters: { + docs: { + description: { + story: + 'Order list with search input for filtering by order number or employee name.', + }, + }, + }, }; export const WithActions: Story = { render: () => , + parameters: { + docs: { + description: { + story: 'Order list with action buttons in the header area.', + }, + }, + }, }; export const WithSearchAndActions: Story = { render: () => , + parameters: { + docs: { + description: { + story: 'Full-featured order list with both search and action buttons.', + }, + }, + }, }; export const SimpleTabs: Story = { @@ -231,13 +425,21 @@ export const SimpleTabs: Story = { ]} /> ), + parameters: { + docs: { + description: { + story: + 'Order list with simplified tab configuration - fewer tabs for simpler workflows.', + }, + }, + }, }; function LoadingWrapper() { const [activeTab, setActiveTab] = useState('all'); return ( -
+
orders={[]} activeTab={activeTab} @@ -252,13 +454,20 @@ function LoadingWrapper() { export const Loading: Story = { render: () => , + parameters: { + docs: { + description: { + story: 'Shows a loading spinner while orders are being fetched.', + }, + }, + }, }; function EmptyWrapper() { const [activeTab, setActiveTab] = useState('all'); return ( -
+
orders={[]} activeTab={activeTab} @@ -273,13 +482,20 @@ function EmptyWrapper() { export const Empty: Story = { render: () => , + parameters: { + docs: { + description: { + story: 'Shows empty state message when no orders are available.', + }, + }, + }, }; function CustomEmptyIconWrapper() { const [activeTab, setActiveTab] = useState('all'); return ( -
+
orders={[]} activeTab={activeTab} @@ -309,4 +525,12 @@ function CustomEmptyIconWrapper() { export const CustomEmptyIcon: Story = { render: () => , + parameters: { + docs: { + description: { + story: + 'Empty state with a custom success icon for "all caught up" scenarios.', + }, + }, + }, }; diff --git a/src/components/OrderList/OrderList.tsx b/src/components/OrderList/OrderList.tsx index 0f023244..5e1e2b54 100644 --- a/src/components/OrderList/OrderList.tsx +++ b/src/components/OrderList/OrderList.tsx @@ -105,10 +105,10 @@ export function OrderList({ }, [orders, tabs, getOrderStatus]); return ( -
+
{/* Header with tabs and actions */}
-
+
{/* Tabs */}
{tabs.map((tab) => {