# Phase 09 — UI Review

**Audited:** 2026-03-22
**Baseline:** Abstract 6-pillar standards (no UI-SPEC.md)
**Screenshots:** Not captured (no dev server running — code-only audit)

---

## Pillar Scores

| Pillar | Score | Key Finding |
|--------|-------|-------------|
| 1. Copywriting | 3/4 | Contextual copy is strong; "Save"/"Cancel" are generic labels but acceptable for inline note editing |
| 2. Visuals | 3/4 | Clear dark-theme hierarchy; item squares redesign is elegant; fetched image preview shows placeholder text "img" instead of a real thumbnail |
| 3. Color | 3/4 | Cohesive dark palette with consistent accent use; 20+ hardcoded hex values with no token system |
| 4. Typography | 2/4 | Six distinct font sizes in use (9px, 10px, 11px, 12px, 13px, 16px/18px); scale is fragmented with near-duplicate sizes |
| 5. Spacing | 3/4 | Mostly consistent pixel-based spacing; one asymmetric padding in toast (5px top/bottom vs 8px sides) |
| 6. Experience Design | 3/4 | Good state coverage for errors and loading; empty lookup results state shows no guidance text; image fetch falls back silently with no user-visible status |

**Overall: 17/24**

---

## Top 3 Priority Fixes

1. **Fetched image preview shows "img" text placeholder instead of a real image** — When a user enters a Shopify URL and the fetch succeeds, the Create New form shows a 36x36 gray box with the literal text "img" (lookup-modal.slint line 396-403). This tells the user nothing about whether their product image was correctly fetched. Fix: load the fetched URL into a Slint `image` type property and bind it to an `Image` element so the actual product thumbnail is visible before confirming.

2. **Typography scale has 6-7 distinct sizes including a 9px size** — The 9px "img" label in the Shopify fetch preview (lookup-modal.slint:401) is below any readable threshold and is also the only use of that size. The 10px "Settings" button label (dashboard.slint:298) is a single-instance size. The result is a range from 9px to 18px with fragmented steps at 9/10/11/12/13/16/18. Fix: consolidate to 4 sizes — 11px (micro labels), 12px (secondary text), 13px (body/input), 16px (modal titles). Replace the 9px text with 11px, and replace the standalone 10px Settings button text with 11px.

3. **Empty lookup results state shows no guidance text** — When the search returns zero results and no "Create New" entry context, the user sees only the "Create New" row at the bottom of an otherwise blank list (lookup-modal.slint lines 186-274). There is no message indicating whether the empty state is because the query returned nothing or because no search has been typed yet. Fix: add a conditional `if root.results.length == 0 && root.search-text != ""` guard to show a short text such as "No products matched your search" above the Create New row.

---

## Detailed Findings

### Pillar 1: Copywriting (3/4)

**Strengths:**
- Modal title "Shipment Product Lookup" is precise and matches the user's own terminology from CONTEXT.md.
- Subtitle "Adding item to [name]" is contextual and clearly communicates scope (lookup-modal.slint:92).
- Sync indicator copy "1 edit pending" / "N edits pending" (dashboard.slint:276) is accurate and non-alarming.
- "Refresh failed - retry" on the three-dots menu item (card.slint:642) is direct and action-oriented.
- Error validation "Name required" (lookup-modal.slint:335) is concise.
- "Fetching image..." (lookup-modal.slint:383) gives clear in-progress feedback.
- "Pick Recipient" menu item (card.slint:498) is contextual.

**Issues:**
- "Save" and "Cancel" (card.slint:375, 401) are generic. In this specific context — inline note editing — they are acceptable because the action scope is unambiguous. Minor flag only.
- The summary popup "Close" button (card.slint:758) is generic, but acceptable given its placement in a modal context.
- The connection status text "Not configured" (dashboard.slint:325) is technically accurate but could be more inviting. Low priority.

### Pillar 2: Visuals (3/4)

