# Phase 08 — UI Review

**Audited:** 2026-03-22
**Baseline:** Abstract 6-pillar standards (no UI-SPEC.md)
**Screenshots:** Not captured (no dev server detected)

---

## Pillar Scores

| Pillar | Score | Key Finding |
|--------|-------|-------------|
| 1. Copywriting | 3/4 | Copy is specific and action-oriented; "Shopify Profile" diverges from plan label "View Customer" without clear rationale |
| 2. Visuals | 3/4 | Avatar circle and contact secondary text add strong visual enrichment; info icon overlaps recipient name at 56px |
| 3. Color | 3/4 | Consistent dark-mode palette with clear semantic roles; no design tokens, all hardcoded hex values |
| 4. Typography | 3/4 | Tight 5-step scale (10–18px) within a dense UI; 18px outlier on the "+" add button creates minor inconsistency |
| 5. Spacing | 4/4 | Consistent absolute-positioned layout with deliberate pixel values; separator 5+1+5 pattern is clean |
| 6. Experience Design | 4/4 | Full state coverage: conditional menu visibility, error toasts with fallback, loading/stale/error/missing states, 1.5s clipboard toast |

**Overall: 20/24**

---

## Top 3 Priority Fixes

1. **Info icon overlaps recipient name** — The info icon is at `x: 48px + 8px = 56px`, the same x as the recipient name text. On short names (e.g. "Bob") the icon renders directly on top of the name text rather than after it. Fix: use a dynamic x offset such as `x: name-area.preferred-width + 52px` or add the icon as a sibling inside a `HorizontalLayout` with the name text.

2. **"Shopify Profile" label does not match the plan spec "View Customer"** — The plan (08-02-PLAN.md line 297, 08-03-PLAN.md UAT verification step 3) consistently calls this menu item "View Customer". The implemented label (card.slint line 586) reads "Shopify Profile". This creates a divergence between what was tested during UAT and what ships. Fix: change `text: "Shopify Profile"` to `text: "View Customer"` in `crates/app/ui/card.slint:586`.

3. **All color values are hardcoded hex literals; no design token layer exists** — With ~60 unique hex values across card.slint alone, any color theme change or accessibility adjustment requires grep-and-replace across multiple files. This is not a user-visible defect today but creates maintenance risk. Fix: define a Slint global struct (e.g. `global AppColors`) with named color constants (`accent-blue`, `surface-elevated`, `text-muted`, etc.) and reference those across all components. This is a refactor task, not a blocking fix.

---

## Detailed Findings

### Pillar 1: Copywriting (3/4)

**What works well:**
- "Open Discord DM" (card.slint:531) — specific and action-oriented, names the exact target app.
- "Copy Email" (card.slint:552) — describes the action precisely (copy, not open).
- "No contact info" (external.rs:15) — clear, non-alarming fallback text. Consistent across code and tests.
- "View Order" (card.slint:606) — unambiguous single-action label.
- "Copied to clipboard" toast (main.rs) — confirms exactly what happened.
- "Archive", "Archive Now", "Unarchive" (card.slint:667) — three distinct states with clear semantic labels.
- Note editor buttons: "Save" (card.slint:375) and "Cancel" (card.slint:401) are generic but contextually acceptable in a compact inline editor.

**Issues found:**

- `card.slint:586` — Menu item text is `"Shopify Profile"`. The original plan (08-02-PLAN.md), UAT verification script (08-03-PLAN.md), and CONTEXT.md all use "View Customer". The renamed label was not documented as an intentional change in any SUMMARY.md. This silently diverges from the tested spec.

- `card.slint:144-150` — Info icon uses Unicode `\u{24D8}` (circled lowercase i). There is no tooltip or accessible label on the `name-area` TouchArea indicating what this icon does. A first-time user has no affordance to discover it opens the summary panel.

- "Retry" label (card.slint:23) on the error button is technically accurate but generic. The in-menu text at card.slint:642 ("Refresh failed - retry") is more descriptive and already contextualizes the action; these two should be consistent.

### Pillar 2: Visuals (3/4)

