Table List Component Pattern
This document describes the standardized pattern for creating table/list components in the FlowPOS frontend-pwa application.
Overview
All table/list components should follow a consistent pattern that includes:
- Sorting functionality with visual indicators
- Responsive design (full width on large screens, mobile cards)
- Consistent styling (padding, hover effects, sticky actions column)
- Reusable sorting hook for state management
Pattern Components
1. Sorting Hook (useTableSort)
The useTableSort hook manages sorting state and provides utilities for sorting data.
Location: src/hooks/useTableSort.ts
Usage in Parent Component:
import { useTableSort } from "@/hooks/useTableSort";
export default function MyPage() {
const { sortField, sortDirection, handleSort } = useTableSort();
// ... rest of component
}
Props to pass to List component:
sortField={sortField}sortDirection={sortDirection}onSort={handleSort}
2. List Component Structure
Required Props Interface
export interface MyListProps {
items: ItemType[];
sortField: string;
sortDirection: "asc" | "desc";
onSort: (field: string) => void;
// ... other props (onEdit, onDelete, etc.)
}
Required Imports
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
Search as SearchIcon,
X,
} from "lucide-react";
import { getSortedData, type SortConfig } from "@/hooks/useTableSort";
Component Structure
- Sort Icon Function:
const getSortIcon = (field: string) => {
if (sortField !== field) {
return <ArrowUpDown className="w-4 h-4 opacity-50" />;
}
return sortDirection === "asc" ? (
<ArrowUp className="w-4 h-4" />
) : (
<ArrowDown className="w-4 h-4" />
);
};
- Sort Configuration:
const sortConfig: SortConfig<ItemType> = {
name: (item) => item.name || "",
email: (item) => item.email || "",
// Add all sortable fields
};
const sortedItems = getSortedData(items, sortConfig, sortField, sortDirection);
- Container Styling:
return (
<div className="w-full space-y-4">
{/* Header with search and create button */}
{/* Table/Cards */}
</div>
);
- Table Header with Sort Buttons:
<th className="px-4 py-3 text-left text-sm font-semibold">
<button
type="button"
className="flex items-center gap-2 hover:bg-muted/80 transition-colors rounded px-1 py-1 -mx-1 -my-1"
onClick={() => onSort("fieldName")}
aria-label={`Sort by ${t("translation.key")}`}
>
{t("translation.key")}
{getSortIcon("fieldName")}
</button>
</th>
- Table Body Styling:
<tr className="hover:bg-muted/50 transition">
<td className="px-4 py-3 text-sm text-foreground">
{/* Content */}
</td>
</tr>
- Sticky Actions Column:
<th className="px-4 py-3 text-right text-sm font-semibold sticky right-0 bg-muted z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.1)] no-print-col">
{t("common.actions")}
</th>
<td className="px-4 py-3 text-sm sticky right-0 bg-card shadow-[-2px_0_4px_rgba(0,0,0,0.1)] no-print-col">
{/* Action buttons */}
</td>
- Empty State:
{sortedItems.length === 0 ? (
<div className="bg-card rounded-lg border border-border">
<div className="px-4 py-8 text-center text-muted-foreground">
{t("noItems")}
</div>
</div>
) : (
// Table/Cards
)}
- Mobile Cards:
<div className="md:hidden flex flex-col gap-4 p-4">
{sortedItems.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
{t("noItems")}
</div>
) : (
sortedItems.map((item) => (
<div
key={item.id}
className="border border-border rounded-xl p-4 shadow-sm bg-card text-card-foreground"
>
{/* Card content */}
</div>
))
)}
</div>
3. Parent Page Component
Required State
// Sorting state
const [sortField, setSortField] = useState<string>("");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
Sorting Handler
const handleSort = (field: string) => {
if (sortField === field) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortDirection("asc");
}
};
Container Styling
return (
<div className="w-full max-w-full px-4 py-6 md:px-6 lg:px-8">
{/* Form or List component */}
</div>
);
Styling Standards
Table Styling
- Header padding:
px-4 py-3 - Cell padding:
px-4 py-3 - Text size:
text-sm - Hover effect:
hover:bg-muted/50 - Header background:
bg-muted text-muted-foreground - Border:
divide-y divide-border
Responsive Design
- Desktop table:
hidden md:table - Mobile cards:
md:hidden - Container:
w-fullfor full width on large screens - Responsive padding:
px-4 py-6 md:px-6 lg:px-8
Actions Column
- Sticky positioning:
sticky right-0 - Background:
bg-muted(header),bg-card(cells) - Shadow:
shadow-[-2px_0_4px_rgba(0,0,0,0.1)] - Z-index:
z-10(header only)
Example: Complete Implementation
See the following components for reference implementations:
CustomerList.tsx- Full example with multiple sortable columnsSupplierList.tsx- Similar patternProductList.tsx- Complex example with many columnsBrandList.tsx- Simple example with single columnModelList.tsx- Two-column example
Checklist for New List Components
- Import sorting icons from
lucide-react - Import
getSortedDataandSortConfigfrom@/hooks/useTableSort - Add
sortField,sortDirection,onSortto props interface - Implement
getSortIconfunction - Create
sortConfigobject with all sortable fields - Use
getSortedDatato get sorted items - Add sort buttons to all sortable column headers
- Apply consistent styling (
py-3,hover:bg-muted/50, etc.) - Add sticky actions column with proper styling
- Implement responsive mobile cards
- Update parent page with sorting state and handler
- Make container full width (
w-full max-w-full) - Test sorting on all columns
- Test responsive behavior
Migration Guide
To migrate an existing list component:
- Add sorting props to component interface
- Import sorting utilities and icons
- Add sorting state to parent component
- Implement sort handler in parent
- Add sort icon function to list component
- Create sort config and use
getSortedData - Update table headers with sort buttons
- Apply styling standards (padding, hover, sticky column)
- Update container to full width
- Test thoroughly
Notes
- Sorting is client-side only (sorts the current page of data)
- The pattern uses TypeScript for type safety
- All components should be accessible (aria-labels on sort buttons)
- Print styles are handled with
no-print-colclass - Mobile cards should show all important information