# FormItem fix spec — Phase X.4.2 batch C

> **MCP cache incomplete — partial spec only.** The figma-mcp-cache for FormItem returned a sparse `<frame>` metadata response with 60+ sub-symbols (one per Type × Layout × Theme × Label Width × Status combination). No JSX/Tailwind detail was included. The MCP guidance was to recursively call `get_design_context` on each `symbol id` — out of scope for this batch. This spec covers only what is derivable from (a) symbol axis names, (b) symbol bounding-box dimensions, (c) `prop-aliases.md` entries, (d) existing `--form-item-*` tokens in `variables.css`.

Inputs:

- Figma cache: `figma-data/normalized/figma-mcp-cache/formitem.tsx` (sparse frame metadata)
- Canonical: `src/components/FormItem/FormItem.vue`
- Token aliases: `src/design-system/translation/token-aliases.ts`
- Prop aliases: `src/design-system/translation/prop-aliases.md`
- Variables: `src/tokens/variables.css`

Figma variant axes (from cache symbol names, parsed by `Type=X, Layout=Y, Theme=Z, Label Width=W, Status=S`):

| Axis | Values |
| --- | --- |
| `Type` | `Label & Input`, `Label & Selector`, `Label & Textarea`, `Label & Radio`, `Label & checkbox`, `Label & Switch` |
| `Layout` | `1 line`, `1 line & Right`, `2 lines` |
| `Theme` | `Dark`, `Light` |
| `Label Width` | `120 px`, `200 px`, `Dynamic` |
| `Status` | `Normal`, `Error` |

Axis ↔ canonical prop mapping (per `prop-aliases.md` Known Entries):

| Code Prop | Figma Property | Status |
| --- | --- | --- |
| `labelWidth` | `Label Width` | approved alias |
| `layout` | `Layout` | approved alias |
| `status` | `Status` | approved alias |
| `theme` | `Theme` | approved alias |
| `type` | `Type` | approved alias |

Bounding-box dimensions (figma 真源, from symbol `width`/`height`):

| Layout | Label Width | Width | Height (Normal) | Height (Error) |
| --- | --- | --- | --- | --- |
| `2 lines` | `Dynamic` | `440px` | `64px` (Input/Selector) / `132px` (Textarea Normal) / `84px` (Input/Selector Error) / `152px` (Textarea Error) | — |
| `1 line` | `200 px` / `120 px` / `Dynamic` | `638px` | `32px` (Input/Selector/Switch/Radio/checkbox Normal) / `52px` (Input/Selector Error) / `100px` (Textarea Normal) / `120px` (Textarea Error) | — |

These dimensions are already encoded as canonical tokens:

- `--form-item-root-width-single-line: 638px`
- `--form-item-root-width-two-lines: 440px`
- `--form-item-label-width-120: 120px`
- `--form-item-label-width-200: 200px`
- `--form-item-label-width-dynamic: auto`

---

## Mismatches

### a. Spacing

| Aspect | Figma 真源 (inferred) | Canonical 现状 | Fix |
| --- | --- | --- | --- |
| `1 line` row height | `32px` (per symbol bbox) | `min-height: 32px` on `.form-item__label` and field-like elements | OK; ensure consistency — propose token `--form-item-row-height: 32px` |
| `2 lines` label-to-content stacking | symbol bbox suggests `32px label + 32px field` = no extra gap (vertical stacking) | `.form-item--two-lines .form-item__main { gap: 0 }` | OK |
| Error message offset (`52px - 32px = 20px` extra) | implies `~20px` for error line below field | `.form-item__content { gap: 4px }` + `.form-item__message { font-size: 12px; line-height: 16px }` = 4 + 16 = 20 ✅ | OK; replace literals with tokens (see below) |
| Textarea normal height (`100px`) | dedicated min height | `min-height: 100px` literal | extract `--form-item-textarea-min-height: 100px` |
| `.form-item__main { gap: 8px }` | inferred — figma label↔field spacing | `gap: 8px` literal | replace with `gap: var(--sp-xs)` |
| `.form-item__field { padding: 8px 12px }` | inferred from input/select padding (alignment with Input component figma 真源) | `padding: 8px 12px` literal | replace with `padding: var(--sp-xs) var(--sp-s)` |
| `.form-item__choice-group { gap: 16px }` | inferred radio/checkbox option spacing | `gap: 16px` literal | replace with `gap: var(--sp-m)` |
| `.form-item__choice { gap: 8px }` | inferred dot ↔ label | `gap: 8px` literal | replace with `gap: var(--sp-xs)` |
| `.form-item__label { gap: 4px; padding: 4px 0 }` | inferred (required asterisk ↔ label, top/bottom internal padding) | literals | replace with `gap: var(--sp-xxs); padding: var(--sp-xxs) 0` |
| `.form-item__content { gap: 4px }` | inferred field ↔ message | `gap: 4px` literal | replace with `gap: var(--sp-xxs)` |

