import type { Meta, StoryObj } from '@storybook/nextjs-vite' import { LayoutGrid, LayoutList, Plus, RefreshCw } from 'lucide-react' import { useState } from 'react' import { ListPageLayout } from '../components/layout/list-page-layout' import { Button } from '../components/ui/button' import type { FilterGroup, SortConfig, SortDirection } from '../components/ui/filter-modal' import { Table, type TableColumn, type TableFilters } from '../components/ui/table' interface Device { id: string name: string status: 'online' | 'offline' | 'pending' type: string lastSeen: string } const sampleDevices: Device[] = [ { id: '1', name: 'MacBook Pro', status: 'online', type: 'Laptop', lastSeen: '2024-01-20' }, { id: '2', name: 'iPhone 15', status: 'online', type: 'Mobile', lastSeen: '2024-01-20' }, { id: '3', name: 'Windows Desktop', status: 'offline', type: 'Desktop', lastSeen: '2024-01-15' }, { id: '4', name: 'iPad Air', status: 'pending', type: 'Tablet', lastSeen: '2024-01-18' }, { id: '5', name: 'Linux Server', status: 'online', type: 'Server', lastSeen: '2024-01-20' }, ] const deviceColumns: TableColumn[] = [ { key: 'name', label: 'Device Name' }, { key: 'type', label: 'Type', hideAt: 'lg', filterable: true, filterOptions: [ { id: 'laptop', label: 'Laptop', value: 'laptop' }, { id: 'mobile', label: 'Mobile', value: 'mobile' }, { id: 'desktop', label: 'Desktop', value: 'desktop' }, { id: 'tablet', label: 'Tablet', value: 'tablet' }, { id: 'server', label: 'Server', value: 'server' }, ] }, { key: 'status', label: 'Status', filterable: true, filterOptions: [ { id: 'online', label: 'Online', value: 'online' }, { id: 'offline', label: 'Offline', value: 'offline' }, { id: 'pending', label: 'Pending', value: 'pending' }, ] }, { key: 'lastSeen', label: 'Last Seen' }, ] const meta = { title: 'Layout/ListPageLayout', component: ListPageLayout, parameters: { layout: 'fullscreen', docs: { description: { component: ` A standardized layout component for list-based pages throughout the OpenFrame application. ## Layout Structure \`\`\` ┌─────────────────────────────────────────────────────────────┐ │ Title (left aligned) │ Header Actions (right aligned) │ ├─────────────────────────────────────────────────────────────┤ │ Search Bar (full width) │ ├─────────────────────────────────────────────────────────────┤ │ Table/Grid with Filters │ │ (main content) │ └─────────────────────────────────────────────────────────────┘ \`\`\` ## Key Features - **Consistent Spacing**: All pages use identical padding and gaps - **Responsive Design**: Works seamlessly across all screen sizes - **Accessibility**: Proper semantic HTML and ARIA support - **Error Handling**: Built-in error state display - **Flexible Actions**: Supports any combination of buttons/controls - **Search Integration**: Standardized search bar positioning ` } } }, tags: ['autodocs'], argTypes: { padding: { control: 'select', options: ['none', 'sm', 'md', 'lg'], description: 'Container padding size' }, background: { control: 'select', options: ['default', 'card', 'transparent'], description: 'Container background style' } } } satisfies Meta export default meta type Story = StoryObj /** * Basic list page layout with a title, search bar, and table content. */ export const Basic: Story = { args: { title: 'The quick brown fox', searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, children: ( ) } } /** * List page with header actions including refresh and add buttons. */ export const WithHeaderActions: Story = { args: { title: 'Devices', headerActions: (
), searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, children: (
) } } /** * List page with view toggle actions (table/grid view). */ export const WithViewToggle: Story = { args: { title: 'Devices', headerActions: (
), searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, children: (
) } } /** * List page displaying an error state. */ export const WithError: Story = { args: { title: 'Devices', searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, error: 'Failed to load devices. Please try again later.', children: null } } /** * List page with pre-filled search value. */ export const WithSearchValue: Story = { args: { title: 'Devices', headerActions: ( ), searchPlaceholder: 'Search devices...', searchValue: 'MacBook', onSearch: () => {}, children: (
d.name.includes('MacBook'))} columns={deviceColumns} rowKey="id" /> ) } } /** * List page with empty data state. */ export const EmptyState: Story = { args: { title: 'Devices', headerActions: ( ), searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, children: (
) } } /** * List page with small padding. */ export const SmallPadding: Story = { args: { title: 'Compact View', searchPlaceholder: 'Search...', searchValue: '', onSearch: () => {}, padding: 'sm', children: (
) } } /** * List page with large padding. */ export const LargePadding: Story = { args: { title: 'Spacious View', searchPlaceholder: 'Search...', searchValue: '', onSearch: () => {}, padding: 'lg', children: (
) } } /** * List page with card background. */ export const CardBackground: Story = { args: { title: 'Devices', searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, background: 'card', children: (
) } } /** * Interactive example with working search functionality. */ export const Interactive: Story = { args: { title: 'Devices', searchPlaceholder: 'Search by name or type...', searchValue: '', onSearch: () => {}, children: null }, render: function InteractiveStory() { const [searchValue, setSearchValue] = useState('') const filteredDevices = sampleDevices.filter(device => device.name.toLowerCase().includes(searchValue.toLowerCase()) || device.type.toLowerCase().includes(searchValue.toLowerCase()) ) return ( } searchPlaceholder="Search by name or type..." searchValue={searchValue} onSearch={setSearchValue} >
) } } /** * Example mimicking the Scripts page layout. */ export const ScriptsPageExample: Story = { args: { title: 'Scripts', headerActions: (
), searchPlaceholder: 'Search scripts...', searchValue: '', onSearch: () => {}, children: (
[]} rowKey="id" /> ) } } /** * Example mimicking the Logs page layout. */ export const LogsPageExample: Story = { args: { title: 'Logs', headerActions: ( ), searchPlaceholder: 'Search logs...', searchValue: '', onSearch: () => {}, children: (
[]} rowKey="id" /> ) } } /** * Interactive example with mobile filter button. * Resize to mobile viewport to see the filter button next to search bar. */ export const WithMobileFilter: Story = { args: { title: 'Devices', searchPlaceholder: 'Search devices...', searchValue: '', onSearch: () => {}, children: null, }, render: function WithMobileFilterStory() { const [searchValue, setSearchValue] = useState('') const [filters, setFilters] = useState({}) const [sortBy, setSortBy] = useState() const [sortDirection, setSortDirection] = useState() // Define filter groups const filterGroups: FilterGroup[] = [ { id: 'status', title: 'Status', options: [ { id: 'online', label: 'Online', count: 3 }, { id: 'offline', label: 'Offline', count: 1 }, { id: 'pending', label: 'Pending', count: 1 }, ], }, { id: 'type', title: 'Device Type', options: [ { id: 'Laptop', label: 'Laptop', count: 1 }, { id: 'Mobile', label: 'Mobile', count: 1 }, { id: 'Desktop', label: 'Desktop', count: 1 }, { id: 'Tablet', label: 'Tablet', count: 1 }, { id: 'Server', label: 'Server', count: 1 }, ], }, ] // Define sort config const sortConfig: SortConfig = { columns: [ { key: 'name', label: 'Device Name' }, { key: 'type', label: 'Type' }, { key: 'status', label: 'Status' }, { key: 'lastSeen', label: 'Last Seen' }, ], sortBy, sortDirection, } // Filter devices based on search and filters const filteredDevices = sampleDevices.filter((device) => { // Search filter const matchesSearch = searchValue === '' || device.name.toLowerCase().includes(searchValue.toLowerCase()) || device.type.toLowerCase().includes(searchValue.toLowerCase()) // Status filter const statusFilter = filters.status || [] const matchesStatus = statusFilter.length === 0 || statusFilter.includes(device.status) // Type filter const typeFilter = filters.type || [] const matchesType = typeFilter.length === 0 || typeFilter.includes(device.type) return matchesSearch && matchesStatus && matchesType }) // Sort devices const sortedDevices = [...filteredDevices].sort((a, b) => { if (!sortBy) return 0 const aValue = a[sortBy as keyof Device] const bValue = b[sortBy as keyof Device] if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1 if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1 return 0 }) const handleSort = (column: string, direction: SortDirection) => { setSortBy(column) setSortDirection(direction) } return ( } searchPlaceholder="Search devices..." searchValue={searchValue} onSearch={setSearchValue} mobileFilterGroups={filterGroups} onMobileFilterChange={setFilters} currentMobileFilters={filters} mobileSortConfig={sortConfig} onMobileSort={handleSort} mobileFilterTitle="Sort and Filter" >
) }, }