---
name: step-03d-layer3-frontend
description: "Layer 3: Frontend pages, i18n, routes, documentation, and tests"
model: opus
parent_step: steps/step-03-execute.md
---

**ULTRA THINK about frontend completeness: new pages, routes, i18n keys, AND permissions/navigation alignment with Layer 1 seed data.**

# Layer 3 — Frontend (Pages + I18n + Documentation)

### Companion Specs Loading (delegate mode)

```javascript
if (delegate_mode && specification_loading_plan) {
  const specsDir = path.dirname(delegate_prd_path);
  // Layer 3 loads: screens + usecases companions
  for (const file of specification_loading_plan.layer3_frontend) {
    const specPath = path.join(specsDir, file);
    if (fileExists(specPath)) {
      const specData = readJSON(specPath);
      if (specData.screens) {
        // Full screen definitions: columns[], fields[], filters[], actions[], kpis[]
        // Use for: DataTable columns, form fields, filter configs, action buttons, dashboard KPIs
        console.log(`Loaded screens: ${specData.screens.length} screen specs with columns/fields/filters`);
      }
      if (specData.useCases || specData.usecases) {
        const ucs = specData.useCases || specData.usecases;
        // UC steps inform form validation, navigation flows, error states
        console.log(`Loaded usecases: ${ucs.length} use cases for form flows`);
      }
    }
  }
}
```

> When companion specs are loaded:
> - **List pages**: use `screen.columns[]` for exact DataTable column definitions (key, label, type, sortable)
> - **Form pages**: use `screen.fields[]` for exact form field definitions (name, type, required, validation)
> - **Filter configs**: use `screen.filters[]` for search/filter UI generation
> - **Dashboard pages**: use `screen.kpis[]` for KPI card and chart definitions
> - **Actions**: use `screen.actions[]` for action buttons (create, export, bulk operations)

### ⛔ HARD RULE — /ui-components is NON-NEGOTIABLE (read BEFORE any Layer 3 action)

> **VIOLATION CHECK:** If ANY .tsx page file was created by Write tool WITHOUT
> a prior Skill("ui-components") call in this execution, the frontend layer is INVALID.
>
> **You MUST NOT:**
> - Generate .tsx page code in Agent prompts and dispatch to Snipper agents
> - Write page content directly via the Write tool
> - Copy-paste page templates from your knowledge
>
> **You MUST:**
> - Call Skill("ui-components") which loads the style guide, responsive guidelines,
>   accessibility rules, and pattern files (entity-card, data-table, dashboard-chart, grid-layout, kanban)
> - Let /ui-components generate all pages with correct conventions
>
> **Why this matters:** Without the skill, pages miss CSS variables, DataTable/EntityCard,
> memo()/useCallback, responsive mobile-first design, and accessibility patterns.
>
> **Agent boundary rule:** Snipper sub-agents DO NOT have access to the Skill tool.
> Therefore, .tsx page generation MUST NEVER be delegated to Snipper agents.
> Pages are ALWAYS generated by the principal agent via Skill("ui-components").
> Snipper agents handle: API clients, routes, wiring, i18n, tests — NOT pages.

### Load Frontend References (deferred from top of step)

- Read `references/smartstack-frontend.md` now — lazy loading, i18n, page structure, CSS variables, EntityLookup (sections 1-6)
- Read `references/smartstack-frontend-compliance.md` now — documentation, form testing, compliance gates (sections 7-9)

### Pre-flight: Shared component existence check

Before generating any page, verify shared components exist:

```
Glob("src/components/ui/DataTable.*")
Glob("src/components/ui/EntityCard.*")
Glob("src/components/ui/PageLoader.*")
Glob("src/components/ui/EntityLookup.*")
```

If ANY shared component is MISSING:
  → Log warning: "Shared component {Component} not found — will be generated locally"
  → When invoking Skill("ui-components"), add instruction:
    "MISSING SHARED COMPONENTS: {list}. Generate these locally in src/components/ui/"

**EntityLookup special handling (FK fields):**
If ANY entity has FK Guid fields (e.g., EmployeeId, DepartmentId) AND EntityLookup is missing:
  → Include EntityLookup generation in the Skill("ui-components") invocation
  → Add to the skill prompt: "MISSING SHARED COMPONENT: EntityLookup. Generate EntityLookup in src/components/ui/EntityLookup.tsx"
  → The /ui-components skill has the EntityLookup pattern in smartstack-frontend.md section 6
  → Critical requirement for the skill: response parsing MUST use `(res.data.items || res.data).map(mapOption)`
  → Verify after generation: the component MUST contain `(res.data.items || res.data).map(mapOption)` — if not, fix it
  → Do NOT generate EntityLookup manually via Write tool — always via Skill("ui-components")

