# Design rubric — form judgment

The knowledge `/ui-design` applies to turn a correct-but-plain scaffolded form into a
*designed* one. Each section gives the decision + the rule + counter-examples. Output of
applying this rubric = the directive set (`currentUserFk`, an ordered `sections[]`, `control`,
`dateBounds`, `fullWidth`, `formLayout`, `editMode`, order) you persist and re-scaffold with.

---

## 1. The current-user FK — who gets the "Me" button

Exactly **one** FK per form may carry `currentUserFk: true`: the FK whose target is **the
signed-in user, acting on this record**. It renders a one-click "Me" that fills the field
with `useAuth().user.id`.

**It IS the current-user FK when** the field means "the user this record is *for* / *by* /
*assigned to*", and its target resolves to the auth user — e.g. `userId` on an Employee
record (you create your own), `assigneeId`/`ownerId`/`requesterId` on a task or request.

**It is NOT** (→ plain lookup, no "Me"):
- a **self-referential** FK — `managerId` (FK → Employee), `parentId`, `reportsToId`. You are
  not your own manager; and the value is an *Employee* id, not a *User* id — "Me" would write
  the wrong id entirely.
- an **arbitrary person** FK you don't act as — `approverId`, `validatedById`, `createdById`
  (system-set), `delegateId`.
- any FK to a **non-person** entity (department, category, contract type).

> Heuristic, not a regex: the old generator matched `/employees?|users?|…/` on the FK target
> and wrongly put "Me" on `managerId`. The whole point of this skill is that **a name can't
> decide this** — the *meaning* does. Decide per field; default to no "Me" when unsure.

If two FKs both look like the current user, pick the one the user literally *is* on this form
(the subject), not a secondary relation. Never set more than one.

---

## 2. Sections — grouping fields into titled cards

A section is no longer only visual: on an EDIT fiche the renderer is **read-first** by
default — each section renders as a read-only card that opens for editing through its own
"Modifier" toggle. A section is therefore a **functional unit of editing**: group fields the
user changes *together* (the whole contract, the whole identity), never fields that merely
look alike.

Group **only when it clarifies**. A short form (≲ 5 editable fields) is best as **one sober
section, no header** (author no `sections[]` — the renderer drops the redundant card title).
Reach for sections when the form has distinct *concerns*:

- **Identity / subject** — who/what the record is (the current-user FK, name, birth date).
- **Core / business** — the domain fields (job, contract, rate, amounts, references).
- **Scheduling** — the dates, when they form a coherent group.
- **Status / system** — read-only/computed fields surfaced for context.

Rules:
- Emit the judgment as an **ordered `sections[]`** (`{key, label?, description?, columns?,
  fields[]}` through `apply-form-directives`) — the array order IS the card order on screen.
  The per-field `section: "<label>"` string is the legacy form (still accepted, never mixed
  with `sections[]` in one call).
- **Precedence discipline — derive, never reinvent, unless the machine grouped**: when the
  pagespec carries `sections[]` or form `tabs[]` with `sectionsOrigin: "authored"` **or with no
  `sectionsOrigin` at all** (fail-safe: a legacy pagespec is presumed BA judgment), that
  grouping is an already-paid human judgment. Derive 1:1; your role shrinks to the
  intra-section order, the date/Me/layout directives, and the missing labels.
  `sectionsOrigin: "derived"` hands the grouping back to you: those sections were
  machine-promoted (tabs[] seed, derive-form-sections backfill) — regroup freely, your
  overlay wins at render anyway (read it through `lib/page-spec-sections.sectionsOriginOf`).
  Only invent a grouping on a pagespec that has none.
- Use a small number of sections (2–4), each with ≥ 2 fields. One field alone in a section is
  noise — fold it into a neighbour.
- Name sections by **meaning** in the project's language (the label becomes the i18n default;
  a proper translated label is set upstream in `create-screen`/the pagespec i18n).
- Don't section just because you *can*. No grouping beats arbitrary grouping.
- `editMode: "direct"` is the deliberate opt-out of the read-first edit fiche (dense
  admin-style pages where everything must be editable at once). Default = `read-first` —
  don't restate it.

