---
name: frontend-component
description: >
  Generates React page components (list, detail, form) consuming
  @atlashub/smartstack npm package: PermissionGuard, Slot, SmartStackProvider.
  i18n keys are split per entity to prevent concurrent-generation overwrites.
phase: development/frontend
cli: cli/scaffold-component
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Frontend Component — Pages + Extension Points

Generates React page components following SmartStack feature structure with
`PermissionGuard`, `Slot`/`Fill` extension points, and i18n per-entity files.

All UI primitives are imported from `@atlashub/smartstack` (npm package) — not
from the SDK source tree.

## ⚠ Source of truth — customisation-ui baseline (2026-04-22)

Templates in `cli/scaffold-component/generate.ts` are derived **verbatim** from
the reference app under `D:\01 - projets\SmartStack.app\features\customisation-ui\web\smartstack-web\`.
Baseline pages read:

- `src/pages/platform/administration/uiConfiguration/ThemesListPage.tsx`
- `src/pages/platform/administration/uiConfiguration/PresetsListPage.tsx`
- `src/pages/platform/administration/tenants/OrganisationCreatePage.tsx`
- `src/pages/platform/administration/tenants/TenantDetailPage.tsx`
- `src/components/ui/PageTemplate.tsx`
- `src/components/ui/DataTable.tsx`
- `src/components/ui/EntityCard.tsx`
- `src/base.css`, `src/index.css`

The conformity rules the generator enforces are cross-checked by
`skills/development/frontend/ui-polish/tokens.json` (`hardRules` array).

**Legacy rule**: older versions of this SKILL.md described patterns like bare
`<div>` roots, raw `<table>`, Tailwind color utilities (`bg-red-500`), etc. —
those are obsolete. The current templates use `<PageTemplate>` + `<DataTable>` +
CSS variable tokens (`var(--color-accent-500)`) + `.input`/`.btn-primary` helper
classes. **Do not re-introduce the legacy shapes.**

## Layout (emitted)

Pages live under `src/pages/`, matching customisation-ui's source-of-truth
convention AND the registry import path emitted by scaffold-routes
(`@/pages/{appLower}/{module}/{section}/`). Hooks / services / types live
under `src/features/`, written by scaffold-api-client. Pages reach them
through the `@/` alias :

```
src/pages/{appLower}/{module}/{section}/
  {Entity}ListPage.tsx        — DataTable, route helpers, computed-field columns
  {Entity}DetailPage.tsx      — read-only display, computed values projected
  {Entity}FormPage.tsx        — input fields, computed fields excluded
  {Entity}DashboardPage.tsx   — KPI cards + alerts (when 'dashboard' in views[])

src/features/{module}/{entityLower}/
  hooks/use{Entity}.ts        — React Query (use{Entity}, useDashboard{Entity}, mutations)
  services/{entity}Service.ts — axios wrappers
  types/index.ts              — DTOs (List, Detail, Create, Update, Dashboard*)
