---
name: ui-polish
description: >
  Audits and auto-fixes React pages in a generated SmartStack frontend against the
  customisation-ui design system standards (CSS variable tokens, PageTemplate
  wrapper, DataTable/EntityCard components, lucide-react icons, semantic badges,
  permission keys, i18n namespaces). Produces a violation report (audit mode) or
  applies mechanical fixes (apply mode) based on tokens.json. Invoked after
  Phase 4 of ba-develop to guarantee visual consistency with the
  reference app.
phase: development/frontend
cli: ui-polish
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# ui-polish

Lints `*.tsx` files under `src/pages/**` and `src/features/**/pages/**` against
the SmartStack design system tokens and patterns defined in `tokens.json`.

Source of truth for all rules: `tokens.json` (extracted verbatim from
`D:\01 - projets\SmartStack.app\features\customisation-ui\web\smartstack-web\src\base.css`
+ baseline pages `PresetsListPage.tsx`, `ThemesListPage.tsx`, `TenantDetailPage.tsx`,
`OrganisationCreatePage.tsx`).

## When to use

- After Phase 4 of `ba-develop` completes and the smoke-HTTP gate
  has passed (pages load without 404) — run `--audit` to detect violations,
  then `--apply` to mechanically repair the auto-fixable ones.
- Before committing generated pages to verify they match the reference
  SmartStack.app anatomy.
- When extending an existing generated app manually — run `--audit` before PR.

## Modes

### `--mode audit`

Scans every `*.tsx` under the project's page directories and returns a JSON
report listing each violation (one per rule fired per file). No file
modifications. Exit code `1` if any ERROR severity finding is present, `0`
otherwise (warnings/info do not fail the gate).

### `--mode apply`

Re-runs the audit, then applies mechanical fixes for every finding flagged
`autoFixable: true` in `tokens.json`. Fixes include:

- **R1-pagetemplate** — wrap page body in `<PageTemplate title=…>` if missing.
- **R3-primary-cta** — replace `bg-blue-500`/`bg-[#xxx]` with `bg-[var(--color-accent-500)]`
  on buttons recognized as primary CTAs (first button in page header).
- **R7-error-banner-anatomy** — rewrite `bg-red-*` error banners to the
  token-based anatomy.
- **R10-permission-key-format** — strip `{appCode}.` prefix from permission
  keys; normalize separators to `.`.
- **R11-slot-anatomy** — inject `<Slot name="<entity>.header.actions" />`
  after the PageTemplate opening tag if missing.
- **R13-badge-anatomy** — rewrite `bg-green-500`/`text-white` badges to the
  `bg-[var(--success-bg)] ...` anatomy.
- **R14-no-hardcoded-colors** — remap saturated/semantic Tailwind color
  utilities (`text-blue-600`, `bg-emerald-500`, `border-rose-400`, …) **anywhere**
  in `className` to the accent ramp (`var(--color-accent-<shade>)`) or the
  matching status token (`var(--<status>-bg|text|border)`). **Status dots** — a
  `bg-<status>` on a tiny round element (`rounded-full` + `h-2`/`w-2`/`size-2`…)
  maps to the saturated `var(--<status>-dot)`, not the pale `var(--<status>-bg)`
  surface (which is invisible at dot size).
- **R15-no-neutral-hardcoded-colors** — remap neutral *background* utilities
  (`bg-white`, `bg-gray-100`, `bg-zinc-900`, …) to `var(--bg-card|muted|app)`.
  Neutral `text-*`/`border-*` are **reported only** — the generated theme has no
  neutral text/border token to map them to.
- **R16-token-role-misuse** — flip a status token used outside its role:
  `bg-[var(--<status>-text)]` (saturated foreground used as a fill → illegible,
  worst in dark mode) → `bg-[var(--<status>-bg)]`; `text-[var(--<status>-bg)]`
  (pale surface used as text → invisible) → `text-[var(--<status>-text)]`. Catches
  bugs R2/R14 miss (the value is already a `var(--token)`, only its role is wrong).

Findings flagged `autoFixable: false` are left for Claude to handle (e.g.
hardcoded strings that need i18n extraction, useParams null-checks that
require domain knowledge).

## Invocation

```bash
# Audit — report only, no mutation
npx tsx skills/development/frontend/ui-polish/cli/ui-polish/index.ts \
  --project-path "/abs/path/to/generated-app" \
  --mode audit

# Apply — mechanical fixes then re-audit
npx tsx skills/development/frontend/ui-polish/cli/ui-polish/index.ts \
  --project-path "/abs/path/to/generated-app" \
  --mode apply
```

## Arguments

| Arg | Type | Required | Description |
|-----|------|----------|-------------|
| `--project-path` | string | yes | Absolute path to the generated SmartStack web app (the folder containing `src/pages`). |
| `--mode` | `audit` \| `apply` | yes | `audit` reports only, `apply` rewrites files. |
| `--rules` | string list | no | Comma-separated rule IDs to include (e.g. `R1,R3,R7`). Default = all. |
| `--page` | string | no | Comma-separated page path(s) relative to project root — scan only those (default = all pages under `src/pages` + `src/features`). When set, 1st-level local imports (`@/components/**`, `@/features/**`) are also scanned for violations. |
| `--url` | string | no | Comma-separated URL paths (e.g. `/administration/tenants/{id}`) — resolved to component files via `PageRegistry.register()` data in `src/extensions/*Registry.ts`. |
| `--tokens-file` | string | no | Override tokens.json path (default = sibling of this script). |