**What works well:**
- Avatar circle (26x26px, border-radius 13px, `#2a3560` background, `#7ea8ff` initial glyph) is visually consistent with the item square color palette — a deliberate and coherent visual decision.
- Contact secondary text at `y: 24px` creates a two-line name area that differentiates Discord username, email, and "No contact info" at a glance.
- Three-section ellipsis menu with separators provides clear visual grouping of Communication, External Links, and Card Actions.
- Conditional visibility of menu sections (items hidden when data empty, separators hidden when adjacent sections are empty) keeps the menu clean.
- The `#2d3348dd` (88% opacity) semi-transparent background on the remove-X button correctly layers over item squares.

**Issues found:**

- `card.slint:144-151` — The info icon `\u{24D8}` is at `x: 48px + 8px = 56px`. The recipient name text starts at `x: 48px`. On names shorter than ~8px (which is nearly impossible in practice but present in logic), or more practically, on names with a short computed render width, the icon appears overlapping rather than trailing the name. The literal `56px` constant does not adapt to name width. For names 5-6 characters wide at 13px font (~39-46px computed), the icon clears the text. But for exactly 1-character names ("A" at ~8px), the icon will visually collide with the name at position 56 vs 48. This is a layout collision risk, not a theoretical one, since `recipient-initial` is guaranteed to produce 1-character values.

- Contact secondary text and recipient name both begin at `x: 48px` with no overflow guard. At very short tile widths (card minimum ~200px at 3-column layout), a long email like `recipient@longdomain.example.com` will be clipped without the `overflow: elide` property that the option-grid tile correctly adds. `card.slint:135-141` — the contact-secondary Text has no `overflow` property set.

- The `\u{24D8}` info icon has no hover state or cursor change, making it unclear it is clickable. The `name-area` TouchArea covers it, but visually there is no affordance.

### Pillar 3: Color (3/4)

**Color roles identified (consistent application):**
- Background hierarchy: `#1a1e2a` (window) > `#12151f` (card region) > `#242838` (card face) > `#2d3348` (popup/elevated)
- Accent blue: `#4a7cff` / `#7ea8ff` — used for interactive elements (buttons, CTAs, Discord items)
- Warning/archive amber: `#f0a030` — archive actions, missing labels, unassigned cards
- Success/add green: `#4caf50` — add button glyph only
- Error red: `#ef5350` — error states, remove button
- Text hierarchy: `#e0e4ef` (primary) > `#c0c8da` (secondary/external links) > `#8a92a8` (tertiary) > `#6b7590` (muted/secondary contact text)

**Issues found:**

- All ~60 color values across card.slint, dashboard.slint, and option-grid.slint are hardcoded hex literals with no token layer. Any change to the accent blue (e.g., for a lighter theme or accessibility contrast adjustment) requires manual grep across all files.

- `card.slint:252` — Item remove button background uses `#2d3348dd` (hex with alpha channel). This is the only instance in the codebase using an alpha-suffix hex value. All other transparency is handled via the `opacity` property. This inconsistency is minor but introduces a different code pattern without comment.

- Communication section menu items (`#7ea8ff`) vs External Links section items (`#c0c8da`) use different text colors within the same menu. The intent appears semantic (Discord/email = accent blue, Shopify/external = light secondary), but the distinction is subtle and may not be perceptible to all users. It is not documented as intentional in any SUMMARY.md or CONTEXT.md.

### Pillar 4: Typography (3/4)

**Font sizes in use across Phase 8 files:**

card.slint: 10px (option-grid tile secondary — actually in option-grid.slint), 11px, 12px, 13px, 16px, 18px
option-grid.slint: 10px, 11px, 12px, 20px (emoji)

Effective scale (excluding emoji outlier): 10–11–12–13–16–18px — 6 distinct values.

**Font weights in use:** 600 (summary section headers), 700 (avatar/item initials). Only 2 weight values — within standard bounds.

**Issues found:**

- `card.slint:311` — The "+" add button uses `font-size: 18px`. All other UI text is 11–13px. The 18px value was present before Phase 8 but remains as an outlier that breaks the implicit type scale. Within Phase 8 additions specifically, all new text elements (avatar initial 11px, contact secondary 11px, menu items 12px) are consistent with the established scale.