```

Page imports use the alias :

```ts
import { useBudgets, useDeleteBudget } from '@/features/budgets/budget/hooks/useBudget';
import type { Budget } from '@/features/budgets/budget/types';
```

This split keeps page files trim while letting api-client own the data layer
and routes own the URL/registry contract.

## ⚠ BLOCKING — i18n rules

**Any generated component violating these rules MUST fail audit and be rewritten.**

### i18n.1 — No hardcoded visible strings

Every string the user sees flows through `t('key')`. That includes labels,
titles, placeholders, button text, column headers, tooltips, empty states,
error messages, and confirmations.

### i18n.2 — One file per entity, all 4 locales

Keys are written to **per-entity** JSON to avoid concurrent-generation overwrites
when multiple entities share a module:

```
web/{appcode}-web/src/i18n/fr/hrm/employee.json
web/{appcode}-web/src/i18n/en/hrm/employee.json
web/{appcode}-web/src/i18n/it/hrm/employee.json
web/{appcode}-web/src/i18n/de/hrm/employee.json
```

The client's `i18next` backend MUST load `{locale}/{module}/*.json` with each
file registered as its own namespace (the entity name). Example init:

```ts
import i18n from 'i18next';
import Backend from 'i18next-http-backend';

i18n.use(Backend).init({
  fallbackLng: 'en',
  ns: ['employee', 'contract', /* one per entity */],
  backend: { loadPath: '/i18n/{{lng}}/{{ns-path}}.json' },
});
```

### i18n.3 — Key namespace

Inside each entity JSON, keys are flat (no entity prefix inside the file).
Reference them via the namespace:

```tsx
const { t } = useTranslation('employee');
<h1>{t('list.title')}</h1>        // reads employee.json → list.title
<span>{t('form.fields.lastName')}</span>
```

### i18n.4 — Validation keys scoped to their field

Don't reuse a validation message from another field just because it reads
similarly. `lastNameRequired` is ONLY for the `lastName` input. If you need one
for `target`, create `targetRequired`.

## ⚠ BLOCKING — React patterns

### react.1 — `useParams<{ id: string }>()` always null-checked

The DynamicRouter convention uses `:id` for every detail/edit path. The hook
must be null-checked before use — no bang assertions:

```tsx
const { id } = useParams<{ id: string }>();
if (!id) return <Navigate to=".." replace />;
const { data } = useEmployee(id);
```

### react.2 — `useEffect` cleans up subscriptions

Any `window.*.onEvent`, `setInterval`, `setTimeout`, `addEventListener` MUST
have a cleanup return.

### react.3 — Typed props, no `any`

Props interfaces are mandatory. FormData state is typed from the spec fields,
not `Record<string, unknown>`.

### react.4 — Imports from `@atlashub/smartstack`

```tsx
import { PermissionGuard, Slot } from '@atlashub/smartstack';
```

Never from `@/components/auth/PermissionGuard` or any local path — those are
SDK-managed.

## FK fields → `<EntityLookup>` (not `<input>`)

When the orchestrator (`/ba-develop` Phase 3a) populates `fkTo` on a field
(`type: guid` + name ending in `Id`), `scaffold-component` emits
`<EntityLookup apiEndpoint="/api/{module}/{plural-kebab}/lookup" />` instead
of the legacy `<input type="text">`. The combobox component lives in the
client project at `src/components/ui/EntityLookup.tsx` — scaffolded by
`scaffold-ui-primitives` at Phase 3.0 — and consumes the `use{Target}Lookup`
React Query hook scaffolded by `scaffold-api-client` (paginated, 60s
`staleTime`).

Resolution rule (orchestrator side): for each field whose `type` ∈ `{guid, uuid}`
and whose name ends with `Id`, look up the matching
`Relations: ... *→1 {Target} (FK {ThisFieldName} ...)` line in the entity's
`entité.md`. If found, set `fkTo: { entity: '{Target}', module: '{ownerModule}' }`.
Same-module FKs reuse the current module; cross-module FKs read the owner
module from the attribute description (`FK cross-module vers {APP}/{MODULE}.{Target}`).
When unresolved, the field falls back to `<input>` and the orchestrator
emits a warning — audit `DEV-UI-022` then surfaces it as **BLOCKING** on
the next pass.

The `<EntityLookup>` import + the per-target `use{Target}Lookup` import are
added to the FormPage automatically (deduplicated when multiple FKs target
the same entity).

## Permission keys

Pattern: `{module}.{section}.{action}` — NO appCode prefix. Examples:
- `hrm.employees.read` (guards list + detail)
- `hrm.employees.create`
- `hrm.employees.update`
- `hrm.employees.delete`

## Slot/Fill pattern

Every page exposes named slots so clients/extensions can inject content:

```tsx
<Slot name="hrm.employee.list.before" />
<Slot name="hrm.employee.detail.after" context={{ data }} />
<Slot name="hrm.employee.form.fields.after" context={{ formData, onChange }} />
```

The slot catalogue is mirrored by `frontend/extension-config/scaffold-extension-config`.

## After generation — MANDATORY

Once the CLI has finished writing the files, invoke the audit skill on the
generated paths:

```
@.claude/skills/development/audit/SKILL.md
Audit the components I just generated in src/features/{module}/{entity}/
and the locale files I added under i18n/{locale}/{module}/{entity}.json.
```

The audit MUST be green before declaring the generation "done".

## Invocation

```bash
npx tsx skills/development/frontend/component/cli/scaffold-component/index.ts \
  --spec '{"module":"hrm","entity":"Employee","section":"employees","views":["list","detail","form"],"fields":[{"name":"firstName","type":"string","required":true}],"projectPath":"/path"}'
```

## Per-page validation gate — `validate-page` (sibling CLI)

The orchestrator in `ba-develop` Phase 3a runs `validate-page`
after every `scaffold-component` invocation as a deterministic gate (one page
in, pass/fail out). The CLI is read-only — it never writes files — and
returns an envelope listing every violation with line numbers + suggested
fixes that the retry path forwards back into the spec under `priorErrors[]`.

```bash
npx tsx skills/development/frontend/component/cli/validate-page/index.ts \
  --project-path "/path/to/web-root" \
  --page-file "src/pages/myapp/hrm/employees/EmployeeListPage.tsx"
