# Phase 04 — UI Review

**Audited:** 2026-03-22
**Baseline:** Abstract 6-pillar standards (no UI-SPEC.md)
**Screenshots:** Not captured (no dev server detected — Slint native desktop app, code-only audit)

---

## Pillar Scores

| Pillar | Score | Key Finding |
|--------|-------|-------------|
| 1. Copywriting | 3/4 | CTAs are contextual; "Save"/"Cancel" are acceptable in this micro-edit context; summary popup "Close" is minimal |
| 2. Visuals | 2/4 | Info icon overlaps recipient name text at identical x/y coordinates; icon-only controls ("+", "x") have no tooltips |
| 3. Color | 3/4 | Consistent dark palette; 18 hardcoded hex values with no token abstraction; accent (#4a7cff) is well-contained |
| 4. Typography | 2/4 | 6 distinct font sizes across the two phase files (9–18px); scale has gaps and lacks semantic grouping |
| 5. Spacing | 3/4 | Spacing is manually pixel-placed throughout; no arbitrary rem values but inconsistent padding (4px/12px/16px mix) |
| 6. Experience Design | 3/4 | Solid state coverage for errors, refresh, archive, pending edits; summary popup empty-state copy is absent |

**Overall: 16/24**

---

## Top 3 Priority Fixes

1. **Info icon renders on top of recipient name** — Users see the circled-i glyph overlapping the first characters of every recipient name, making names partially unreadable — Move the info icon to after the name text by giving it a dynamic `x` offset, or place it right-aligned in the name row (e.g. `x: parent.width - 30px; y: 10px`) and adjust the `name-area` TouchArea to cover the full row.

2. **6 font sizes with no semantic grouping** — The scale jumps from 9px (image placeholder fallback) through 10px (Settings label), 11px, 12px, 13px, 16px, 18px — too many stops for a card-dense interface, making visual hierarchy ambiguous — Consolidate to 4 sizes: 11px (meta/secondary), 12px (body/data), 13px (primary card content), 16px (modal titles); eliminate 9px and 18px by using text glyphs at 16px or icon sizing.

3. **Summary popup sections show empty strings with no empty-state copy** — When `summary-all-items`, `summary-last-shipment-date`, etc. are blank strings, the popup shows section headers with nothing below them — Add a conditional fallback: `text: root.summary-all-items != "" ? root.summary-all-items : "None on record"` for each section's data `Text` element.

---

## Detailed Findings

### Pillar 1: Copywriting (3/4)

**Positive findings:**
- Note edit CTAs use "Save" and "Cancel" which are appropriate for inline micro-edits (card.slint:375, 401).
- Three-dots menu items use specific action labels: "Open Discord DM", "Copy Email", "Shopify Profile", "View Order", "Archive", "Archive Now", "Unarchive", "Pick Recipient" — all contextual and unambiguous (card.slint:499–687).
- Lookup modal uses "Shipment Product Lookup", "Adding item to {name}", "Search products...", "Create New", "Name required" — all purposeful (lookup-modal.slint:82–336).
- Refresh error state uses "Refresh failed - retry" (card.slint:642) — specific, not generic.
- Pending edit indicator: "1 edit pending" / "N edits pending" (dashboard.slint:276) — good pluralization.

**Issues:**
- Summary popup "Close" button (card.slint:758) is a minimal generic label. In a floating panel anchored to a card, "Done" would better signal intent. Low severity.
- Connection status labels "Not configured", "Connecting...", "Connected", "Disconnected" are adequate but "Not configured" could be more actionable ("Configure settings").

### Pillar 2: Visuals (2/4)

**Critical issue — Info icon overlaps recipient name:**
- The info icon (`\u{24D8}`) is placed at `x: 48px + 8px, y: 10px` (card.slint:146–151).
- The recipient name Text is at `x: 48px, y: 10px` (card.slint:117–122).
- Since the name starts at x:48 and the icon at x:56, the icon renders within the first ~8px of the name text, overlapping every card's recipient label.
- The `name-area` TouchArea covers the full top 38px row (card.slint:84–95), so the tap target is correct — only the visual rendering is broken.

**Icon-only controls lack affordance context:**
- The "+" add button is a standalone glyph inside a circle with no tooltip or accessible label (card.slint:294–320). Users cannot distinguish this from a generic toggle.
- The per-item "x" remove button (card.slint:244–270) appears only on hover with no label. Hover-reveal is intentional per PLAN, but there is no hover-label for the remove action, only for the item name.
- The `\u{24D8}` info icon has no visible label or tooltip indicating it opens a summary. The TouchArea is the full name row, but the visual affordance is ambiguous — users may not discover the summary panel.

**Positive findings:**
- Hover feedback is well implemented: add button dims to 0.6 opacity at rest and brightens to 1.0 on hover with background color change (card.slint:301–302).
- Remove button fades in via opacity:0.0 to 1.0 on hover — clean reveal pattern.
- Stale badge, unassigned warning color, and archived opacity (0.5 dimming) provide clear status differentiation.
- Three-dots menu `\u{2026}` has background highlight on hover and an error-state color shift (card.slint:453).
- Card visual hierarchy is reasonable: name row at top, status pill + date, items, note — a logical reading order.

### Pillar 3: Color (3/4)

**Hardcoded hex values (no token system):**
The following 18 distinct hex values appear in card.slint with no shared variable or design token abstraction:

| Value | Role |
|-------|------|
| #242838 | Card background |
| #1a1e2a | Window / note-edit field background |
| #12151f | Card grid background (dashboard) |
| #2d3348 | Secondary surface (menu, cancel button) |
| #2a3560 | Avatar bg, status pill bg |
| #3a4060 | Hover state for menu items |
| #4a5578 | Border color (menu, popup) |
| #4a7cff | Primary accent (CTA, links) |
| #4caf50 | Add item green |
| #7ea8ff | Blue text (status pill text, avatar initial) |
| #e0e4ef | Primary text |
| #8a92a8 | Secondary text |
| #6b7590 | Tertiary / date text |
| #c0c8da | Shopify menu item text |
| #ef5350 | Error / remove red |
| #f0a030 | Warning / stale / archive amber |
| #ffffff | White (CTA text) |
| #3a1a1a | Error button background |

dashboard.slint adds `#4a7cffe0` (semi-transparent accent for toast) and `#ffffff30` (undo button overlay).

**Assessment:**
- The palette is internally consistent and semantically reasonable (blue for primary, amber for warnings, red for errors).
- Accent (#4a7cff) is used appropriately: Save button, "Create" button, Close link in summary popup, connection status links — not over-applied to decorative elements.
- Dark-on-dark pairs (e.g. #8a92a8 text on #2d3348 background) may have marginal contrast for secondary content. Not formally measured but worth checking with a contrast tool.
- No token abstraction means a palette change requires touching 18+ inline values across files. Acceptable for a Slint prototype but will create maintenance friction.

### Pillar 4: Typography (2/4)

**6 font sizes in card.slint + dashboard.slint:**

| Size | Usage |
|------|-------|
| 9px | Image placeholder fallback text in lookup-modal ("img") |
| 10px | Settings button label (dashboard.slint) |
| 11px | Stale badge, missing label, note Save/Cancel, retry, summary Close, pending-edit indicator |
| 12px | Status date, note preview, item display label, hover label, menu items, secondary text |
| 13px | Recipient name, item squares initials, lookup results, refresh-all button |
| 16px | Three-dots ellipsis, modal titles (lookup), back breadcrumb arrow |
| 18px | Add item "+" glyph, "Create New" "+" glyph |

**Issues:**
- 7 sizes across the full UI (workspace-wide) exceeds the recommended 4-size ceiling for a dense card interface.
- 9px is used only for the "img" placeholder text inside a 36x36 thumbnail in the lookup modal — this is essentially invisible and should be removed or replaced with an icon.
- 18px is used exclusively for the "+" add button glyphs. Using `font-size: 18px` on a single character glyph is a workaround for the absence of an icon library; standardizing on 16px would reduce the scale.
- 10px appears only on the Settings button label — this size is at the legibility boundary on standard displays.
- Two weights are used: 600 (section headers in summary popup) and 700 (avatar initials, modal title). This is appropriate — two weights is within the recommended two-weight ceiling.

**Positive findings:**
- Weight usage is disciplined: heavy weight (700) is used only for the avatar initial and modal title, not scattered across body copy.
- Font sizes are declared as literal `px` values inline, which is consistent with Slint's unit system.

### Pillar 5: Spacing (3/4)

**Spacing values used in card.slint and dashboard.slint:**
- Padding: 4px (menu VerticalLayout), 12px (grid gaps, popup spacing), 16px (summary popup padding, toast padding-left)
- Spacing: 8px (toast, breadcrumb), 12px (summary popup sections)
- Pixel-placed coordinates: x/y are manually set throughout (e.g. `y: 40px`, `y: 76px`, `y: 96px`, `y: 148px`)

**Issues:**
- Card internal layout uses absolute pixel positioning (`x: 14px; y: 40px`) rather than a layout container. This creates implicit dependencies: if the card height changes or a row shifts, downstream y-values must all be updated manually.
- The card height is fixed at 196px (dashboard.slint:469), and all interior y-values are hardcoded against that constant. This is fragile — adding a new row requires careful arithmetic across card.slint.
- Padding mix: 4px inside the three-dots menu, 12px in card grid, 16px in popup and toast. Three distinct padding values without a clear scale rationale.
- No arbitrary rem values found — spacing uses px throughout, which is appropriate for a fixed-resolution Slint window.

**Positive findings:**
- Grid gap math is clean and consistent: `(parent.width - 48px) / 3` for card width with 12px gaps (dashboard.slint:468–471).
- Item square stride is explicitly documented: 42px = 36px square + 6px gap (card.slint:204).
- Toast dimensions are explicit with clear layout padding values.

### Pillar 6: Experience Design (3/4)

**State coverage audit:**

| State | Present | Location |
|-------|---------|----------|
| Loading / connecting | Yes | `connection-status` enum with "Connecting..." text (dashboard.slint:325–333) |
| Refresh in progress | Yes | `refresh-disabled` flag disables button and changes cursor (card.slint:650) |
| Error state | Yes | `show-error`, `refresh-error`, error chip with retry button (card.slint:693–712) |
| Pending edits | Yes | `pending-edit-count` indicator with "N edits pending" amber text (dashboard.slint:273–279) |
| Empty search results | Partial | No empty state message when card list is empty or search yields zero cards |
| Archive confirmation | Yes | Two-step: "Archive" -> "Archive Now" with TBA intermediate state; opacity dimming on archived cards |
| Item remove confirmation | Partial | `remove-confirming` property exists on card but the confirm prompt in the three-dots menu is not visible in card.slint; the per-square X button fires `remove-single-item` immediately without confirmation |
| Summary popup empty sections | No | Section headers render with no content when summary fields are blank strings |
| Note edit cancel recovery | Yes | Cancel sets `editing-note = false` without saving, preserving original note |
| Unassigned card state | Yes | Warning color, italic name, "Pick Recipient" menu option (card.slint:491–509) |

**Issues:**
- Per-item remove (the hover X on item squares) fires immediately via `root.remove-single-item(sq-index)` with no confirmation dialog (card.slint:265–269). The `remove-confirming` property exists in `CardData` and `RecipientCard` but is not wired to the item-square remove flow — only to the legacy `remove-item-clicked` callback path.
- Summary popup sections show bare section headers when data is empty (e.g. first-time recipient with no shipment history). Users see "Last Shipment Date" followed by nothing.
- No empty state for the card grid itself. If all cards are filtered out by search, the card area shows a blank dark rectangle with no guidance message.

**Positive findings:**
- Disabled states are implemented correctly: `refresh-disabled` disables cursor and prevents click (card.slint:650–656).
- Connection status dot changes color across all 5 states (dashboard.slint:318–322) with semantic color mapping.
- Toast system supports undo action for archive operations.
- Escape key closes all overlays (summary popup, card menu, lookup modal, recipient picker).
- Auto-focus is implemented for lookup modal search input and create form name input.

---

## Files Audited

- `crates/app/ui/card.slint` (782 lines) — RecipientCard component, summary popup, card action menu
- `crates/app/ui/dashboard.slint` (764 lines) — DashboardWindow, CardData struct, card grid
- `crates/app/ui/lookup-modal.slint` (468 lines) — Item lookup and create modal
- `crates/app/src/dashboard/state.rs` — referenced via SUMMARY for CardEditState context
- `.planning/phases/04-item-and-recipient-detail-editing/04-01-SUMMARY.md`
- `.planning/phases/04-item-and-recipient-detail-editing/04-02-SUMMARY.md`
- `.planning/phases/04-item-and-recipient-detail-editing/04-03-SUMMARY.md`
- `.planning/phases/04-item-and-recipient-detail-editing/04-01-PLAN.md`
- `.planning/phases/04-item-and-recipient-detail-editing/04-02-PLAN.md`
- `.planning/phases/04-item-and-recipient-detail-editing/04-03-PLAN.md`
