# SmartStack Frontend Patterns — Mandatory Reference

> **Loaded by:** step-03d (Layer 3, deferred) and step-04 (validation)
> **Purpose:** Mandatory frontend conventions. `/ui-components` generates pages — this file provides guard rails.
> **Enforcement:** POST-CHECKs in step-04 verify compliance.

---

## 1. Lazy Loading (React.lazy + Suspense)

ALL page components MUST be lazy-loaded. Only critical entry pages (HomePage, LoginPage) may use static imports.

```tsx
// Named exports — use .then() to wrap
// Path: @/pages/{App}/{Module}/{Section}/{Page}
const EmployeesPage = lazy(() =>
  import('@/pages/HumanResources/EmployeeManagement/Employees/EmployeesPage')
    .then(m => ({ default: m.EmployeesPage }))
);

// Suspense wrapper with PermissionGuard
element: (
  <Suspense fallback={<PageLoader />}>
    <PermissionGuard permissions={ROUTES['hr.employees'].permissions}>
      <EmployeesPage />
    </PermissionGuard>
  </Suspense>
)
```

**Rules:**
- Do not static-import page components in route files
- Use `<Suspense fallback={<PageLoader />}>` around lazy components
- Use `.then(m => ({ default: m.ComponentName }))` for named exports
- Unified AppLayout is also lazy-loaded
- Client `App.tsx` MUST always use lazy imports for business pages (static imports kill code splitting)

---

## 2. I18n / Translations (react-i18next)

ALL user-facing text MUST use translations. 4 languages required: fr, en, it, de.

### File Structure

```
src/i18n/locales/{lang}/{module}.json  # Per-module translation file (4 languages)
```

### Module JSON Template

```json
{
  "title": "Module display name",
  "description": "Module description",
  "actions": {
    "create": "Create {entity}",
    "edit": "Edit {entity}",
    "delete": "Delete {entity}",
    "save": "Save",
    "cancel": "Cancel",
    "search": "Search...",
    "export": "Export",
    "refresh": "Refresh"
  },
  "labels": {
    "name": "Name",
    "code": "Code",
    "description": "Description",
    "status": "Status",
    "createdAt": "Created at",
    "updatedAt": "Updated at",
    "createdBy": "Created by",
    "isActive": "Active"
  },
  "columns": {
    "name": "Name",
    "code": "Code",
    "status": "Status",
    "actions": "Actions"
  },
  "form": {
    "name": "Name",
    "namePlaceholder": "Enter name...",
    "code": "Code",
    "codePlaceholder": "Enter code...",
    "description": "Description",
    "descriptionPlaceholder": "Enter description..."
  },
  "errors": {
    "loadFailed": "Failed to load data",
    "saveFailed": "Failed to save",
    "deleteFailed": "Failed to delete",
    "notFound": "Not found",
    "permissionDenied": "Permission denied"
  },
  "validation": {
    "nameRequired": "Name is required",
    "codeRequired": "Code is required",
    "nameMaxLength": "Name must be less than {{max}} characters"
  },
  "messages": {
    "created": "{entity} created successfully",
    "updated": "{entity} updated successfully",
    "deleted": "{entity} deleted successfully",
    "confirmDelete": "Are you sure you want to delete this {entity}?"
  },
  "empty": {
    "title": "No {entity} found",
    "description": "Create your first {entity} to get started"
  }
}
```

### Usage

```tsx
const { t } = useTranslation(['employees']);
t('employees:title', 'Employees')                                             // Simple key + fallback
t('employees:messages.created', '{{entity}} created', { entity: 'Employee' }) // Interpolation
t('common:actions.save', 'Save')                                              // Cross-namespace
```

### Namespace Registration (CRITICAL — POST-CHECK C39)

After creating i18n JSON files, register each namespace in `src/i18n/config.ts`:

```typescript
import employees from './locales/fr/employees.json';
// In resources: fr: { employees, common, navigation, ... }
// OR in ns array: ns: ['common', 'navigation', 'employees'],
```

Root cause (test-apex-007): JSON files existed but namespaces were not registered → `useTranslation(['module'])` returned empty strings.

**Rules:**
- Provide fallback value as 2nd argument to `t()`
- Use namespace prefix: `t('namespace:key')` — never `t('key')` without namespace
- Generate 4 language files (fr, en, it, de) with identical key structures
- Register new namespaces in i18n config file after creating JSON files
- Do not hardcode user-facing strings in JSX

---

## 3. Page Structure

> `/ui-components` generates complete pages. These rules ensure compliance.

### Standard List Page Structure

1. **HOOKS** — `useTranslation`, `useNavigate` at the top
2. **STATE** — `loading`, `error`, `data` via `useState`
3. **DATA LOADING** — `useCallback` + `useEffect` pattern
4. **LOADING STATE** — `Loader2` spinner centered (`min-h-[400px]`)
5. **ERROR STATE** — error message + retry button
6. **CONTENT** — header (title + `DocToggleButton` + create button) + `DataTable` or empty state