---

## 3. Date pickers — inline vs popover, and temporal bounds

Two **orthogonal** judgments per date field: its **presentation** (`control`) and its
**nature** (`dateBounds`). Decide both.

**Presentation — inline vs popover:**
- The **default is popover** (`control: "date-popover"`, or simply omit it): compact,
  click-to-open. Use it for **every** date unless one earns inline.
- **At most ONE** date may be `control: "date-inline"` (always-open in-flow calendar): the
  form's **primary scheduling date** when picking it is the main act of the form (e.g. the
  start date on a booking/leave form). More than one inline = the wall of calendars.
- A **date of birth stays a popover** — an always-open calendar is wasted height for a date
  you set once, and the popover's **month + year dropdowns** already reach 1985 in one click.
  If no date is clearly primary, make them **all popover**. Inline is a deliberate highlight.

**Nature — `dateBounds` (past / future / any):**
- `dateBounds: "past"` for a date that **can't be in the future** — birth date, hire date,
  issue/sign date, any historical record. The picker forbids future year/day/typing.
- `dateBounds: "future"` for a date that **can't be in the past** — deadline, due date,
  expiry, next review, contract *end*. The picker forbids past year/day/typing.
- **Omit** (≡ `any`) when a date legitimately spans both directions (a flexible "effective
  date"). Don't constrain just because you can — a wrong bound blocks a valid entry.
- Bounds are a **judgment of meaning**, exactly like the "Me" button (§1): the field *name*
  is only a signal (`startDate` may be past or future depending on the form). Decide from
  what the form *does*. The deterministic generator never infers this; you record it here.

---

## 4. Layout & order

- **`formLayout: "two-column"`** (default) for most forms — compact fields flow in a balanced
  responsive grid; textareas and the inline calendar span full width automatically.
- **Odd runs balance themselves**: the renderer promotes the LAST cell of an odd run of
  half-width cells to `md:col-span-2`, so a 3-field form never leaves an orphan beside dead
  space. Use `fullWidth: true/false` only to FORCE a span (a primary lookup, a long text
  input) or FORBID the automatic promotion (a tiny toggle that would look absurd stretched) —
  never to restate what the balancing already does.
- **`single-column`** for very short forms (≲ 3 fields), wizards, or when every field is
  full-width.
- **Order** (`fields[]` / the `order` directive): most important first. A good default is
  **identity → core/business → dates → optional → read-only/system last**. Put required,
  high-signal fields near the top; bury computed/system read-only fields at the end (they
  only show when populated anyway).

---

## 5. When to go bespoke (Mode B)

Stay in directive-mode unless the layout is genuinely beyond the vocabulary:
- multi-step / wizard flows, side-by-side compare panes, an embedded map/chart/preview,
  a non-form arrangement.
Then edit the page and mark it `// @customised`. Everything expressible with sections +
controls + layout + order must stay **directive-driven** (regenerable).

## 6. Lifecycle — the moment a field is asked

A form asks each question at the moment the business can answer it. A field belongs to a
**lifecycle phase** when its value only EXISTS once the record has advanced: a payment date
exists once the invoice is paid, a departure date once the employee leaves, exit readings
once the vehicle goes out. Phased fields are **excluded from CREATE** and **status-gated on
EDIT** — persisted as the pagespec's **first-order `lifecycle` block** (canonical schema
`lib/page-spec-lifecycle.ts`), written through `apply-form-directives`'s `lifecycle` input.
It is NOT a uiDesign directive: the capture moment is business semantics, not layout.

Decide from FACTS, strongest first:

1. **The action anchor** — a pagespec action carries a `workflowTransition` and its
   `payloadParameters` name form fields: those fields belong to a phase
   `{key: <code>, statuses: [<toStatus>], capturedBy: <code>}`. This is compile, not
   judgment — `derive-lifecycle --mode derive` already wrote it before your pass; you only
   confirm and complete (e.g. a sibling field the action does not capture but that clearly
   lives in the same phase).
