# Pattern — `{Entity}ListPage.tsx`

## When to use

Any page that renders a flat list of entities: contacts, leads, tickets, orders,
products. Columns are known and finite (≤10). Search + pagination are expected.

For grid-of-cards (gallery), use `entity-card.md`. For pipeline stages,
use `kanban-board.md`.

## Anatomy

```
PermissionGuard (read)
  PageTemplate (title, icon, actions)
    Slot             — {entity}.header.actions
    error banner     — token-based (R7)
    Slot             — {entity}.table.columns.before
    isLoading ?      — Loader2 animate-spin
      DataTable<T>   — from @atlashub/smartstack
    Slot             — {entity}.table.columns.after
```

## Canonical example (compliant)

```tsx
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Loader2, Plus, Pencil, Trash2, List } from 'lucide-react';
import { PermissionGuard, Slot, DataTable } from '@atlashub/smartstack';
import { PageTemplate } from '@/components/ui/PageTemplate';
import { useContacts, useDeleteContact } from '../hooks/useContact';
import type { Contact } from '../types';

type DataTableColumn<T> = {
  key: string;
  label: string;
  sortable?: boolean;
  align?: 'left' | 'center' | 'right';
  render?: (item: T) => ReactNode;
};

export function ContactListPage() {
  const { t } = useTranslation('contact');
  const navigate = useNavigate();
  const { data, isLoading, error } = useContacts();
  const deleteMutation = useDeleteContact();

  const columns: DataTableColumn<Contact>[] = [
    { key: 'lastName', label: t('list.columns.lastName'), sortable: true },
    { key: 'email', label: t('list.columns.email'), sortable: true },
    {
      key: '__actions',
      label: t('list.actions'),
      align: 'right',
      render: (item) => (
        <div className="flex items-center justify-end gap-1">
          <PermissionGuard permission="crm.contacts.update">
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); navigate(`./${item.id}/edit`); }}
              className="p-1.5 rounded-[var(--radius-badge)] hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
              title={t('list.edit')}
            >
              <Pencil className="w-4 h-4" />
            </button>
          </PermissionGuard>
          <PermissionGuard permission="crm.contacts.delete">
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); void deleteMutation.mutateAsync(item.id); }}
              className="p-1.5 rounded-[var(--radius-badge)] hover:bg-[var(--error-bg)] text-[var(--text-muted)] hover:text-[var(--error-text)]"
              title={t('list.delete')}
            >
              <Trash2 className="w-4 h-4" />
            </button>
          </PermissionGuard>
        </div>
      ),
    },
  ];

  return (
    <PermissionGuard permission="crm.contacts.read">
      <PageTemplate
        title={t('list.title')}
        subtitle={t('list.subtitle')}
        icon={<List className="w-6 h-6" />}
        actions={
          <PermissionGuard permission="crm.contacts.create">
            <button
              type="button"
              onClick={() => navigate('./new/edit')}
              className="flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white text-sm font-medium hover:bg-[var(--color-accent-600)] transition-colors"
            >
              <Plus className="w-4 h-4" />
              {t('list.create')}
            </button>
          </PermissionGuard>
        }
      >
        <Slot name="contact.header.actions" />

        {error && (
          <div className="mb-4 p-3 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)] text-sm text-[var(--error-text)]">
            {t('list.error')}: {String(error)}
          </div>
        )}

        <Slot name="contact.table.columns.before" />

        {isLoading ? (
          <div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
            <Loader2 className="w-5 h-5 animate-spin mr-2" />
            {t('list.loading')}
          </div>
        ) : (
          <DataTable<Contact>
            data={data?.items ?? []}
            columns={columns}
            searchable
            searchPlaceholder={t('list.search')}
            pagination={{ pageSize: 20, showSizeSelector: true }}
            getRowKey={(item) => item.id}
            emptyMessage={t('list.empty')}
            onRowClick={(item) => navigate(`./${item.id}`)}
          />
        )}

        <Slot name="contact.table.columns.after" />
      </PageTemplate>
    </PermissionGuard>
  );
}
```

## Rules this pattern enforces

- **R1** — `<PageTemplate>` wrap
- **R3** — primary CTA uses `bg-[var(--color-accent-500)] text-white hover:bg-[var(--color-accent-600)]`
- **R4** — every user-facing string flows through `useTranslation('entity')` + `t('key')`
- **R5** — icons only from `lucide-react`
- **R6** — loading state = `<Loader2 className="animate-spin" />`
- **R8** — `<DataTable>` (npm `@atlashub/smartstack`), NOT raw `<table>`
- **R10** — permission keys `{module}.{section}.{action}` (no appCode prefix)
- **R11** — Slot injection points: `{entity}.header.actions`, `{entity}.table.columns.before/after`
- **R13** — status badges use `bg-[var(--<status>-bg)]` anatomy
- **R16** — radius tokens (`rounded-[var(--radius-button|card|badge)]`)
- **R19** — no palette (`bg-gray-*`, `text-blue-*`) nor absolute (`bg-white`, `bg-black`) colors
- **R20** — borders always paired with `border-[var(--border-color)]`

## Import contract

| Name | From | Notes |
|---|---|---|
| `PermissionGuard`, `Slot`, `DataTable` | `@atlashub/smartstack` | npm package |
| `PageTemplate` | `@/components/ui/PageTemplate` | local baseline file |
| Icons (Loader2, Plus, Pencil, Trash2, List) | `lucide-react` | no other icon lib |
| `useTranslation` | `react-i18next` | |
| `useNavigate` | `react-router-dom` | |

**DataTableColumn type is NOT re-exported from `@atlashub/smartstack`** — declare
the minimal local type alias as shown (lines 12-18 of the canonical example).

## Do NOT

- Raw `<table>` / `<thead>` / `<tbody>` — use `<DataTable>`
- `bg-white` → `bg-[var(--bg-card)]`
- `bg-gray-50` → `bg-[var(--bg-hover)]` or `bg-[var(--bg-secondary)]`
- `border` alone → `border border-[var(--border-color)]`
- `text-blue-700` → `text-[var(--info-text)]`
- `className="bg-[#6366f1]"` → use the accent token
- Hardcoded user-facing strings — always `t('key')`
- `<div onClick>` for buttons — use `<button type="button">`