**API response guard (MANDATORY):** Always use `result?.items ?? []` when extracting arrays.
SmartStack's axios interceptor may resolve with `undefined` on auth failures (401/403) instead of rejecting. Without the guard, `data.length` crashes.

### Detail Page Rules

- Edit button: `navigate('edit')` → separate edit page, not modal
- Tabs: local `useState<TabKey>('info')` — do NOT `navigate()` on tab click
- Tab content: inline conditional `{activeTab === 'tab' && <Content />}`
- Lazy tab loading: `visitedTabsRef` tracks visited tabs to avoid redundant API calls
- Sub-resource tabs load data filtered by parent entity ID inline
- POST-CHECK C37 enforces: tabs switch content in-place, not redirect to other pages

---

## 3b. Form Pages (Create / Edit)

All forms MUST be full pages with their own URL route. Do NOT use modals, dialogs, drawers, or popups.

### Route Convention

Route paths MUST use **kebab-case** matching navigation seed data (which uses `ToKebabCase()`).

| Action | Route | Page component |
|--------|-------|---------------|
| Create | `/{module}/create` | `EntityCreatePage` |
| Edit | `/{module}/:id/edit` | `EntityEditPage` |

> **v3.7+ (DynamicRouter):** Routes are resolved automatically from navigation seed data.
> `ComponentKey` in seed data matches `PageRegistry.register('key', ...)` entries.
> No manual route registration needed.

### Route Registration

> **Note (v3.7+):** With DynamicRouter + PageRegistry, route registration is automatic.
> Run `scaffold_routes outputFormat="componentRegistry"` to generate `componentRegistry.generated.ts`,
> then ensure `main.tsx` imports it. The patterns below apply only to legacy projects.

```tsx
// Pattern B (JSX nested children):
{ path: '{module-kebab}', children: [
    { index: true, element: <Suspense fallback={<PageLoader />}><ListPage /></Suspense> },
    { path: 'create', element: <Suspense fallback={<PageLoader />}><CreatePage /></Suspense> },
    { path: ':id', element: <Suspense fallback={<PageLoader />}><DetailPage /></Suspense> },
    { path: ':id/edit', element: <Suspense fallback={<PageLoader />}><EditPage /></Suspense> },
]}

// Pattern A (applicationRoutes — MUST include module segment in path):
const applicationRoutes: ApplicationRouteExtensions = {
  'human-resources': [
    { path: 'employee-management/employees', element: <ListPage /> },           // includes module
    { path: 'employee-management/employees/create', element: <CreatePage /> },   // includes module
    { path: 'employee-management/employees/:id', element: <DetailPage /> },      // includes module
    { path: 'employee-management/employees/:id/edit', element: <EditPage /> },   // includes module
    // WRONG: { path: 'employees', ... } — missing module segment → 404
  ],
};
```

**Section-level routes:** `list` and `detail` do NOT add path segments (handled by `index: true` and `:id`). Only other sections (dashboard, approve, import) add `{section-kebab}` child routes.

**PermissionGuard for sections:**
```tsx
element: (
  <Suspense fallback={<PageLoader />}>
    <PermissionGuard permissions={ROUTES['app.module.section'].permissions}>
      <SectionPage />
    </PermissionGuard>
  </Suspense>
)
```

**Rules:**
- Do NOT use `<Modal>`, `<Dialog>`, `<Drawer>` for create/edit forms
- Do NOT use `useState(isOpen)` to toggle form visibility — forms are pages
- Navigate: `navigate('create')` or `navigate(\`${id}/edit\`)`
- Back button: `navigate(-1)`
- Create dedicated `EntityCreatePage.tsx` and `EntityEditPage.tsx` page components
- Form submitting state: `setSubmitting(true)` → API call → `navigate(-1)` on success

---

## 4. CSS Variables (Theme System)

Do not use hardcoded Tailwind colors. Always use CSS variables for theme support.

| Usage | CSS Variable | Tailwind usage |
|-------|-------------|---------------|
| Background | `var(--bg-primary)` | `bg-[var(--bg-primary)]` |
| Card background | `var(--bg-card)` | `bg-[var(--bg-card)]` |
| Text primary | `var(--text-primary)` | `text-[var(--text-primary)]` |
| Text secondary | `var(--text-secondary)` | `text-[var(--text-secondary)]` |
| Borders | `var(--border-color)` | `border-[var(--border-color)]` |
| Accent | `var(--color-accent-500)` | `text-[var(--color-accent-500)]` |
| Card radius | `var(--radius-card)` | `style={{ borderRadius: 'var(--radius-card)' }}` |
| Input radius | `var(--radius-input)` | `rounded-[var(--radius-input)]` |
| Button radius | `var(--radius-button)` | `rounded-[var(--radius-button)]` |
| Hover background | `var(--bg-hover)` | `hover:bg-[var(--bg-hover)]` |
| Success states | `var(--success-bg)`, `var(--success-text)` | Status badges |
| Error states | `var(--error-bg)`, `var(--error-text)`, `var(--error-border)` | Error displays |

