---
phase: screens
kind: reference
---

# React Templates — How a `screen.md` spec becomes real code

The screens captured in `screen.md` are designed to be lifted directly into a
SmartStack target app. Each SmartComponent type maps to a concrete production
page that `scaffold-component` (and the rest of the frontend dev skills)
generates in SmartStack.app (`web/smartstack-web/src/components/ui/`):

| SmartComponent type | Production page (SmartStack.app)              |
|---------------------|------------------------------------------------|
| `SmartListView`     | `*ListPage.tsx` (`<FiltersBar>` + `<DataTable>`) — single page, `scaffold-component` `list` view |
| `SmartForm`         | `*FormPage.tsx` / `*DetailPage.tsx` — sectioned (SectionCard per category) or tabbed layout; the FormPage's EDIT mode is **read-first** by default (sections in read mode, per-section "Modifier" toggle — create stays direct); related tabs are generated child components in the SAME file (see the 360 section below) |
| `SmartDashboard`    | dashboard widget grid (custom analytics)       |
| `SmartKanban`       | `KanbanBoard.tsx`                              |
| `SmartCard`         | `EntityCard.tsx` (tile/gallery)                |
| `SmartAppHome`      | `App*HomePage.tsx` — KPIs + module quickLinks  |
| `SmartModuleHome`   | `Module*HomePage.tsx` — KPIs + section quickLinks |
| `SmartSectionHome`  | `Section*HomePage.tsx` — KPIs + resource quickLinks |

The 1:1 page mapping is intentional: each BA screen becomes exactly one React
page in the generated app, so the spec you write is the page that ships.

## SmartListView → single ListPage

A SmartListView spec like:

```json
{
  "code": "SCR-HR-EMP-LIST-001",
  "type": "SmartListView",
  "entity": "Employee",
  "config": {
    "filters": {
      "orientation": "horizontal",
      "fields": [
        { "field": "status", "label": "Status", "type": "select", "options": ["active","inactive"] },
        { "field": "departmentId", "label": "Department", "type": "lookup", "entity": "Department" }
      ]
    },
    "columns": [
      { "field": "fullName", "label": "Name", "type": "text", "sortable": true },
      { "field": "status",   "label": "Status", "type": "badge", "options": ["active","inactive"] }
    ],
    "actions": [
      { "key": "create", "label": "New", "permission": "hr.employees.create" }
    ],
    "rowClickTarget": "SCR-HR-EMP-DETAIL-001"
  }
}
```

translates almost 1-to-1 to the final React page (one `EmployeesListPage.tsx`
file with filters AND table inside it):

```tsx
export function EmployeesListPage() {
  const [filters, setFilters] = useState<EmployeeFilters>({});
  const { data, loading } = useEmployees(filters);
  const { can } = usePermissions();
  const navigate = useNavigate();

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold text-[var(--text-primary)]">Employees</h1>
        {can('hr.employees.create') && (
          <button onClick={() => navigate('create')}
                  className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded">
            New
          </button>
        )}
      </div>
      <FiltersBar
        orientation="horizontal"
        fields={[
          { field: 'status',     label: 'Status',     type: 'select', options: ['active','inactive'] },
          { field: 'department', label: 'Department', type: 'lookup', entity: 'Department' },
        ]}
        value={filters}
        onChange={setFilters}
      />
      <DataTable
        data={data ?? []}
        loading={loading}
        columns={[
          { key: 'fullName', label: 'Name', sortable: true },
          { key: 'status',   label: 'Status',
            render: (row) => <Badge value={row.status} options={['active','inactive']} /> },
        ]}
        onRowClick={(row) => navigate(`detail/${row.id}`)}
        pagination={{ pageSize: 25 }}
      />
    </div>
  );
}
```

The structural transformation is mechanical:
- `screen.title` → page header `<h1>`
- `config.filters.fields[]` → `<FiltersBar>` props (orientation + fields)
- `config.actions[]` (top-level) → header buttons, each gated by `can(action.permission)`
- `config.columns[]` → DataTable `columns` prop, with `sortable`/`filterable`
  passed through verbatim and `type` driving the `render` function
- `config.actions[]` (`rowLevel: true`) → DataTable row action handlers
- `config.rowClickTarget` → `onRowClick` that navigates to the target screen

## SmartForm with relatedTabs → DetailPage with the 360 tabbed shell

For SmartForm, the inner `tabs[]` and `relatedTabs[]` collapse into ONE
`<TabStrip>` (the scaffold-ui-primitives tab-bar container: two-layer
underlined tabs, hidden scrollbar, chevron nudge arrows on overflow,
URL-synced `?tab=key`) — field tabs first, related tabs after.
`scaffold-component` emits, IN THE SAME FILE, one child component per related
tab; the child owns its hooks so an inactive tab never fetches. This is the
REAL generated shape (there is no `<RelatedTab>` primitive — the tab body is
generated inline per `displayMode`):