**Strengths:**
- Item squares redesign (card.slint lines 195-321) is a strong visual improvement over pipe-separated text. 36x36 squares with 2-letter initials, a deterministic 8-color palette, and a round + add button create a visually coherent item area.
- The hover overlay for item squares uses an opaque background rectangle to mask the default label text (card.slint:274-291) — a clever solution for the dynamic label requirement.
- The X remove button uses opacity toggle instead of conditional visibility (card.slint:251) to prevent hover flicker — correct technique.
- The semi-transparent backdrop on the lookup modal (#00000066) appropriately dims the app content (lookup-modal.slint:41).
- Dark-theme color differentiation is consistent: #1a1e2a background, #242838 cards, #12151f card grid.
- The info icon (ⓘ at card.slint:145) provides summary access affordance on the recipient name row.

**Issues:**
- The Shopify fetched image preview renders a gray box with the text "img" (lookup-modal.slint:393-404). This is a placeholder that was never upgraded to an actual `Image` element. When the fetch succeeds, the user has no visual confirmation of the image content.
- The search result thumbnail in the lookup modal is an unconditional gray placeholder rectangle (lookup-modal.slint:203-211), not an actual image. This is expected given the NoopClient returns seed data, but it means users see a non-functional affordance.
- The hover label for item squares (card.slint:284-290) shows "Item Name +N" instead of the full count of other items. For example hovering the first of three items shows "Widget A +2". This is readable but slightly cryptic for new users — "+2 more" would be clearer.

### Pillar 3: Color (3/4)

**Palette structure (backgrounds):**
- Primary surface: #1a1e2a (6 uses) — window background
- Card surface: #242838 + #2d3348 (10+2 uses) — card backgrounds, popup menus
- Interactive hover: #3a4060 (12 uses) — consistent hover state
- Accent: #4a7cff (4 direct text uses, 3 background uses) — primary action color

**Text colors:**
- Primary text: #e0e4ef (12 uses) — strong contrast on dark backgrounds
- Secondary text: #8a92a8 (16 uses) — consistent muted text
- Tertiary text: #6b7590 (8 uses) — metadata/dates
- Warning/pending: #f0a030 (6 uses) — sync indicator, archive actions, unassigned, stale
- Accent text: #7ea8ff (6 uses) — communication links, item initials

**Issues:**
- 20+ distinct hardcoded hex values with no token abstraction. Two near-identical values exist: #c0c8da (card.slint:589, order link) and #c0c6e0 (dashboard.slint:299, settings hover). These differ by only one channel step and appear to be the same intent applied inconsistently. With no token system, drift like this is invisible.
- The 8-color item square palette (card.slint:217-225) introduces 8 additional hardcoded colors for item backgrounds. These are contextually appropriate (deterministic palette coloring) but further deepen the hardcoded-color footprint.
- The settings button hover text color #c0c6e0 (dashboard.slint:299) appears to be unique in the entire file. This is a single-use hardcoded value that diverges from the established #e0e4ef / #c0c8da pattern.

### Pillar 4: Typography (2/4)

**Size inventory across three files:**

| Size | Uses | Context |
|------|------|---------|
| 9px | 1 | "img" placeholder text in Shopify fetch preview (lookup-modal.slint:401) |
| 10px | 1 | Settings button label (dashboard.slint:298) |
| 11px | ~12 | Micro labels: stale badge, error/retry, sync indicator, save/cancel buttons, initials, avatar |
| 12px | ~20 | Secondary body: status date, note preview, menu items, summary labels, chip-bar |
| 13px | ~15 | Primary body: recipient names, input fields, modal results, card menu items |
| 16px | 2 | Three-dots button glyph (card.slint:462), back arrow breadcrumb (dashboard.slint:398) |
| 18px | 2 | Add button "+" glyph (card.slint:311), lookup "+" icon (lookup-modal.slint:247) |
| Modal title | 1 | "Shipment Product Lookup" at 16px (lookup-modal.slint:83) — same size as icon glyphs |

**Assessment against abstract standard (>4 sizes = flag):**
The core body scale uses 11/12/13 with effective differentiation between micro, secondary, and primary. However 9px and 10px are single-use outliers that fragment the scale. The 16px usage is split between a modal heading (intentional) and an icon glyph (coincidental), which obscures the role-based reading of the scale. Total unique sizes: 7 (9, 10, 11, 12, 13, 16, 18). Flag threshold of 4 exceeded.

**Font weights:**
- 600 (semibold): summary popup section headers (card.slint:733-745), modal title (lookup-modal.slint:84)
- 700 (bold): avatar initials (card.slint:108), item square initials (card.slint:235)

Two weights in use. This is within the abstract standard of ≤2 weights. Well-controlled.

### Pillar 5: Spacing (3/4)

**Pattern analysis:**
- Card internal padding: 14px left/right margins used consistently across nearly all card elements (card.slint x: 14px pattern throughout).
- Card grid: 12px gutters between cards and from grid edge (dashboard.slint:463-471). Consistent.
- Item square stride: 42px (36px square + 6px gap) — documented and applied consistently (card.slint:204, 296).
- Modal inner padding: 24px horizontal margins on all modal content (lookup-modal.slint:143, 302, 349, 407). Consistent.
- Summary popup padding: 16px on all sides with 12px section spacing (card.slint:729-730). Consistent.

**Issues:**
- Toast layout has asymmetric padding: `padding-top: 5px; padding-bottom: 5px; padding-left: 16px; padding-right: 8px` (dashboard.slint:639-642). The 5px top/bottom creates a non-standard value in an otherwise 4/8/12/16 rhythm.
- The sync indicator is positioned at `x: parent.width - 154px - 160px` (dashboard.slint:274), which is calculated manually rather than derived from the SearchBar width property. This coupling between the indicator position and SearchBar width formula (dashboard.slint:264) is a maintenance risk but not a UX spacing issue.
- Menu item separator is implemented as an 11px-tall Rectangle with a 1px inner Rectangle at y:5px (card.slint:511-521). The 11px outer height creates 5px of visual spacing above and below the 1px line. This is functional but an unusual approach compared to standard padding-based separators.

### Pillar 6: Experience Design (3/4)

**State coverage:**

| State | Coverage | Evidence |
|-------|----------|---------|
| Error state | Good | `show-error` with retry button (card.slint:693-712); `refresh-error` with visual indicator on three-dots button (card.slint:453); inline name validation (lookup-modal.slint:332-338) |
| Loading state | Partial | `shopify-fetch-in-progress` shows "Fetching image..." text (lookup-modal.slint:379-385); no loading state for initial card data load |
| Empty state | Missing | Lookup modal shows no "no results" message when search returns 0 items |
| Disabled state | Good | `refresh-disabled` disables click and changes cursor (card.slint:650-651); `refresh-all-disabled` dims button (dashboard.slint:343) |
| Pending/offline state | Good | Sync indicator with count (dashboard.slint:273-279); pending edit queue persists to disk |
| Confirmation | Good | Per-item removal uses instant X button with hover reveal (no accidental triggers); note editing has explicit Save/Cancel |

**Interaction design notes:**
- Auto-focus on modal search input via `focus-lookup-search()` public function (dashboard.slint:203-205; lookup-modal.slint:31-33) is excellent — eliminates extra click after opening.
- Auto-focus on Create New name field via `init` callback (lookup-modal.slint:286-288) is correct and consistent.
- Escape key closes the lookup modal from both the backdrop FocusScope and the TextInput key-pressed handlers (lookup-modal.slint:53-61, 164-170). Multiple escape paths are reliable.
- The Retry button in `show-error` state (card.slint:709-711) has a TouchArea but its clicked handler is empty — no callback is wired. This means the retry button displays but does nothing when clicked. This is a dead UI affordance.
- Summary popup auto-focuses its FocusScope on open for Escape key capture (card.slint:776-779). Good pattern.
- Connection status indicator uses a status=4 "Connected (no Shopify)" state (dashboard.slint:328-329) but the color mapping at line 318-323 only handles 0/1/2/4 — status 3 falls through to the final `#e05050` red case, which is correct, but status 4 uses the yellow `#f0c040` color rather than its own distinct color, making "Connected (no Shopify)" visually indistinguishable from "Connecting..." at a glance.

---

## Files Audited

- `crates/app/ui/card.slint` (782 lines) — RecipientCard component with item squares, note editing, summary popup, menu
- `crates/app/ui/dashboard.slint` (763 lines) — DashboardWindow with card grid, sync indicator, modal hosting
- `crates/app/ui/lookup-modal.slint` (467 lines) — LookupModal with search, results list, Create New form
- `.planning/phases/09-item-note-crud-wiring/09-01-SUMMARY.md` through `09-07-SUMMARY.md` — execution context
- `.planning/phases/09-item-note-crud-wiring/09-01-PLAN.md` through `09-07-PLAN.md` — design intent
- `.planning/phases/09-item-note-crud-wiring/09-CONTEXT.md` — user decisions