### Task Progress
TaskUpdate(taskId: layer3_task_id, status: "in_progress")
TaskUpdate(taskId: progress_tracker_id,
  description: "Module: {module_code}. Current: step-03 (Execute), Layer 3",
  activeForm: "Executing Layer 3")

> **Frontend patterns** are in `references/smartstack-frontend.md` (already loaded above):
> - API client generation (MCP scaffold_api_client)
> - Route scaffolding (MCP scaffold_routes) — section 1 + 3b
> - Page types (ListPage, DetailPage, CreatePage, EditPage) — section 3
> - FK field handling (EntityLookup component) — section 6
> - Form structure (no modals — all full pages) — section 3b
> - Detail page tabs (local state only) — section 3 "Tab Behavior Rules"
> - Testing patterns (co-located form tests) — section 8
> - Section-level routes and permissions — section 3b
> - I18n JSON structure and registration — section 2
> - Compliance gates (5 mandatory checks) — section 9

### Frontend Tasks (sequential or parallel within layer)

For each module:
- API client: MCP scaffold_api_client
- Routes — TWO mandatory steps:
  1. Generate registry: MCP scaffold_routes (source: 'controllers', outputFormat: 'componentRegistry', dryRun: false)
     → Creates/updates componentRegistry.generated.ts + navRoutes.generated.ts
  2. Verify PageRegistry registration (see below)
- Pages per section: When a section exists (e.g., "list"), generate ALL 4 page types:
  → {Section}ListPage.tsx (index route)
  → {Section}DetailPage.tsx (/:id route) — click on list item navigates here
  → Create{Section}Page.tsx (/create route)
  → Edit{Section}Page.tsx (/:id/edit route)
  "detail" is NEVER a separate section — it's the /:id route of the list section.
- Pages: **MANDATORY — INVOKE Skill("ui-components")** for ALL page generation.
  ⚠ BLOCKING REQUIREMENT: You MUST call Skill("ui-components") for EVERY page (.tsx).
  NEVER generate .tsx page code directly. NEVER write page content in Agent prompts.
  NEVER use Write tool to create pages without first calling Skill("ui-components").
  The skill loads the full style guide + patterns (entity-card, data-table, dashboard-chart, grid-layout).
  Follow smartstack-frontend.md patterns:
  → React.lazy() for all page imports (named export wrapping)
  → `<Suspense fallback={<PageLoader />}>` around all lazy components
  → Page structure: hooks → useEffect(load) → loading → error → content
  → CSS variables only: bg-[var(--bg-card)], text-[var(--text-primary)]
  → DataTable/EntityCard (not raw HTML `<table>`)
  → If entity has FK Guid fields: generate EntityLookup component in @/components/ui/EntityLookup (see smartstack-frontend.md section 6)
  → Dashboard pages: generate StatCard + ChartCard locally (see ui-components patterns/dashboard-chart.md)
- Form pages: Create/Edit forms are full pages with own routes:
  → EntityCreatePage.tsx with route /{module}/create
  → EntityEditPage.tsx with route /{module}/:id/edit
  → Zero modals/popups/drawers/dialogs for forms
  → Back button with navigate(-1) on every form page
  → See smartstack-frontend.md section 3b for templates
- Tabs on detail pages: Tabs must switch content locally:
  → Tab click handler: setActiveTab(tabKey) only — do not navigate() to another page
  → Tab content: render inline with {activeTab === 'tabKey' && <TabContent />}
  → Sub-resource data: fetch via API filtered by parent ID, display in tab panel
  → Do not use navigate('../leaves?employee=${id}') in tab handler
  → See smartstack-frontend.md section 3 'Tab Behavior Rules'
- FK fields: Foreign key Guid fields (e.g., EmployeeId) must use EntityLookup:
  → Do not render FK fields as `<input>` — users cannot type GUIDs
  → Do not render FK fields as `<select>` — even with API-loaded options, `<select>` is not acceptable
  → Only use `<EntityLookup />` from @/components/ui/EntityLookup for searchable selection
  → Each FK field needs: apiEndpoint, mapOption (display name), search support on backend
  → Do not use `<select value={formData.departmentId}>`, `<option value={dept.id}>`, `e.target.value` for FK fields
  → See smartstack-frontend.md section 6 for the full EntityLookup pattern
