---
name: ui-design
description: >
  Apply DESIGN JUDGMENT to a generated SmartStack form/page that the deterministic
  scaffolder cannot infer — which FK is the signed-in user (the one-click "Me"
  shortcut), how to group fields into sections, which date is a primary inline
  calendar vs a compact popover, the column layout and the field order. Decides the
  judgment, then EITHER re-scaffolds the page through scaffold-component with explicit
  spec directives (the page stays regenerable) OR, for a layout no directive can
  express, edits the page and marks it @customised (the bespoke seam). The CLI renders;
  this skill decides. Pair with /ui-components, which fixes mechanical tokens — this
  fixes meaning and layout. Use when a generated page "works but reads wrong":
  wrong/missing "Me" button, a wall of calendars, ungrouped fields, off layout.
argument-hint: "[entity|page-path] [--screenshot <path>] [--bespoke]"
allowed-tools: Read, Grep, Glob, Bash, Edit
---

# ui-design — the UI JUDGMENT layer (architecture C)

The generator is deterministic by design: it renders a correct, *plain* standard and
**must not guess meaning**. Anything that needs design sense — *is this FK the current
user? what's essential vs secondary? is this date the primary one?* — is **judgment**,
and judgment is this skill's job. You decide; the CLI renders.

> **Two layers, one pipeline.** `scaffold-component` writes the standard (deterministic,
> reproducible). **`/ui-design`** (this skill, LLM) adds the judgment. **`/ui-components`**
> (deterministic, over `ui-polish`) fixes mechanical tokens. They compose, in this order:
>
> ```
> scaffold-component  →  /ui-design  →  /ui-components
> (standard, plain)      (meaning,       (tokens, helper
>                         layout)         classes)
> ```

| | `/ui-design` (this) | `/ui-components` |
|---|---|---|
| Nature | **LLM judgment** | **Deterministic** (no LLM variance) |
| Fixes | sections, "Me" FK, picker, layout, order | hardcoded colors, `.btn`/`.badge-*`, page width |
| Output | spec directives **or** an `@customised` page | mechanical token edits |
| Question | "is the layout / grouping / semantics right?" | "are the tokens right?" |

## When to use

- A generated form "works but reads wrong": a **"Me" button on the wrong FK** (a self-ref
  like `managerId`), **no "Me"** where it belongs, a **wall of always-open calendars**,
  **ungrouped** fields under one flat card, an unbalanced layout, or a poor field order.
- The user points at a page (often with a screenshot) and asks to make the UI coherent.
- After `scaffold-component` / `/ba-develop`, to lift a page from "correct" to "designed".

This skill is for **layout & semantics**. For hardcoded colors / theme-class bypass, that is
`/ui-components` — run it *after* this.

## The directive vocabulary (what scaffold-component honors)

Your judgment is expressed as **spec directives** the renderer already understands
(`development/frontend/component/cli/scaffold-component/types.ts`):