```tsx
import { TabStrip } from '@/components/ui/TabStrip'

export function ClientDetailPage() {
  const { id } = useParams<{ id: string }>()
  const { data, isLoading } = useClient(id)
  // …URL-synced activeTab/switchTab state…
  return (
    <PermissionGuard permission="crm.clients.read">
      <PageTemplate title={headerTitle} /* … */>
        <TabStrip label={t('client.detail.tabs.label', { defaultValue: 'Sections' })} activeKey={activeTab}>
          <button role="tab" id="tab-info" /* … */>{t('client.detail.tabs.info')}</button>
          {/* related triggers are permission-gated per tab: */}
          <PermissionGuard permission="crm.invoices.read">
            <button role="tab" id="tab-invoices" /* … */>{t('client.detail.related.invoices.label')}</button>
          </PermissionGuard>
        </TabStrip>

        {activeTab === 'info' && (/* <dl> of the entity's OWN fields */)}
        {activeTab === 'invoices' && (
          <PermissionGuard permission="crm.invoices.read">
            <div role="tabpanel" id="tabpanel-invoices" aria-labelledby="tab-invoices">
              <ClientRelatedInvoicesTab relatedId={id} />
            </div>
          </PermissionGuard>
        )}
      </PageTemplate>
    </PermissionGuard>
  )
}

/** Emitted in the SAME file — displayMode "table": server-paged embedded list. */
function ClientRelatedInvoicesTab({ relatedId }: { relatedId: string }) {
  const { t } = useTranslation('crm')
  const navigate = useNavigate()
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  // The FK filter key IS the declared relationship FK, verbatim — the backend
  // GetAll exposes the matching `[FromQuery] Guid? clientId`.
  const { data, isLoading, error } = useInvoices({ page, pageSize, clientId: relatedId })
  return (
    <div>
      {/* « Créer » pre-fills the relation: create()?clientId={relatedId} */}
      <ResponsiveDataTable<InvoiceListDto>
        data={data?.items ?? []}
        columns={columns}
        serverMode
        page={page}
        totalCount={data?.totalCount ?? 0}
        onPageChange={setPage}
        onRowClick={(item) => navigate(routes.invoices.detail(item.id))}
        /* … */
      />
    </div>
  )
}
```

The mapping:
- `config.tabs[]` → a trigger + panel rendering a `<dl>` of the tab's OWN fields
- `config.relatedTabs[]` → a permission-gated trigger + panel mounting a
  generated child component whose body follows `displayMode`. A tab whose
  target `(app, module)` leaves the page's own surface carries a SECOND gate:
  `useModuleAvailability().hasModule('<app>', '<module>')` on both the trigger
  and the panel (and folded into the strip's visible-key list, so the active tab
  can never land on a hidden trigger) — the tab does not exist for a client that
  was never given the target module. Body per `displayMode`:
  `table` → `<ResponsiveDataTable serverMode>` filtered by the FK
  (`use{RelatedPlural}({ …paging, {relationFk}: relatedId })`), row-click →
  related detail route, « Créer » pre-filled via `create()?{relationFk}={id}`;
  `cards` → card grid + mini-pager; `summary` → count cartouche
  (`pageSize: 1` → `totalCount`) + « voir tout » link
- `config.fields[].type` → input component (`<TextInput>`, `<DateInput>`,
  `<EntityLookup>`, `<EnumSelect>`, …). Note: `<EntityLookup>` is **scaffolded
  into the client project** by `scaffold-ui-primitives` at `/ba-develop` Phase 3.0
  (it lives in `src/components/ui/EntityLookup.tsx`, not in `@atlashub/smartstack`).
  `scaffold-component` emits it for any field carrying `fkTo` — audit
  `DEV-UI-022` (BLOCKING) refuses raw `<input name="...Id">` for FK fields.
- `config.fields[].required` → form validation
- `config.fields[].computed` → derived state via the business rule engine

## SmartHome* → HomePage with KPI grid + nav cards

The three home types share the same visual recipe — KPI/widget grid at the
top, navigation cards below:

```tsx
export function CrmAppHomePage() {
  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">CRM</h1>

      <WidgetGrid
        widgets={[
          { key: 'open-deals', label: 'Open deals', type: 'kpi',
            entity: 'Opportunity', aggregation: 'count', field: 'status=open', col: 3 },
          { key: 'won-amount', label: 'Won YTD', type: 'kpi',
            entity: 'Opportunity', aggregation: 'sum', field: 'amount;status=won', col: 3 },
        ]}
      />

      <QuickLinkGrid
        links={[
          { key: 'to-prospects', label: 'Prospects', icon: 'users',    screenTarget: 'SCR-CRM-PROSPECTS-HOME-001' },
          { key: 'to-clients',   label: 'Clients',   icon: 'building', screenTarget: 'SCR-CRM-CLIENTS-HOME-001' },
        ]}
      />
    </div>
  );
}
```

`SmartModuleHome` and `SmartSectionHome` use the exact same structure with
their respective `quickLinks` pointing one level deeper in the navigation
hierarchy.

## Why the spec maps so closely to the shipping page

Because a `screen.md` entry names the same primitives the generated app uses
(columns, fields, widgets, quickLinks, related tabs), the cost of going from
"captured spec" to "shipping page" is almost zero — `scaffold-component` emits
the page, the developer hooks the real API via `useQuery`, and the page is done.

This is the explicit goal of the screens phase: the screen captured in
`screen.md` is the screen that ships, with one round of API wiring and one round
of optimization, not a throw-away wireframe.

## CSS variables

The generated SmartStack.app pages use this design-token naming convention:

| Token              | Use                                         |
|--------------------|---------------------------------------------|
| `--bg-card`        | card background                             |
| `--text-primary`   | primary text color                          |
| `--text-secondary` | secondary / muted text                      |
| `--border-color`   | borders                                     |
| `--color-accent-500` | accent / call-to-action color             |
| `--success-bg/text`  | success badges                            |
| `--error-bg/text`    | error badges                              |
| `--radius-card`    | card border radius                          |
| `--radius-button`  | button border radius                        |

When you describe a screen, you do not need to mention colors or styling — the
tokens are applied automatically by the generated SmartStack.app components.

## Loading and empty states

The generated SmartStack.app components always provide:
- A loading state (spinner) while data is being fetched.
- An empty state ("No data") when the entity has no rows.

You do not need to specify these in `screen.md` — they ship with every
generated page.

## Bottom line

When you propose a screen, you are writing the first draft of a React page.
Be precise about column types, action permissions and field constraints — the
generated page reflects them directly.