## Output envelope

Extends the standard execute envelope with an extra `report` key:

```json
{
  "success": true,
  "command": "ui-polish",
  "data": { "mode": "audit", "rulesRun": ["R1", "R3", "R7", ...], "valid": false },
  "report": {
    "projectPath": "/abs/...",
    "filesInspected": 12,
    "findings": [
      {
        "ruleId": "R1-pagetemplate",
        "severity": "error",
        "file": "src/pages/crm/prospection/ProspectionListPage.tsx",
        "line": 5,
        "message": "Page root is a bare <div>. Must wrap in <PageTemplate title=…>.",
        "autoFixable": true
      }
    ],
    "counts": { "errors": 3, "warnings": 2, "infos": 1 },
    "byRule": { "R1-pagetemplate": 1, "R3-primary-cta": 2, ... }
  },
  "errors": [],
  "warnings": [],
  "nextSteps": [
    "3 ERROR findings — run with --mode apply to auto-fix the 2 fixable ones.",
    "The remaining 1 error (R4-i18n-namespace) requires Claude to extract hardcoded strings."
  ]
}
```

## Rules reference

All rule definitions live in `tokens.json` under `hardRules[]`. Each has:

- `id`: `Rxx-kebab-slug`
- `description`: human-readable explanation
- `violation`: what triggers a finding
- `autoFixable`: `true` if `--mode apply` can repair mechanically
- `severity`: `error` | `warning` | `info`

Current rule set (v1.3.0):

| ID | Severity | Auto-fix | Rule |
|----|----------|----------|------|
| R1-pagetemplate | error | yes | Every page MUST wrap in `<PageTemplate>` |
| R2-no-hex-colors | warning | no | No hex values in className — use `var(--token)` |
| R3-primary-cta | error | yes | Primary CTA must use `bg-[var(--color-accent-500)]` |
| R4-i18n-namespace | error | no | No hardcoded strings — use `useTranslation('…')` |
| R5-lucide-icons | warning | no | Icons from `lucide-react` only |
| R6-loader2-spinner | warning | no | Use `<Loader2 animate-spin />` for loading |
| R7-error-banner-anatomy | error | yes | Error banner must use token-based bg/border/text |
| R8-datatable-for-lists | info | no | List views SHOULD use `<DataTable>` |
| R9-useparams-nullcheck | error | no | `useParams<{id:string}>()` must be null-checked |
| R10-permission-key-format | error | yes | Permission keys: `{module}.{section}.{action}` |
| R11-slot-anatomy | info | yes | Pages SHOULD expose Slot injection points |
| R12-darktheme-base | warning | no | Dark theme base must be `#161618`, not `#0a0a0a` |
| R13-badge-anatomy | warning | yes | Status badges use `bg-[var(--status-bg)]` anatomy |
| R14-no-hardcoded-colors | error | yes | No saturated Tailwind color anywhere in `className` — mapped to accent / status tokens (status dots → `var(--<status>-dot)`) |
| R15-no-neutral-hardcoded-colors | warning | partial | No neutral Tailwind color — `bg-*` mapped to surface tokens; `text-*`/`border-*` reported only |
| R16-token-role-misuse | error | yes | Status token bound to the wrong utility (`bg-[var(--<status>-text)]` / `text-[var(--<status>-bg)]`) — flipped back to its role |
| R17-status-token-as-decoration | warning | no | Status token used as decorative/categorical palette (`{ blue: 'var(--info-bg)' }`) — reserve status for real status |
| R18-colors-outside-classname | warning | no | Tailwind color utilities in JS data structures (objects, arrays, config maps) outside `className` — bypasses R14/R15 |
| R19-hex-in-inline-style | warning | no | Hex/rgb/rgba in `style={{ }}` props — bypasses the design token system |
| R20-accent-overuse | warning | no | Accent token on non-CTA: thick border (`border-2+`), pale fill as section bg (`accent-50/100`), full fill (`≥500`) on non-button |

> **R3/R7/R13 vs R14/R15** — R3 (button), R7 (banner) and R13 (badge) are the
> precise, context-aware fixes; R14/R15 are the general net that catches every
> remaining hardcoded color elsewhere on the page. When both fire on the same
> color at the same location, the audit keeps only the precise finding.

## Design constraints (anti-legacy)

This skill was **not** derived from the existing `skills/development/frontend/component/**`
templates. Rules were extracted directly from:

1. **baseline pages** (`PresetsListPage.tsx`, `ThemesListPage.tsx`, `OrganisationCreatePage.tsx`)
   — the DOM anatomy that the generated code must match;
2. **`base.css` + `index.css`** — the authoritative list of CSS variables and
   helper classes;
3. **`extensions/README.md`** — the Slot/Fill contract.

The existing frontend skills are **informational only**. Never replicate a
pattern from them unless it also exists in the baseline.