- The 10px font size on the option-grid tile contact-secondary (`option-grid.slint:99`) is the smallest text in the UI. At 10px on a dark background this may fall below comfortable legibility thresholds, particularly at Windows system DPI scaling below 125%. Given the dense UI context this is acceptable, but worth monitoring.

- No font family is declared anywhere in the Slint UI files. The UI relies entirely on the system default font (whatever Slint picks for the platform). This is intentional for a desktop utility app but means the 13/12/11px scale assumes a proportional system font.

### Pillar 5: Spacing (4/4)

**Spacing analysis:**

Phase 8 additions use the following spatial values:
- Avatar circle: `x: 14px, y: 10px, width/height: 26px`
- Recipient name shift: `x: 48px` (14px margin + 26px avatar + 8px gap — arithmetically correct)
- Contact secondary: `x: 48px, y: 24px` (aligns under name, 14px below y:10px start)
- Menu item height: `28px` throughout all new menu items (consistent with pre-existing items)
- Separator wrapper: `height: 11px` with inner 1px line at `y: 5px` (5+1+5 = 11px — clean and explicit)
- Menu popup: `width: 160px, x: parent.width - 170px` (10px right margin — consistent)

**No arbitrary `[Npx]` or `[Nrem]` values found** in any Phase 8 files.

The absolute-position layout (x/y coordinates rather than layout managers) is consistent throughout the codebase and Phase 8 additions follow the same conventions precisely. The 48px name-start calculation (14 + 26 + 8) is arithmetically documented in the SUMMARY.md decision log.

No spacing regressions detected. Score: 4/4.

### Pillar 6: Experience Design (4/4)

**State coverage analysis:**

Loading / in-progress:
- `show-refresh` + `refresh-disabled` (card.slint:20-21) — refresh button disabled during active refresh
- `refresh-all-disabled` (dashboard.slint:52) — global refresh button disabled state
- `shopify-fetch-in-progress` (dashboard.slint:96) — lookup modal loading state
- `pending-edit-count` (dashboard.slint:85) — sync pending indicator

Error states:
- `show-error` + `refresh-error` (card.slint:24-25) — card-level error with retry affordance
- `refresh-error` triggers amber dots-button background (card.slint:453) and "Refresh failed - retry" in menu (card.slint:642)
- External action failures show error toasts via `show_toast` (main.rs callback pattern)
- Discord DM has two-step fallback: `discord://` protocol → HTTPS web URL → error toast

Empty states:
- All new menu items are hidden (not disabled) when corresponding data is empty string (card.slint:526, 547, 581, 601)
- Separators only appear between populated sections (computed bool helpers: `has-communication-items`, `has-external-link-items`)
- `contact-secondary` shows "No contact info" as explicit fallback (external.rs:15)
- `recipient-initial` falls back to "?" for empty name (external.rs:21)
- Contact secondary text conditionally hidden when empty string (card.slint:135)

Feedback / confirmation:
- "Copied to clipboard" toast at 1500ms (appropriate brevity for a reversible, low-stakes action)
- Silent success on external URL open (correct per CONTEXT.md — OS handles the switch)
- Archive confirmation handled by the existing two-step Archive → Archive Now pattern
- Item remove has a confirm step (`remove-confirming` property)

Accessibility gaps (noted, not scored):
- No `accessible-label` properties on any TouchArea elements (Slint supports these)
- Info icon has no hover state or tooltip

Score: 4/4 — all interaction states are handled appropriately for a desktop utility app.

---

## Files Audited

- `crates/app/ui/card.slint` (782 lines) — primary Phase 8 UI file
- `crates/app/ui/dashboard.slint` (764 lines) — CardData struct, callbacks, card loop bindings
- `crates/app/ui/option-grid.slint` (177 lines) — RecipientTileData, contact-secondary in tile
- `crates/app/src/dashboard/external.rs` (236 lines) — helper functions and unit tests
- `crates/app/src/main.rs` (partial: lines 1-430, grep on enrichment fields and callbacks)
