# Phase 12.1.1 — UI Review

**Audited:** 2026-03-22
**Baseline:** 12.1.1-UI-SPEC.md (approved design contract)
**Screenshots:** Not captured — Slint desktop app, no web dev server

---

## Pillar Scores

| Pillar | Score | Key Finding |
|--------|-------|-------------|
| 1. Copywriting | 3/4 | All modal copy matches spec; empty card grid state copy absent |
| 2. Visuals | 4/4 | Italic/warning treatment, "Pick Recipient" menu placement, and modal hierarchy all match spec |
| 3. Color | 3/4 | All modal colors match spec; write-back error toast uses accent blue instead of spec-mandated warning amber |
| 4. Typography | 4/4 | All font sizes, weights, and italic treatment match spec exactly |
| 5. Spacing | 4/4 | Padding, row heights, and spacing scale all match spec within acceptable tolerance |
| 6. Experience Design | 3/4 | ESC dismiss and auto-focus exceed spec; focused input border and empty grid state missing |

**Overall: 21/24**

---

## Top 3 Priority Fixes

1. **Write-back error toast uses accent blue (#4a7cffe0) instead of warning amber (#f0a030)** — Users cannot distinguish an assignment error toast from informational/success toasts at a glance — Change `background: #4a7cffe0` on the toast Rectangle in `dashboard.slint` line 624 to `background: root.toast-has-undo || root.toast-is-warning ? #f0a030cc : #4a7cffe0` (or add a `toast-is-warning` bool property set to `true` for the write-back error path in `main.rs`)

2. **Empty card grid state missing ("No orders found. Check your Shopify connection in Settings.")** — When zero Shopify orders arrive (no connection, wrong tag, or first run before sync), the user sees a blank card area with no feedback — Add an `if root.cards.length == 0` conditional Text element in `dashboard.slint` in the card scroll area rendering "No orders found. Check your Shopify connection in Settings." at 13px/#6b7590 centered

3. **Focused search input border does not change to #4a7cff** — Users lose the focused-state affordance on the search TextInput in RecipientPickerModal (and pre-existing in LookupModal) — In `recipient-picker.slint` lines 122-125, make `border-color` conditional on `search-input.has-focus`: `border-color: search-input.has-focus ? #4a7cff : #3a4060;`

---

## Detailed Findings

### Pillar 1: Copywriting (3/4)

All copywriting contract strings present and exact:

| Spec String | Found In | Status |
|-------------|----------|--------|
| "Pick Recipient for {name}" | `recipient-picker.slint` line 80 | PASS |
| "Search recipients..." | `recipient-picker.slint` line 154 | PASS |
| "No unlinked recipients found." | `recipient-picker.slint` line 198 | PASS |
| "Create New Recipient" | `recipient-picker.slint` line 228 | PASS |
| "Recipient Name" | `recipient-picker.slint` line 251 | PASS |
| "Shopify Profile URL" | `recipient-picker.slint` line 278 | PASS |
| "Auto-filled from order" | `recipient-picker.slint` line 285 | PASS |
| "Back to List" | `recipient-picker.slint` line 307 | PASS |
| "Create & Assign" | `recipient-picker.slint` line 330 | PASS |
| "Pick Recipient" (menu item) | `card.slint` line 496 | PASS |
| "Assignment saved locally. Failed to update GitHub Project." | `main.rs` lines 2184, 2213, 2236, 2318, 2343, 2383 | PASS |

**Gap:** The spec copywriting contract includes "No orders found. Check your Shopify connection in Settings." for the empty card grid state. No such string exists anywhere in `crates/app/ui/` or `crates/app/src/`. This state is silently blank for users with no Shopify data.

**Note:** Error toast text is repeated verbatim across 6 call sites in `main.rs` (lines 2184, 2213, 2236, 2318, 2343, 2383). No copy divergence, but worth extracting to a constant for maintainability.

### Pillar 2: Visuals (4/4)

All visual requirements met:

- **Unassigned card name treatment:** Two-element conditional Text pattern implemented correctly in `card.slint` lines 116-132. Normal name at `#e0e4ef`, unassigned name at `#f0a030` with `font-italic: true`. The Slint compiler accepted `font-italic` directly (no two-element visibility swap needed).
- **"Pick Recipient" menu placement:** Declared as first item in ellipsis menu (`card.slint` line 491) inside `if root.is-unassigned`, followed by a separator Rectangle (lines 512-521). Correct position and visibility condition.
- **Modal visual hierarchy:** RecipientPickerModal declared last in `dashboard.slint` body (line 736), after LookupModal, achieving correct z-order overlay. Full-screen backdrop `#00000066` with centered 400px panel confirmed.
- **Close button treatment:** Implements a circular hover highlight (`border-radius: 12px`, `background: close-header-touch.has-hover ? #3a4060 : transparent`) at `recipient-picker.slint` lines 91-92 — this exceeds the spec's bare close button specification in a positive way, matching `lookup-modal.slint` pattern.
- **Visual hierarchy clear:** Modal header 16px/600, body 13px/400, labels 11px — size gradient is clear.
- **TouchArea z-order:** `entry-touch` TouchArea declared last within each list row Rectangle (`recipient-picker.slint` line 183), correctly captures hover events.

### Pillar 3: Color (3/4)

**Modal colors — all verified against spec:**

| Element | Spec | Actual | Status |
|---------|------|--------|--------|
| Backdrop | #00000066 | #00000066 (line 24) | PASS |
| Panel background | #242838 | #242838 (line 63) | PASS |
| Search input background | #1a1e2a | #1a1e2a (line 122) | PASS |
| Search input border (default) | #3a4060 | #3a4060 (line 123) | PASS |
| Search input border (focused) | #4a7cff | #3a4060 (static, no focus state) | FAIL |
| List item hover | #2d3348 | #2d3348 (line 169) | PASS |
| List item text | #e0e4ef | #e0e4ef (line 177) | PASS |
| Placeholder text | #6b7590 | #6b7590 (line 156) | PASS |
| "Create New Recipient" text | #4a7cff | #4a7cff (line 230) | PASS |
| "Create New Recipient" icon | #4a7cff | #4a7cff (line 224) | PASS |
| Close button icon | #8a92a8 | #8a92a8 (line 96) | PASS |
| "Create & Assign" button (default) | #4a7cff | #4a7cff (line 327) | PASS |
| "Create & Assign" button (hover) | #3a6aee | #3a6aee (line 327) | PASS |
| "Back to List" button (default) | #2d3348 | #2d3348 (line 304) | PASS |
| "Back to List" button (hover) | #3a4060 | #3a4060 (line 304) | PASS |
| Warning (unassigned name) | #f0a030 | #f0a030 (card.slint line 129) | PASS |
| Warning ("Pick Recipient" menu) | #f0a030 | #f0a030 (card.slint line 499) | PASS |

**Gap — Toast background color mismatch:**
The spec (Interaction States section) states the write-back error toast should use `#f0a030` background (warning, not destructive — the assignment succeeded locally). The actual toast Rectangle in `dashboard.slint` line 624 uses `background: #4a7cffe0` (accent blue, same as all other toasts). There is no `toast-is-warning` bool or any conditional on the toast background, so the write-back error toast renders identically to informational toasts.

No hardcoded non-spec colors found in the new components. No hex values outside the documented palette used.

### Pillar 4: Typography (4/4)

Font sizes found in `recipient-picker.slint` against spec contract:

| Spec Role | Spec Size | Actual Usage | Status |
|-----------|-----------|--------------|--------|
| Heading (modal title) | 16px/600 | 16px/600 (lines 81-82) | PASS |
| Body (recipient names, inputs) | 13px/400 | 13px (lines 133, 155, 176, 229, 270, 308, 331) | PASS |
| Label (field labels) | 11px/400 | 11px (lines 252, 279) | PASS |
| Helper text (empty state, URL hint) | 12px/400 | 12px (lines 199, 286) | PASS |
| Close button icon | 14px | 14px (line 95) | PASS |
| "+" create icon | 18px | 18px (line 223) | PASS |
| "Create & Assign" button | 13px/600 | 13px/600 (lines 331-332) | PASS |

Italic treatment in `card.slint` line 131: `font-italic: true` on the unassigned name Text element. Slint accepted it directly — no two-element swap required. Font size 13px matches spec.

No out-of-spec font sizes or weights detected in the new components.

### Pillar 5: Spacing (4/4)

Spacing values in `recipient-picker.slint` against declared scale:

| Element | Spec | Actual | Status |
|---------|------|--------|--------|
| Modal panel outer padding | md (16px) | `padding: 16px` (line 73) | PASS |
| Outer VerticalLayout spacing | md (16px) | `spacing: 12px` (line 74) | Minor (12 vs 16) |
| Search input height | 32px (exception) | 32px (line 121) | PASS |
| List item row height | 36px (exception) | 36px (lines 167, 214) | PASS |
| Modal panel min-width | 400px | `width: 400px` (line 61) | PASS |
| Modal panel max-height | 480px | `height: min(480px, ...)` (line 62) | PASS |
| List/form section spacing | sm (8px) | `spacing: 8px` (lines 116, 298) | PASS |
| Create form section spacing | md (16px) | `spacing: 12px` (line 246) | Minor (12 vs 16) |
| HorizontalLayout padding | sm (8px) | `padding-left: 8px` (line 173) | PASS |
| Button row spacing | sm (8px) | `spacing: 8px` (line 298) | PASS |
| Separator line height | 1px | 1px (line 208) | PASS |

The two `spacing: 12px` instances (lines 74 and 246) are not in the declared scale (scale goes 8/16). However, 12px is between sm and md and these are internal layout values that do not affect the visual rhythm perceptibly. No arbitrary `[Npx]` or `[Nrem]` Tailwind-style overrides exist (Slint uses literal pixel values throughout, consistent with the existing codebase pattern).

### Pillar 6: Experience Design (3/4)

**Implemented correctly:**
- Modal state resets on all close paths: backdrop click (`main.rs` pattern), ESC key, close button — all set `search-text = ""`, `show-create-form = false`, `create-name = ""` (`recipient-picker.slint` lines 35-42, 50-54, 105-110)
- ESC key handling via `FocusScope` (`recipient-picker.slint` lines 29-43) — this exceeds the spec (spec only required backdrop/close-button dismiss)
- Auto-focus search input on modal open via `changed focus-trigger` handler (`recipient-picker.slint` lines 140-146)
- Scroll reset on open (`list-flick.viewport-y = 0px` line 144)
- Write-back error toast at 5000ms (`main.rs` lines 2188-2195) — matches spec-declared 5000ms duration
- Assignment is non-blocking: local card update immediate, GH write-back in background thread
- Modal closes before background thread fires, so no "assigning in-flight" blocking state needed (consistent with spec's "no spinner required" decision)
- `create-name` reset after Create & Assign (`recipient-picker.slint` line 345) — prevents stale name on next open
- `mouse-cursor: pointer` on all interactive TouchAreas

**Gaps:**

1. **Search input focused border not implemented:** Spec declares `border-color: #4a7cff` when focused. `recipient-picker.slint` line 123 has `border-color: #3a4060` with no conditional. The `search-input` TextInput element is named and accessible — adding `border-color: search-input.has-focus ? #4a7cff : #3a4060` to the wrapper Rectangle is a one-line fix. Note: `lookup-modal.slint` also lacks this, so this is a pre-existing pattern gap the spec intended to correct.

2. **Empty card grid state not implemented:** When zero cards arrive from the Shopify sync, the card scroll area is blank. Spec copywriting contract specifies "No orders found. Check your Shopify connection in Settings." No conditional empty-state rendering exists in `dashboard.slint`. This affects first-run UX when Shopify is misconfigured or the "wit-what" tag returns no orders.

3. **"Pick Recipient" menu item height is 28px, not 36px:** Other ellipsis menu items use `height: 28px` consistently (`card.slint` lines 492, 527, etc.). The spec does not explicitly prescribe this height for the menu item (only for picker list rows), so this is consistent with the existing ellipsis menu item pattern and not a violation.

---

## Registry Safety

Registry audit: shadcn not initialized. No third-party component registries used. All UI is Slint custom components. Registry audit skipped per protocol.

---

## Files Audited

- `crates/app/ui/recipient-picker.slint` (new component — primary audit target)
- `crates/app/ui/card.slint` (unassigned treatment + ellipsis menu item)
- `crates/app/ui/dashboard.slint` (CardData extension, picker modal wiring, toast rendering)
- `crates/app/src/main.rs` (toast text, toast duration, assignment write-back callbacks)
- `.planning/phases/12.1.1-.../12.1.1-UI-SPEC.md` (audit baseline)
- `.planning/phases/12.1.1-.../12.1.1-CONTEXT.md` (design decisions reference)
- `.planning/phases/12.1.1-.../12.1.1-01-SUMMARY.md` through `12.1.1-05-SUMMARY.md` (implementation reference)