<!-- TODO A12: Replace direct i18n generation with MCP scaffold_i18n when B4 is ready -->
- I18n: Generate 4 JSON files per module namespace (fr, en, it, de)
  → Follow JSON template from smartstack-frontend.md
  → Keys: actions, labels, errors, validation, columns, form, messages, empty
  → All t() calls must use namespace prefix + fallback: t('ns:key', 'Default text')
  → **Register namespace in i18n config** (critical — POST-CHECK C39 catches this, but fix it now):
    1. Find i18n config: `Glob("src/**/i18n/config.ts")` or `index.ts` or `i18n.ts`
    2. Read the config → find the resources/ns registration pattern
    3. Add the new namespace import + registration for all 4 languages
    4. If config uses dynamic imports: add namespace to the `ns` array
    5. Verify: `grep -q "{module_namespace}" src/**/i18n/config.ts` → must match
    6. **I18n File/Import Alignment Check:** Verify that every locale JSON file on disk for this module
       has a corresponding import in the i18n config index.ts. Glob `src/**/i18n/locales/*/` for module files,
       then verify each is imported. Missing imports cause silent translation failures (keys show as raw strings).
- Permissions: Call MCP generate_permissions for the module permission root (2 segments: {app}.{module}),
  then also call MCP generate_permissions for each section (3 segments: {app}.{module}.{section}).
- Section routes: Ensure navigation seed data has ComponentKey matching PageRegistry keys.
  ComponentKey convention: `{app_code}.{module_code}.{section_code}` (dot-separated, lowercase).
  DynamicRouter generates the URL paths from seed data Route field.
- MUST use src/pages/{App}/{Module}/ hierarchy (NOT flat)

### Documentation (after frontend pages exist)

> **After frontend pages are created, generate module documentation via `/documentation` skill.**

```
Invoke /documentation {module_code} --type user

This generates:
- src/pages/docs/business/{app}/{module}/doc-data.ts (data file)
- src/pages/docs/business/{app}/{module}/index.tsx (page wrapper)
- src/i18n/locales/fr/docs-{app}-{module}.json (French doc translations)
- Updates App.tsx routing for doc page
- Updates docs-manifest.json
```

**Verify DocToggleButton presence in list/detail pages:**
- Every `*ListPage.tsx` and `*DetailPage.tsx` MUST import and render `DocToggleButton` in their header
- Import: `import { DocToggleButton } from '@/components/docs/DocToggleButton';`
- Placement: inside the header actions area (see `smartstack-frontend.md` section 7)

### If NOT economy_mode AND multiple entities: Parallel Agents (within layer)

> **Protocol:** See `references/parallel-execution.md`

```
IF NOT economy_mode AND entities.length > 1:
  # PHASE A — Parallel: infrastructure frontend (NO page generation)
  # Snipper agents DO NOT have access to the Skill tool, so they CANNOT call /ui-components.
  # Pages MUST be generated by the principal agent in Phase B.
  For each entity, launch in parallel (single message):
    Agent(subagent_type='Snipper', model='opus',
      prompt='Execute Layer 3 INFRASTRUCTURE for {EntityName}:
        **MANDATORY: Read references/smartstack-frontend.md FIRST**
        - API client: MCP scaffold_api_client
        - Routes: MCP scaffold_routes (outputFormat: "componentRegistry", dryRun: false)
          → MUST generate componentRegistry.generated.ts + navRoutes.generated.ts
        - Verify: componentRegistry.generated.ts has PageRegistry.register() for new pages
        - Verify: main.tsx imports componentRegistry.generated (one-time check)
        - NO manual App.tsx wiring needed — DynamicRouter handles routing
        - I18n: 4 JSON files (fr, en, it, de) + REGISTER namespace in i18n config
        - DO NOT generate any .tsx page files — pages are handled by the principal agent
        - Your task ID is {task_id}. Call TaskUpdate(status: "in_progress") before starting.
        - Call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done.')
  # Wait for all agents to complete

  # PHASE B — Sequential: pages via /ui-components (principal agent)
  # Snipper agents cannot call Skill() — only the principal agent can.
  For each entity (sequentially):
    **INVOKE Skill("ui-components")** — pass entity context:
      - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
      - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
      - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
      - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
      - "FK FIELDS: EntityLookup for ALL FK Guid fields"
      - "I18n: ALL text uses t('namespace:key', 'Fallback')"
    Generate form tests: co-located .test.tsx for Create and Edit pages

ELSE:
  # Economy mode: Agent principal handles all entities SEQUENTIALLY.
  # MANDATORY: You MUST still call Skill("ui-components") for page generation.
  # economy_mode only disables parallel agents — it does NOT skip /ui-components.
  For each entity (sequentially):
    1. MCP scaffold_api_client
    2. MCP scaffold_routes (outputFormat: 'componentRegistry')
    3. Verify PageRegistry registration (componentRegistry.generated.ts)
       No manual App.tsx wiring needed — DynamicRouter resolves at runtime
    4. **INVOKE Skill("ui-components")** — pass entity context:
       - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
       - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
       - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
       - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
       - "I18n: ALL text uses t('namespace:key', 'Fallback')"
    5. I18n: 4 JSON files (fr, en, it, de) + register namespace in i18n config
    6. Form tests: co-located .test.tsx for Create and Edit pages
```