**Card pattern:**
```tsx
<div
  className="bg-[var(--bg-card)] border border-[var(--border-color)] p-6"
  style={{ borderRadius: 'var(--radius-card)' }}
>
  <h2 className="text-lg font-semibold text-[var(--text-primary)]">Title</h2>
  <p className="text-sm text-[var(--text-secondary)]">Description</p>
</div>
```

Do NOT use: `bg-white`, `border-gray-200`, `text-gray-900`, hardcoded hex/rgb.

---

## 5. Component Rules

| Need | Component | Source |
|------|-----------|--------|
| Data table | `DataTable` | `@/components/ui/DataTable` |
| Entity cards | `EntityCard` | `@/components/ui/EntityCard` |
| FK field lookup | `EntityLookup` | Generate via `/ui-components` (see §6) |
| KPI statistics | `StatCard` | Generate locally per dashboard |
| Loading spinner | `Loader2` | `lucide-react` |
| Page loader | `PageLoader` | `@/components/ui/PageLoader` |
| Docs toggle | `DocToggleButton` | `@/components/docs/DocToggleButton` |

- Do not use raw `<table>` — use `DataTable`
- Do not create custom spinners — use `Loader2` from lucide-react
- Do not import axios directly — use `@/services/api/apiClient`
- Use `PageLoader` as Suspense fallback
- Use `EntityLookup` for FK Guid fields (not `<select>` or `<input>` for GUIDs)

---

## 6. Foreign Key Fields & Entity Lookup

Do not render FK (Guid) fields as plain text input or `<select>`. FK fields MUST use `EntityLookup`.

### Field Type Classification

| Property type | Form field | Component |
|---------------|-----------|-----------|
| `string` / `string?` | Text input | `<input type="text" />` |
| `Guid` (FK — e.g., `EmployeeId`, `DepartmentId`) | **Entity Lookup** | `<EntityLookup />` |
| `bool` | Toggle/Checkbox | `<input type="checkbox" />` |
| `int` / `decimal` | Number input | `<input type="number" />` |
| `DateTime` / `DateOnly` | Date input | `<input type="date" />` with CSS variables |
| `enum` | Select dropdown | `<select>` |

**How to detect FK fields:** Any property named `{Entity}Id` of type `Guid` with a corresponding navigation property.

#### Date Field Pattern

```tsx
<div>
  <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
    {t('{module}:form.startDate', 'Start Date')}
  </label>
  <input
    type="date"
    value={formData.startDate}
    onChange={(e) => handleChange('startDate', e.target.value)}
    required
    className="w-full px-3 py-2 rounded-[var(--radius-input)] border border-[var(--border-color)] bg-[var(--bg-tertiary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
  />
</div>
```

### EntityLookup Props

```typescript
interface EntityLookupProps {
  apiEndpoint: string;        // e.g., '/api/human-resources/employees'
  value: string | null;       // Currently selected entity ID
  onChange: (id: string | null) => void;
  label: string;              // Field label
  placeholder?: string;
  mapOption: (item: any) => { id: string; label: string; sublabel?: string };
  required?: boolean;
  disabled?: boolean;
  error?: string;
}
```

### Usage in Forms

```tsx
import { EntityLookup } from '@/components/ui/EntityLookup';

<EntityLookup
  apiEndpoint="/api/human-resources/departments"
  value={formData.departmentId}
  onChange={(id) => handleChange('departmentId', id)}
  label={t('module:form.department', 'Department')}
  placeholder={t('module:form.departmentPlaceholder', 'Search for a department...')}
  mapOption={(dept) => ({ id: dept.id, label: dept.name, sublabel: dept.code })}
  required
/>
```

### I18n Keys for EntityLookup

Add to the module's translation files:

```json
{
  "form": {
    "employee": "Employee",
    "employeePlaceholder": "Search for an employee...",
    "department": "Department",
    "departmentPlaceholder": "Search for a department..."
  }
}
```

### API Search Convention (Backend)

EntityLookup requires: `GET /api/{resource}?search={term}&pageSize=20`

Response format: `{ "items": [...], "totalCount": 42 }`

The backend service's `GetAllAsync` MUST accept `string? search` parameter and filter on display fields (Name, Code, etc.).

### Rules

- Do NOT render Guid FK as `<input type="text">` — user should never type/paste a GUID
- Do NOT use `<select>` for FK fields — fails with 100+ entities (no search, loads all at once, no sublabels)
- Show entity display name (Name, FullName, Code+Name), not the GUID
- Include `mapOption` to define display mapping
- Support clearing selection (unless required + already set)
- Load selected entity display on mount (for edit forms with pre-existing value)
- Debounced search (300ms), minimum 2 characters
- Load initial options when dropdown opens (first 20)

---

> **Sections 7-9 (Documentation, Testing, Compliance Gates) have been moved to `references/smartstack-frontend-compliance.md` for context reduction.**
> Load that file for: DocToggleButton integration (§7), frontend checklist (§7b), cross-tenant UI patterns (§7c), form testing templates (§8), and 5 compliance gates (§9).
