# Pattern — Grid of `<EntityCard>`

## When to use

Gallery-style lists where each row is a rich card (avatar + title + subtitle
+ stats + tags + actions) rather than a dense table. Typical use cases:
theme presets, template catalogues, dashboards with tenant cards.

If the display is a flat list with columns → use `list-page.md` + `<DataTable>`.
If the display is a kanban with drag-drop → use `kanban-board.md`.

## Anatomy

```
PermissionGuard (read)
  PageTemplate (title, icon, actions)
    Slot                 — {entity}.header.actions
    filter bar           — optional, bg-[var(--bg-card)]
    grid container       — grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4
      EntityCard × N     — from @atlashub/smartstack
        avatar, title, subtitle, description, stats[], badge, tags[], actions
    Slot                 — {entity}.card.content (per-card extension)
```

## Canonical example (compliant)

```tsx
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Plus, Palette } from 'lucide-react';
import { PermissionGuard, Slot, EntityCard } from '@atlashub/smartstack';
import { PageTemplate } from '@/components/ui/PageTemplate';
import { useThemes } from '../hooks/useTheme';

export function ThemeGalleryPage() {
  const { t } = useTranslation('theme');
  const navigate = useNavigate();
  const { data: themes = [] } = useThemes();

  return (
    <PermissionGuard permission="platform.theming.read">
      <PageTemplate
        title={t('list.title')}
        subtitle={t('list.subtitle')}
        icon={<Palette className="w-6 h-6" />}
        actions={
          <PermissionGuard permission="platform.theming.create">
            <button
              type="button"
              onClick={() => navigate('./new')}
              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="theme.header.actions" />

        <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 p-4">
          {themes.map((theme) => (
            <EntityCard
              key={theme.id}
              title={theme.name}
              subtitle={theme.mode === 'dark' ? t('list.subtitle.dark') : t('list.subtitle.light')}
              description={theme.description ?? undefined}
              badge={theme.isDefault ? {
                label: t('list.badge.default'),
                variant: 'success',
              } : undefined}
              stats={[
                { label: t('list.stats.tenants'), value: theme.tenantCount },
                { label: t('list.stats.presets'), value: theme.presetCount },
              ]}
              tags={theme.tags ?? []}
              actions={[
                {
                  label: t('list.actions.preview'),
                  onClick: () => navigate(`./${theme.id}/preview`),
                },
                {
                  label: t('list.actions.edit'),
                  onClick: () => navigate(`./${theme.id}/edit`),
                  permission: 'platform.theming.update',
                },
              ]}
              onClick={() => navigate(`./${theme.id}`)}
              customFooter={<Slot name="theme.card.content" context={{ theme }} />}
            />
          ))}
        </div>
      </PageTemplate>
    </PermissionGuard>
  );
}
```

## Rules this pattern enforces

- **R1** — `<PageTemplate>`
- **R3** — primary CTA accent
- **R4** — every visible string via `t('...')`
- **R5** — lucide-react icons
- **R8** — NOT applicable (grid of cards is an explicit exception to "lists use DataTable")
- **R10** — `platform.theming.<action>` permission format
- **R11** — Slot header.actions + per-card Slot
- **R19** — no palette / absolute colors (EntityCard internals handle their own theming)
- **R20** — no bare borders outside of EntityCard itself

## EntityCard prop contract

| Prop | Type | Purpose |
|---|---|---|
| `avatar` | `ReactNode` | optional leading visual (icon, color swatch, image) |
| `title` | `string` | card header, always present |
| `subtitle` | `string \| null` | optional secondary line |
| `description` | `string \| null` | optional paragraph body |
| `stats` | `Array<{label,value}>` | 2-4 key metrics rendered inline |
| `badge` | `{label, variant}` | status chip (success/warning/error/info) |
| `tags` | `string[]` | pill list rendered at bottom |
| `links` | `Array<{label,href}>` | external refs rendered as text links |
| `actions` | `Array<{label,onClick,permission?}>` | CTA buttons — `permission` auto-wraps in `<PermissionGuard>` |
| `onClick` | `() => void` | makes the whole card clickable |
| `customHeader` | `ReactNode` | replaces the default title/subtitle block |
| `customBody` | `ReactNode` | replaces the default description/stats block |
| `customFooter` | `ReactNode` | injected below tags (good place for a `<Slot>`) |

## Import contract

| Name | From |
|---|---|
| `PermissionGuard`, `Slot`, `EntityCard` | `@atlashub/smartstack` |
| `PageTemplate` | `@/components/ui/PageTemplate` |
| Icons | `lucide-react` |
| `useTranslation` | `react-i18next` |
| `useNavigate` | `react-router-dom` |

## Do NOT

- Hand-roll a `<div className="rounded-lg bg-white shadow"><h3>…</h3>…</div>`
  card — use `<EntityCard>`. The component handles light/dark, radius tokens,
  hover shadows, accessibility roles.
- Pass `badge={{ label: 'Active', variant: 'green' }}` — the variant is
  `success | warning | error | info | accent`, not a color name.
- Use `grid-cols-4` or `grid-cols-5` — baseline is `1 / md:2 / xl:3`.
- Put the action buttons inside `customFooter` — use the `actions` prop so
  EntityCard handles permissions + keyboard focus uniformly.