### Parallel Agents + TaskCreate Integration

When launching agents for multi-entity layers:
- Each agent receives its task ID and metadata context in the prompt
- Agent must call TaskUpdate(status: "in_progress") before starting
- Agent must call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done
- Agent principal monitors via TaskList() after all agents complete
- Agent principal updates activeForm after each entity completion:
  `TaskUpdate(taskId: layer3_task_id, activeForm: "Building {EntityName} frontend (2/5 entities)")`

### Frontend Compliance Gate

> See `references/smartstack-frontend-compliance.md` section 9 "Compliance Gates" for all 6 required checks:
> 1. CSS Variables (theme system)
> 2. Forms as Pages (zero modals/drawers/slide-overs)
> 3. I18n File Structure (4 languages, separate JSON files)
> 4. Lazy Loading (React.lazy() — no static imports)
> 5. useTranslation in Pages (all text translated)
> 6. SmartStack Components (no raw HTML tables — DataTable/EntityCard required)

Do not commit frontend changes until all 6 gates pass.

When delegating to `/ui-components` skill, include explicit instructions:
- "CSS: Use CSS variables ONLY — `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`."
- "Forms: Create/Edit forms are FULL PAGES with own routes (e.g., `/create`, `/:id/edit`)."
- "I18n: ALL text must use `t('namespace:key', 'Fallback')`. Generate JSON files in `src/i18n/locales/`."

### Frontend Tests Inline

> **Tests are scaffolded and run WITHIN Layer 3, not deferred to step-07.**

```
1. Scaffold frontend tests:
   → For each module with create/edit pages:
     → Generate EntityCreatePage.test.tsx and EntityEditPage.test.tsx
     → Co-located next to the page component (NOT in __tests__/ folder)
     → Cover: rendering, validation, submit, pre-fill, navigation, errors
     → Tools: Vitest + React Testing Library + @testing-library/user-event
     → Mock API: vi.mock() or MSW — NEVER real API calls

2. Run tests:
   WEB_DIR=$(find . -name "vitest.config.ts" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
   if [ -n "$WEB_DIR" ]; then
     (cd "$WEB_DIR" && npm run test)
   fi

3. Fix loop (max 3 iterations):
   IF tests fail:
     → Identify root cause (ALWAYS code bug, not test bug)
     → Fix production CODE
     → Re-run tests
     → Repeat until pass or max 3

4. If still failing after 3 iterations: note failures, continue.
   Step-07 "Final Test Sweep" will handle remaining failures.
```

### Typecheck Gate

```bash
npm run typecheck
```

Must pass before commit.

TaskUpdate(taskId: layer3_task_id, status: "completed",
  metadata: { build_gate: "pass" })

### Layer 3 Commits

```
feat({module}): [frontend] pages, routes, i18n, documentation
test({module}): frontend form tests
```

---

## NEXT SUB-STEP (MANDATORY — IMMEDIATE)

> **DO NOT STOP.** DO NOT ask the user. DO NOT present options. IMMEDIATELY load the next sub-step.
> Stopping here is a VIOLATION of the execution guarantee.

Load `steps/step-03e-layer4-devdata.md`