| Directive | On | Effect |
|---|---|---|
| `currentUserFk: true` | a field | renders the one-click **"Me"** button (binds `useAuth().user.id`). **Exactly one per form**; never a self-ref FK. |
| `sections: [{key, label?, description?, columns?, fields[]}]` | the page | the PREFERRED grouping form (through `apply-form-directives`): ordered categories — array order = card order; `fields[]` is sugar, normalised into per-field membership + an ordered `uiDesign.sections` metadata twin. A section is a **functional unit of editing**: on a read-first EDIT fiche each card opens independently via its "Modifier" toggle. `columns` (1-3) sizes the READ grid only. |
| `section: "<label>"` | a field | LEGACY per-field grouping (still honoured — never mixed with `sections[]` in one call): fields sharing the label render under one **titled card** (i18n `form.section.<camel>`). Omit → one sober default card, no header. |
| `editMode: "read-first" \| "direct"` | the page | edit-fiche experience. `read-first` (the default — don't restate it): sections open per-toggle, dirty-gated Save. `direct`: the legacy always-editable page (dense admin forms). |
| `control: "date-inline"` | a date field | an **always-open** in-flow calendar. **At most one** per form (the primary date). |
| `control: "date-popover"` | a date field | a **compact click-to-open** calendar (the safe default — what an omitted control already does). |
| `dateBounds: "past"` | a date field | the date must be **historic** (birth / hire / issue date) → the picker forbids the future: no future year in the dropdown, future days greyed, future typing/chips rejected. |
| `dateBounds: "future"` | a date field | the date must be **forward-looking** (deadline / expiry / review / end date) → the picker forbids the past. |
| `fullWidth: true/false` | a field | `true` → the field always spans **both columns** (like a textarea); `false` → never promoted by the automatic odd-run balancing. The renderer already closes an odd run of half-width cells by promoting its LAST cell — set this only to force or forbid the span. |
| `formLayout` | the page | `two-column` (default) or `single-column` (dense / wizard). |
| field **order** | the spec | the `fields[]` array order = the on-screen order (most important first). |
| `lifecycle: {statusField, phases[]}` | the page | **first-order pagespec key** (rubric §6, canonical schema `lib/page-spec-lifecycle.ts`) — NOT a uiDesign directive: phases anchored on the status enum OWN later-phase fields (hidden at CREATE, status-gated on EDIT — the invoice create form stops asking the payment date) and/or make fields required once a status is reached (`requiredFields`). Written through the same `apply-form-directives` call, ADDITIVELY (an existing block is never rewritten; its idempotency is independent of `uiDesign`). Deterministic anchors are pre-written by `create-prd/cli/derive-lifecycle` — judge only the `needs-judgment` remainder. |

When a directive is **absent**, the renderer applies a safe, plain default (no "Me", popover
dates **unbounded** in both directions, one section). So your job is to add judgment **only
where it improves the page** — never to restate the default. `dateBounds` in particular is a
judgment of *meaning* (like the "Me" button): the generator never infers it from a field name,
so an unbounded date is the default until you decide its nature.

### List directives (`uiDesign.list` — plan UI 2.5)

The same judgment layer exists for LIST pages, written through the same
`apply-form-directives` CLI (`list: { … }` in the spec). Everything REFINES
what the pagespec declares — it never adds capability:

| Directive | Effect | When to author it |
|---|---|---|
| `list.viewMode: "cards"` | PINS cards as the representation — the automatic width-budget switch then never applies (a reader who chose a representation keeps it) | the section's primary reading is visual (directory, catalogue) |
| `list.density: "compact"` | tighter table rows (DataTable `compact`) | high-volume back-office lists (≥ 8 columns, power-user scanning); NEVER on a browsing list |
| `list.statsOrder: ["open","total"]` | display order of the KPI tiles | the decision-driving figure belongs first, the global count last |
| `list.emptyState: {titleKey, descriptionKey, icon, withCreate}` | business-worded empty panel override | the empty list is a CALL TO ACTION (first-run experience), not an error |
| `list.cardFields: {titleKey, subtitleKey, badgeKey, metaKeys[]}` | card anatomy override, by column key | the derived title (first `always` column) reads wrong — e.g. the code is technical and the NAME is the identity |

Rubric: the card TITLE is the value a human uses to NAME the row out loud; the
badge is the state they check first; meta lines are what they compare across
cards (≤ 3). Density is a volumetry judgment, not a taste. An unknown key
warns at generation and falls back to the derivation — check the warnings.

## Mode A — directive-driven (DEFAULT, stays regenerable)

Use whenever the directives above can express the design (the overwhelming majority).

1. **Locate** the target: the entity + section, its pagespec
   (`.smartstack/ba/<APP>/<MODULE>/pagespecs/<Entity>.<view>.md`), the data model
   (`<MODULE>/entité.md`) and the generated page (`src/pages/.../<Entity>FormPage.tsx`).
2. **Read context.** Read the pagespec machine block (fields, fkTo, controls), the entity,
   and — if the user gave a `--screenshot` — read it to ground the visual judgment.
3. **Decide** the directives by applying `references/design-rubric.md`: the current-user FK,
   the section grouping, which (single) date is inline, the layout, the order.
4. **Persist** the judgment into the pagespec (durable, reproducible):

   ```bash
   npx --prefer-offline tsx skills/ui-design/cli/apply-form-directives/index.ts \
     --spec '{"pagespecPath":"<…>/Employee.form.md","formLayout":"two-column",
       "fields":[{"key":"userId","currentUserFk":true,"section":"identity"},
                 {"key":"managerId","section":"organisation"},
                 {"key":"birthDate","control":"date-popover","dateBounds":"past","section":"identity"}],
       "order":["userId","birthDate","jobTitleId","contractTypeId","managerId","activityRate","hireDate","contractEndDate"]}'
   ```

   It writes a namespaced `uiDesign` overlay into the machine block — your other keys
   are untouched, and re-running merges additively.
5. **Re-scaffold live.** Build the `scaffold-component` spec from the entity + pagespec
   (the `fields[]` in ComponentField shape — `name`/`type`/`fkTo`/options) and pass the
   **whole pagespec** (now carrying your `uiDesign` overlay) as `pageSpec`. scaffold-component
   reads `pageSpec.uiDesign` and applies the judgment ITSELF — you do **not** hand-annotate
   the fields. Regenerate the page IN PLACE:

   ```bash
   npx --prefer-offline tsx skills/development/frontend/component/cli/scaffold-component/index.ts \
     --spec '{ …spec…, "pageSpec": { …entire pagespec JSON incl. uiDesign… } }' \
     --pages-dir <dir-of-the-existing-page>
   ```

   The page keeps its `@generated-by` marker → it stays regenerable, and the SAME overlay
   re-applies on every future regen.
6. **Verify**: read the regenerated page and confirm the judgment shows (the "Me" is on the
   right FK and only there, ≤1 open calendar, fields grouped/ordered as decided).

## Mode B — bespoke (`@customised`, the escape hatch)

Only when no directive can express the layout (a multi-step wizard, a side-by-side compare,
an embedded widget, a truly custom arrangement). Pass `--bespoke` or decide it from the need.

1. Start from the scaffolded page (run Mode A first for a clean base).
2. **Edit** the `*FormPage.tsx` with the bespoke design.
3. Add **`// @customised`** as the very first line. `scaffold-component` now honors this and
   will **preserve** the file on future runs (it reports it under `skipped`).
4. Note in your summary that the page is now hand-owned (no longer auto-regenerated).

Prefer Mode A. Bespoke trades reproducibility for flexibility — reserve it for the few pages
that truly need it, and keep the rest regenerable.

## Compose with /ui-components

After Mode A or B, run **`/ui-components`** on the page to fix mechanical tokens / helper
classes. Keep the concerns separate: this skill never hand-fixes a hardcoded hex (that is
ui-polish's deterministic job), and ui-components never makes a layout/semantic decision.

## Guardrails

- **One `currentUserFk` per form** — the FK whose target is the signed-in user *acting on
  the record*. A self-referential or arbitrary person FK (manager, approver, created-by) is
  **NOT** the current user; it gets a plain lookup. (See the rubric.)
- **At most one inline calendar.** Stacking inline calendars rebuilds the wall this skill
  exists to remove. A date of birth is ALWAYS a popover (year navigation matters).
- **Directives over edits.** Never hand-write what a directive expresses — it breaks
  regeneration. Reach for `@customised` only in Mode B.
- **Don't reimplement the renderer.** Drive `scaffold-component`; don't reproduce its JSX.
- The judgment lives in the pagespec (`uiDesign` overlay) so it is durable and reviewable.

> **Pipeline integration (two seams).** (1) **Replay**: `scaffold-component` reads
> `pageSpec.uiDesign` and applies the overlay **deterministically inside the renderer**
> (`applyUiDesignOverlay` — currentUserFk / section / control / dateBounds / fullWidth /
> formLayout / order), and `/ba-develop` Phase 3a passes the pagespec through **verbatim**
> as `pageSpec` — so a persisted overlay is auto-re-applied on every regen, no manual
> step. (2) **First judgment**: `/ba-develop` sub-phase **3.1** runs this skill's rubric
> per module on every form pagespec that has NO overlay yet and persists it via
> `cli/apply-form-directives` BEFORE the 3a fan-out (idempotent — an existing overlay is
> never rewritten; see `ba-develop/references/phases-detail.md` § 3.1). Invoking
> `/ui-design` manually remains the way to REVISE a judgment.

See `references/design-rubric.md` for the judgment rubric (current-user FK, sections,
date pickers, layout, order, and when to go bespoke).