2. **The status anchor** — the entity's status enum carries a value that IS the field's
   moment (`PAYEE` ↔ paymentDate, `TERMINATED` ↔ departureDate/departureReason, `SORTI` ↔
   the exit readings). The name lexicon (payment*/paid*/departure*/exit*/termination*/
   closure*/cancel*/archiv*…) is a CANDIDATE trigger only — assign the phase only when the
   matching status value exists in the enum, verbatim.
3. **No anchor → no phase.** Never invent a lifecycle for an entity without state
   semantics; never gate on a status that is not an enum value verbatim; never phase the
   statusField itself; a field asked at creation AND refined later stays un-phased
   (creation wins). When unsure, leave un-phased — the default is the legacy render,
   always safe.

Bounds: one phase per field; statuses listed explicitly (never "reached or later" — the
Flow BR is the graph, the block never re-declares transitions); **a phased field is never
`required: true`** (its column must stay nullable — phase-scoped requiredness goes in
`phases[].requiredFields` + the capturing action's `payloadParameters[].required`); a field
known at creation but mandatory only later (draft → submitted) goes in `requiredFields`
WITHOUT joining `fields` (it stays a visible optional input at create). Sections stay the
grouping lever (§2) — a phase is not a section; give the « Sortie » fields a section when
they warrant a card (an all-phased section disappears entirely at create).

The write is ADDITIVE and idempotent: an existing `lifecycle` block is never rewritten
(its own idempotency key, independent of `uiDesign`); only genuinely new phase keys merge.

---

## Worked example — the employee create form

**Before** (deterministic, plain — the bug report): a "Me" button on **Responsable**
(`managerId`, a self-ref → wrong id), three **always-open calendars** stacked (birth / hire /
contract-end), every field under one redundant **"DÉTAILS"** card, an unbalanced column.

**Judgment:**
- `userId` → **`currentUserFk: true`** (you create your own employee record); section
  `identity`. `managerId` → **no "Me"** (self-ref), section `organisation`.
- All dates → **popover** (`date-popover`); a DOB must never be inline. None is "primary"
  enough to highlight, so none is inline.
- **Bounds:** `birthDate` + `hireDate` → `dateBounds: "past"` (neither can be in the future);
  `contractEndDate` → `dateBounds: "future"` (an end date can't precede today).
- Sections (ordered): `identity` (userId, birthDate), `organisation` (jobTitleId,
  contractTypeId, managerId), `contract` (hireDate, contractEndDate, activityRate). Read-only
  departure fields/status fall to the end, shown only when set. On the EDIT fiche each of
  these opens independently ("Modifier" per card) — they are units the user changes together.
- `formLayout: "two-column"`; `editMode` left absent (read-first default).

**Directives** (fed to `apply-form-directives`, then to `scaffold-component`):

```json
{
  "formLayout": "two-column",
  "sections": [
    { "key": "identity", "label": "Identité", "fields": ["userId", "birthDate"] },
    { "key": "organisation", "label": "Organisation", "fields": ["jobTitleId", "contractTypeId", "managerId"] },
    { "key": "contract", "label": "Contrat", "fields": ["hireDate", "contractEndDate", "activityRate"] }
  ],
  "fields": [
    { "key": "userId", "currentUserFk": true },
    { "key": "birthDate", "control": "date-popover", "dateBounds": "past" },
    { "key": "hireDate", "control": "date-popover", "dateBounds": "past" },
    { "key": "contractEndDate", "control": "date-popover", "dateBounds": "future" }
  ],
  "order": ["userId","birthDate","jobTitleId","contractTypeId","managerId","hireDate","contractEndDate","activityRate"]
}
```

**Result:** "Me" only on the user, none on the manager; compact popover dates (no wall) whose
month + year dropdowns reach any year in one click; birth/hire dates can't be set in the
future and the contract-end date can't precede today; three meaningful titled cards that open
one at a time on the edit fiche (read-first); a balanced two-column grid — all regenerable.
