---
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` — tabbed layout with optional `<RelatedTab>` panels |
| `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": "department", "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 tabbed shell

For SmartForm, the inner `tabs[]` and `relatedTabs[]` collapse into a single
`<Tabs>` shell — field tabs first, related-collection tabs after:

```tsx
export function ClientDetailPage() {
  const { id } = useParams();
  const { data } = useClient(id);
  const navigate = useNavigate();

  return (
    <Tabs defaultValue="info">
      <TabsList>
        <TabsTrigger value="info">Information</TabsTrigger>
        <TabsTrigger value="billing">Adresse de facturation</TabsTrigger>
        <TabsTrigger value="invoices">Factures</TabsTrigger>
        <TabsTrigger value="contacts">Contacts</TabsTrigger>
      </TabsList>

      <TabsContent value="info">
        <FormFields fields={[/* field tab spec */]} value={data} readonly />
      </TabsContent>
      <TabsContent value="billing">
        <FormFields fields={[/* billing tab spec */]} value={data} readonly />
      </TabsContent>

      <TabsContent value="invoices">
        <RelatedTab
          relatedEntity="Invoice"
          relationship="clientId"
          parentId={id}
          screenTarget="SCR-CRM-INVOICES-LIST-001"
          onClickRow={(row) => navigate(`/invoices/${row.id}`)}
        />
      </TabsContent>
      <TabsContent value="contacts">
        <RelatedTab
          relatedEntity="Contact"
          relationship="clientId"
          parentId={id}
          screenTarget="SCR-CRM-CONTACTS-LIST-001"
        />
      </TabsContent>
    </Tabs>
  );
}
```

The mapping:
- `config.tabs[]` → `<TabsTrigger>` + `<TabsContent>` pair (field-tab content)
- `config.relatedTabs[]` → `<TabsTrigger>` + `<TabsContent>` with embedded
  `<RelatedTab>` that pre-filters the related screen by the parent ID through
  the declared FK
- `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.