```

Eight blocking rules — each violates produce `severity: 'err'` and the CLI
exits 1:

| Rule id | Detects |
|---|---|
| `imports-resolve` | Any `@/…` or relative import that does not resolve to a file on disk. |
| `pagetemplate-wrapping` | Page must import `PageTemplate` from `@/components/ui/PageTemplate` AND render at least one `<PageTemplate …>`. |
| `useparams-null-check` | `useParams<…>()` without an `if (!id) return …` guard within 8 lines. |
| `permissionguard-on-mutations` | Any `mutateAsync` / `mutation.mutate` / `useDelete` / `useUpdate` / `useCreate` reference not enclosed in `<PermissionGuard permission="…">` within 30 lines upward. |
| `no-local-usepermissions` | Any local `function usePermissions(` or `const usePermissions =` definition (the subagent improvisation that bypasses the platform). |
| `i18n-keys-resolve` | `t('key')` whose key is missing in any of the four `src/i18n/{fr,en,it,de}/{module}/{namespace}.json` files. |
| `hook-imports-exist` | Named imports from `@/features/…` whose target file does not export the symbol. |
| `permission-keys-no-appcode` | `permission="X.Y.Z"` literals that don't match `^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)?\.[a-z][a-z0-9-]*$` (lowercase, 3 or 4 segments — section or resource level). |

## Key rules (reminder)

1. **Feature isolation**: each entity in its own folder.
2. **API via `@atlashub/smartstack` client**: never hardcode `/api/...` paths in JSX — hooks abstract this.
3. **React Query**: all fetching via hooks (no `useEffect + fetch`).
4. **PermissionGuard**: protect mutating actions (create, delete, update).
5. **Slot/Fill**: define extension points for customization.
6. **`useParams<{ id: string }>()` strict**: no custom param names (`:userId`, `:ticketId`).
7. **Computed fields are READ-ONLY** : a `fields[]` entry with a non-empty
   `formula` (mirrored from the BA data model) is **excluded from the form**
   — no `<input>`, no entry in `${E}FormData`, no entry in the initial state.
   The List and Detail templates already render the value directly from the
   DTO (`item.<name>` / `data.<name>`) since `scaffold-business` projected
   the formula into the LINQ query — **no stub helper, no `(b) => 0`**. Audit
   DEV-UI-014 rejects any list page that introduces a stub helper.
8. **Dashboard view (optional)** : adding `'dashboard'` to `views[]` emits
   `${E}DashboardPage.tsx` calling the typed `useDashboard${E}` hook
   (produced by `scaffold-api-client` when `entities[].hasDashboard` is
   true). The page renders KPI cards from `consolidated.metrics` (key →
   `t('dashboard.kpi.<key>')`) plus an alerts list with severity badges.
   Filter inputs (`startDate`, `endDate`) drive the hook's query params.
   Slot points : `${entity}.dashboard.{header,kpis.before,kpis.after,footer}`.
   Never fetch raw axios in a Dashboard page — always go through the hook.

## Anatomy cible (baseline customisation-ui)

Every generated page MUST follow this shape. The `ui-polish` skill audits that
the output matches.

### ListPage
```tsx
import { PageTemplate } from '@/components/ui/PageTemplate'
import { DataTable } from '@/components/ui/DataTable'
import { Loader2, Plus, Pencil, Trash2 } from 'lucide-react'
import { PermissionGuard, Slot } from '@atlashub/smartstack'

<PermissionGuard permission="{module}.{section}.read">
  <PageTemplate
    title={t('list.title')}
    subtitle={t('list.subtitle')}
    icon={<List />}
    actions={
      <PermissionGuard permission="{module}.{section}.create">
        <button className="... bg-[var(--color-accent-500)] text-white hover:bg-[var(--color-accent-600)]">
          <Plus /> {t('list.create')}
        </button>
      </PermissionGuard>
    }
  >
    <Slot name="{entity}.header.actions" />
    <DataTable<Entity> ... />
  </PageTemplate>
</PermissionGuard>
```

### DetailPage
```tsx
<PageTemplate title={displayTitle} icon={<FileText />} actions={editButton}>
  <Slot name="{entity}.detail.header" context={{ data }} />
  <div className="p-6 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]">
    <dl className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
      {/* dt/dd rows with text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] */}
    </dl>
  </div>
  <Slot name="{entity}.detail.sidebar" context={{ data }} />
</PageTemplate>
```

### FormPage
```tsx
<PageTemplate title={isEdit ? t('form.editTitle') : t('form.createTitle')}>
  {error && <div className="... bg-[var(--error-bg)] border-[var(--error-border)] text-[var(--error-text)]">...</div>}
  <form onSubmit={handleSubmit} className="space-y-6">
    <div className="p-6 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] space-y-4">
      <Slot name="{entity}.form.fields.before" context={{ formData, onChange }} />
      {/* inputs with className="input w-full disabled:opacity-50" */}
      <Slot name="{entity}.form.fields.after" context={{ formData, onChange }} />
    </div>
    <div className="flex justify-end gap-2">
      <button type="button" className="... bg-[var(--bg-secondary)] hover:bg-[var(--bg-hover)]">Cancel</button>
      <button type="submit" className="... bg-[var(--color-accent-500)] text-white hover:bg-[var(--color-accent-600)]">Submit</button>
    </div>
  </form>
</PageTemplate>
```

### Forbidden patterns (caught by ui-polish audit)

- Bare `<div>`/`<main>` at page root (must be `<PageTemplate>`)
- Raw Tailwind color utilities: `bg-red-500`, `text-green-600`, `border-blue-200` — use `var(--status-{bg,text,border})`
- Hex in `className`: `bg-[#6366f1]` — use `bg-[var(--color-accent-500)]`
- Custom spinners — use `<Loader2 className="animate-spin" />`
- `useParams<{id: string}>()` without null-check
- Permission keys with appCode prefix: `myapp.hrm.employees.read` — use `hrm.employees.read`
- Icon imports from `@heroicons` or `react-icons` — lucide-react only
