ListPanel
ImplementedSlot-based list host for the recurring header + toolbar + table + pagination pattern. Renders a token-driven header (title + actions slot), a renderTable slot for table ecosystem choice (ADR-033-compliant storefront DataTable OR DS AdminDataTable), optional toolbar/filter slot, density: default|compact for POSRegister-shaped layouts (cashier/scanner), inline loading skeleton and empty states, native button pagination. Used in storefront CRUD pages, dashboard tables, and POS register pattern (recipe cited in docs/components/organisms/listpanel.recipes.md).
components/organisms/ListPanel/ListPanel.tsxSlot-based list host for the recurring "header + toolbar + table + pagination" pattern. ListPanel owns the chrome (header, toolbar slot, loading/empty states, pagination); the table itself is caller-owned via the renderTable slot — the canonical composition hands a DataTable to it. `density="compact"` switches to the POS register shape (cashier/scanner). Grouped tables pass rowGroups through to the slot. See `listpanel.recipes.md` for the POSRegister recipe validated against 6 community Medusa POS sources.
ListPanel
Organism — Account & Admin family (Family 9)
Composes a header (title + actions slot), an optional toolbar slot, the renderTable slot (caller provides the table body — ADR-033 table ecosystem choice), the loading/empty state machine (inline skeleton dot and empty block — hand-rolled, not the Skeleton/EmptyState atoms), and native-button pagination that renders only when total > pageSize.
a. Regular density — DataTable composition
Tickets
| Event | Qty |
|---|---|
| Festival Jazz MTL | 4 |
| OSM Symphonie n° 5 | 2 |
| Lion d'Or Gala | 1 |
| Just for Laughs | 3 |
| FrancoFolies | 2 |
b. Compact density — POSRegister shape
Register — $128.00
- T-shirt M× 1
- Poster A2× 2
- VIP pass× 1
c. Grouped — rowGroups via slots.rowGroups → DataTable
Sales by product family
| Event | Qty |
|---|---|
| Jazz & blues | |
| Miles Davis Tribute | 2 |
| Diana Krall Quartet | 4 |
| Classical | |
| Bach Brandenburg 3 | 2 |
| Beethoven Symphony 9 | 1 |
| World music | |
| Afrobeat Night | 3 |
| Celtic Folk Fest | 2 |
d. Loading — inline skeleton dot
Loading orders
e. Empty — inline empty block with emptyAction
Packages
No packages yet
f. Pagination-visible — total > pageSize
All rows
| Name | Qty |
|---|---|
| Row 1 | 1 |
| Row 2 | 2 |
| Row 3 | 3 |
| Row 4 | 4 |
| Row 5 | 5 |
| Row 6 | 6 |
| Row 7 | 7 |
| Row 8 | 8 |
| Row 9 | 9 |
| Row 10 | 10 |
This component supports light and dark mode.
Props
titlestring—Header title — also the region’s aria-label
actionsReactNodenullSlot for header buttons (e.g. + Create)
toolbarReactNodenullSlot for filter/search controls — compose FilterChipGroup here
renderTable*(slots) => ReactNodeSlot: caller provides the table body; ListPanel wraps with header, density padding, loading/empty wrappers, and pagination
rowGroupsListPanelRowGroup<T>[]—Grouped-table support: labeled row clusters handed to the renderTable slot as slots.rowGroups (e.g. forwarded to DataTable’s rowGroups → th[scope="rowgroup"] headers); items derives by flattening the groups when not passed
density"default" | "compact""default"default = storefront spacing; compact = POSRegister-shaped (cashier/scanner register, receipt lists)
itemsT[][]Array of items for pagination/empty computation (derived from rowGroups when omitted)
loadingbooleanfalseWhen true, renders an inline skeleton dot (animate-pulse) instead of the table
emptybooleanitems.length === 0When true, renders an inline empty block instead of the table
emptyTitlestring"No items"Title in the inline empty block
emptyActionReactNodenullCTA slot inside the inline empty block
pagenumber1Current page (1-indexed)
pageSizenumber25Items per page (drives the Page X / Y computation)
totalnumberitems.lengthTotal items across all pages — pagination renders only when total > pageSize
onPageChange(page: number) => void—Pagination callback (Prev/Next are no-ops without it)
Props
ListPanel
titlestring—Header title
actionsReactNodenullSlot for header buttons (e.g. + Create)
toolbarReactNodenullSlot for filter/search controls — compose FilterChipGroup here
renderTable(state) => ReactNode—Slot: caller provides the table body; ListPanel wraps with header, density padding, skeleton/empty wrappers, and pagination
densitydefault | compactdefaultdefault = storefront spacing (table-card idiom); compact = POSRegister-shaped (cashier/scanner register, receipt lists)
itemsT[][]Array of items for pagination skeleton/empty computation
rowGroupsListPanelRowGroup<T>[]—Grouped-table support: labeled row clusters passed through the renderTable slot (slots.rowGroups); items derive by flattening the groups when not passed
loadingbooleanfalseWhen true, renders an inline skeleton dot (animate-pulse) instead of the table
emptybooleanitems.length === 0When true, renders an inline empty block instead of the table
emptyTitlestring"No items"Title in the inline empty block (used when empty=true)
emptyActionReactNodenullCTA slot inside the inline empty block
pagenumber1Current page (1-indexed)
pageSizenumber25Total items per page
totalnumberitems.lengthTotal items across all pages
onPageChange(page: number) => void—Pagination callback
<ListPanel />Accessibility
behavior
1 rulestate
1 ruleUsage
Import
import { ListPanel } from "@epasslive/design-system"Example 1
<ListPanel title=Tickets renderTable={({items}) => <DataTable rows={items} />} />Example 2
<ListPanel density=compact renderTable={({items}) => <RegisterTable rows={items} />} />Example 3
<ListPanel rowGroups={groups} renderTable={({items, rowGroups}) => <DataTable rows={items} rowGroups={rowGroups} />} />Anti-patterns
— Do not hand-roll header+Skeleton+EmptyState+pagination composition — ListPanel is the slot composition
— Do not put POSRegister as a wrapper organism over ListPanel — the recipe pattern keeps organisms neutral