### b. Color

| Element | Figma 真源 token (inferred) | Canonical 现状 | Fix |
| --- | --- | --- | --- |
| Label text (Dark) | `--text-tips` (`#9e9e9e`) | `--fi-label: var(--text-tips)` | OK |
| Label text (Light) | `--text-2` (`#434343`) | `--fi-label: var(--text-2)` | OK |
| Field text (Dark) | `--text-body` | `--fi-text: var(--text-body)` | OK |
| Field text (Light) | `--text-2` | `--fi-text: var(--text-2)` | OK |
| Field placeholder | `--text-placeholder` | `--fi-placeholder: var(--text-placeholder)` | OK |
| Field surface (Dark) | `--bg-layer4` | `--fi-surface: var(--bg-layer4)` | OK |
| Field surface (Light) | `--bg-layer1` | `--fi-surface: var(--bg-layer1)` | OK |
| Field border (Light) | `--line-border` | `--fi-border: var(--line-border)` | OK |
| Field border (Dark) | `transparent` (figma fills surface only) | `--fi-border: transparent` | OK |
| Error border | `--red-hover` | `border-color: var(--red-hover)` | OK |
| Error message | `--red` | `color: var(--red)` | OK |
| Required asterisk | `--red` | `color: var(--red)` | OK |
| Choice border (Dark) | `--line-border` | `--fi-choice-border: var(--line-border)` | OK |
| Choice border (Light) | `--text-disabled` (`#cccccc`) | `--fi-choice-border: var(--text-disabled)` | OK semantically |
| Choice fill (selected) | `--brand` | `background: var(--brand)` | OK |
| Switch on bg | `--brand` | `background: var(--brand)` | OK; figma may have separate off state (currently canonical hard-wires "on"); add off-state `--bg-grey-btn-en` |
| Switch knob | `--color-white` | `background: var(--color-white)` | OK |

### c. Typography

| Element | Figma 真源 | Canonical | Fix |
| --- | --- | --- | --- |
| Label (32px-row form items) | `Roboto/14 body` → `--text-style-body` | `font-size: 14px; line-height: 1.5` literal on `.form-item__label` | replace with `font: var(--text-style-body);` |
| Field text | `Roboto/14 body` → `--text-style-body` | `font-size: 14px; line-height: 1.5` literal on `.form-item__field-text` | replace with `font: var(--text-style-body);` |
| Helper / error message | `Roboto/12 tips` → `--text-style-tips` | `font-size: 12px; line-height: 16px` literal on `.form-item__message` | replace with `font: var(--text-style-tips);` |
| Choice label | `Roboto/14 body` | `font-size: 14px; line-height: 1.5` literal on `.form-item__choice` | replace with `font: var(--text-style-body);` |
| Field icon (selector chevron, error x) | inferred sizes 12px / 16px | literal `font-size: 12px; line-height: 1` | acceptable as text-rendered icon fallback; eventual fix is to swap to `<Icon name="dropdown" size="16">` / `<Icon name="status-error" size="16">` (see Composition) |

### d. Effects

| Element | Figma 真源 | Canonical | Fix |
| --- | --- | --- | --- |
| Field elevation | none (flat field) | none | OK |
| Error halo | none in canonical figma — only border color shift | `border-color` only | OK |

### e. Composition

| Aspect | Figma 真源 | Canonical | Fix |
| --- | --- | --- | --- |
| Field — `Label & Input` content | should be `<Input>` instance | inline `<div class="form-item__field">` placeholder render | gap: canonical fakes input render via slot fallback. Slot is preferred — document that `<slot>` should receive `<Input>` instance; placeholder fallback acceptable for design-time preview only. No tokens to fix here. |
| Field — `Label & Selector` content | `<Select variant="filled">` (per `prop-aliases.md` 表 A composition aliases) | same fake render | same as above |
| Field — `Label & Textarea` | `<Input variant="textarea">` (currently no canonical textarea — TBD) | same | document gap; textarea component itself is out-of-scope for this audit |
| Field — `Label & Radio` | `<Radio>` instances | inline fake `.form-item__radio-dot` | preview only; document |
| Field — `Label & checkbox` | `<Checkbox>` instances | inline fake `.form-item__checkbox-box` | preview only; document |
| Field — `Label & Switch` | `<Switch>` instance | inline fake `.form-item__switch` | preview only; document |
| Resize affordance (textarea) | figma may not show visual handle (browser default) | inline CSS gradient triangle | acceptable runtime addition; document |

### f. Variant coverage

Total figma variants observed in cache: **60 symbols**.

Canonical handles all axes:

- `theme`: Dark / Light ✅
- `layout`: 1 line / 1 line & Right / 2 lines ✅
- `labelWidth`: 120 px / 200 px / Dynamic ✅
- `status`: Normal / Error ✅
- `type`: 6 values ✅

| Variant subset | Canonical coverage | Notes |
| --- | --- | --- |
| `Layout=1 line, LabelWidth=Dynamic, Type=Switch` (right-aligned via `1 line & Right`) | ✅ | OK — `isRightAligned` computed |
| `Layout=2 lines, Type=Switch` | not in figma (no symbol exists) | — |
| `Layout=2 lines, Type=Switch+Right` | not in figma | — |
| Error status for Radio/Checkbox/Switch | not in figma (only Input/Selector/Textarea have Error symbols) | canonical's `hasError && isInputLike` gate is correct |

Coverage is **complete** modulo the design-time preview fakes vs production composition (slot-based). Document this distinction explicitly in component README (out of scope for fix spec).

---

## New domain tokens proposed

| Token | Value | Rationale |
| --- | --- | --- |
| `--form-item-row-height` | `32px` | Inferred from figma `1 line` symbol bbox; canonical uses `min-height: 32px` in 5+ places |
| `--form-item-textarea-min-height` | `100px` | Figma `Textarea Normal` 1-line bbox 100px; explicit anchor |
| `--form-item-error-message-line` | `20px` | `4 + 16` (gap + tips line-height); useful for layout calc |
| `--form-item-switch-width` | `40px` | Switch component dimension; until canonical `<Switch>` is integrated |
| `--form-item-switch-height` | `20px` | Switch component dimension |
| `--form-item-switch-knob-size` | `16px` | Switch knob dimension |
| `--form-item-choice-box-size` | `14px` | Radio/checkbox preview box dimension |

(Optional set — only `--form-item-row-height` and `--form-item-textarea-min-height` are strictly required. The switch/choice tokens become moot once preview fakes are replaced with real `<Switch>` / `<Radio>` / `<Checkbox>` instances.)

---

## Token repoint summary

| Current literal | Proposed token | Location |
| --- | --- | --- |
| `gap: 8px` (`.form-item__main`, `.form-item__choice`) | `gap: var(--sp-xs)` | 2 locations |
| `gap: 4px` (`.form-item__content`, `.form-item__label`) | `gap: var(--sp-xxs)` | 2 locations |
| `gap: 16px` (`.form-item__choice-group`) | `gap: var(--sp-m)` | 1 location |
| `min-height: 32px` (4 places) | `min-height: var(--form-item-row-height)` | 4 locations |
| `min-height: 100px` (textarea) | `min-height: var(--form-item-textarea-min-height)` | 1 location |
| `padding: 8px 12px` (`.form-item__field`) | `padding: var(--sp-xs) var(--sp-s)` | 1 location |
| `padding: 4px 0` (`.form-item__label`) | `padding: var(--sp-xxs) 0` | 1 location |
| `border-radius: 4px` (`.form-item__field`) | `border-radius: var(--r-s)` | 1 location |
| `border-radius: 3px` / `border-radius: 999px` (choice boxes) | keep as literal — these are sub-pixel adjustments; or extract `--r-xs` (=2px, but figma uses 3px for checkbox) — accept literal 3px and 999px (radio) | — |
| `font-size: 14px; line-height: 1.5` (5 places) | `font: var(--text-style-body)` | 5 locations |
| `font-size: 12px; line-height: 16px` (`.form-item__message`) | `font: var(--text-style-tips)` | 1 location |
| Switch dims (`width: 40px; height: 20px`) | propose `--form-item-switch-width/height` (or replace with real `<Switch>`) | 1 location |

---

## Open items / blockers

- **MCP cache incomplete**: 60 sub-symbol detail (paddings, exact field padding, error icon dim, label-icon gap inside `Label & Selector`, textarea resize handle) requires recursive `get_design_context` calls. This spec uses **inferred** values from related components (Input/Select padding 8/12) and `prop-aliases.md` composition-alias table B icons.
- Production fix should swap design-time preview fakes (`.form-item__field`, `.form-item__choice`, `.form-item__switch`) for slot-injected real component instances (`<Input>`, `<Select>`, `<Textarea>`, `<Radio>`, `<Checkbox>`, `<Switch>`). Out of scope for token-level fix spec — flag for follow-up Phase.
- Confirm whether figma `Layout=1 line & Right` for Switch is the **only** right-aligned variant or whether other Types support right-alignment (cache only shows `Switch` symbols with that value).
