# Phase 06 — UI Review

**Audited:** 2026-03-22
**Baseline:** Abstract 6-pillar standards (no UI-SPEC.md)
**Screenshots:** Not captured (no dev server detected on ports 3000, 5173, 8080)

---

## Pillar Scores

| Pillar | Score | Key Finding |
|--------|-------|-------------|
| 1. Copywriting | 2/4 | Search bar has no placeholder text; zero-results card grid has no empty-state message |
| 2. Visuals | 3/4 | Strong visual hierarchy with avatar circles and section headers; product image slots show box emoji placeholder instead of a proper missing-image indicator |
| 3. Color | 3/4 | Consistent dark palette throughout; one non-semantic hardcoded color deviation in option-grid (#6b7590 secondary row not in core palette vocabulary) |
| 4. Typography | 3/4 | Five distinct sizes across phase-06 files (10/11/12/13/16/20px); 20px emoji size in product tile is an outlier; font-weight discipline is good |
| 5. Spacing | 3/4 | Spacing is mostly systematic (8/12px grid); chip bar internal top/bottom padding uses 5px (off-grid) |
| 6. Experience Design | 2/4 | Three-layer Esc and per-mode state are excellent; search empty state (zero card results) and option-grid tile empty state are both unhandled |

**Overall: 16/24**

---

## Top 3 Priority Fixes

1. **Search bar has no placeholder text** — Users have no affordance that this field filters by name or item; they must guess or discover by typing — Add `placeholder-text: "Search by name or item..."` (or equivalent inline static Text styled as #6b7590) to the SearchBar TextInput in `search-bar.slint:52`.

2. **Zero-results empty state missing** — When search or chip filters reduce the card grid to 0 cards, the user sees only a blank dark region with no explanation — Add a centered `Text { text: "No cards match your filters"; color: #8a92a8; }` element inside `card-flickable` in `dashboard.slint`, guarded by `if root.cards.length == 0 && !root.show-option-grid`.

3. **Option-grid tile empty state missing** — When search filters all tiles out of RecipientGrid or ProductGrid, the Flickable is empty with no feedback — Add a `if root.tiles.length == 0` Text guard inside both grids in `option-grid.slint` with copy such as "No recipients match" / "No products match".

---

## Detailed Findings

### Pillar 1: Copywriting (2/4)

**Missing placeholder text in search bar.**
`crates/app/ui/search-bar.slint:52` — The `TextInput` has no placeholder. In Slint, `TextInput` supports `placeholder-text`. Without it, the input appears as a blank pill with only a magnifying glass emoji; users have no cue about search scope (name? status? notes?).

**No zero-results copy for card grid.**
`crates/app/ui/dashboard.slint:457–544` — The `card-flickable` Flickable renders an empty viewport when all cards are filtered out. There is no "no results" message. The result-count badge on the search bar shows "0 results" (`search-bar.slint:65`), which is some feedback, but the main content area stays blank.

**No zero-results copy for option grids.**
`crates/app/ui/option-grid.slint` — Neither `RecipientGrid` nor `ProductGrid` has a guard or message for when `tiles.length == 0`. Searching on a recipient grid that filters all tiles leaves the scrollable area completely empty.

**Positive: result count badge copy is specific.**
`search-bar.slint:65` — "1 result" / "N results" is correctly pluralized and context-specific. No generic "OK" / "Submit" labels present in the phase-06 components.

**Positive: breadcrumb filtered-label shows the tile name.**
`dashboard.slint:413` — Displaying the actual tile name (e.g. "Alice") as breadcrumb text is clear and user-grounded.

---

### Pillar 2: Visuals (3/4)

**Focal point is clear.** The search bar in the title bar area and the card grid below it form a natural primary-secondary visual hierarchy. The pill shape (border-radius 16px, `search-bar.slint:23`) is distinct from the flat card tiles.

**Avatar circles aid recognition in recipient tiles.**
`option-grid.slint:64–79` — The 24px circle with first initial at `#2a3560`/`#7ea8ff` is a clean visual anchor for the recipient tiles. The secondary text line at 10px (`option-grid.slint:99`) provides readable sub-info without noise.

**A-Z section headers are visually differentiated.**
`option-grid.slint:42–50` — 11px bold `#8a92a8` section headers at y:-30px relative to the tile create clear alphabet group breaks. This is a correct pattern for phonebook-style grids.

**Product image placeholder degrades poorly.**
`option-grid.slint:151–153` — The box emoji (`\u{1F4E6}`, 20px) shown when `image-hint` is non-empty is a developer stand-in, not a designed placeholder. It reads as broken UI rather than intentional. At seed data stage this is expected, but visually it creates an inconsistent impression alongside text-only tiles for recipients. A simple `#3a3f52` filled rectangle (already present as the container background) without the emoji would be cleaner.

**Breadcrumb back-arrow uses left-arrow unicode `\u{2190}`.**
`dashboard.slint:397` — The raw unicode arrow renders as a small glyph on most fonts. It is functional but visually thin relative to the 13px breadcrumb label beside it.

**No hover state on chip bar background when chips overflow.** No scroll-edge gradient indicator is present on the ChipBar Flickable (`chip-bar.slint:13–50`). The context document noted "subtle scroll indicator at edges" as a Claude's-Discretion item; this was skipped. Minor.

---

### Pillar 3: Color (3/4)

The six-color palette is applied consistently:
- `#1a1e2a` background
- `#242838` card/tile surfaces
- `#4a7cff` accent (selected chips, focus ring, active badge, refresh button)
- `#e0e4ef` primary text
- `#8a92a8` secondary text
- `#2d3348` inactive chip / hover state
- `#3a3f52` border / search border

**One undocumented color token.**
`option-grid.slint:100` — `#6b7590` is used for `contact-secondary` text. This value also appears in `dashboard.slint:330` (connection status label) and `dashboard.slint:300` (settings button hover text). It sits between `#8a92a8` secondary and the surface colors, used as a third gray tier. It is consistent with itself but not part of the named palette in the context doc, creating an implicit token. Not a hard problem, but worth formalizing.

**Accent color is not overused.** The `#4a7cff` accent appears on: chip selected state, search focus border, refresh button, filtered-label breadcrumb text, pending edit indicator, and the archived-result "Show" button. These are all interactive or status-indicating elements — appropriate usage.

**No hardcoded colors outside the palette.** All hex values in the phase-06 files (`search-bar.slint`, `chip-bar.slint`, `option-grid.slint`, `dashboard.slint`) resolve to the known palette tokens. The semi-transparent toast (`#4a7cffe0`) and white-glass Undo button (`#ffffff30`) are intentional variants.

**Connection status indicator colors are semantic.**
`dashboard.slint:318–322` — Gray/yellow/green/red map correctly to states. The `#f0c040` yellow is an acceptable status-only use not in the normal component palette.

---

### Pillar 4: Typography (3/4)

**Font sizes used in phase-06 components:**

| Size | Usage |
|------|-------|
| 10px | Settings button label, connection status text |
| 11px | Search result badge, chip labels, section headers, avatar initials, pending-edit indicator |
| 12px | Card name text, product tile names, connection status label, "archived results" message |
| 13px | Search input text, refresh button label, breadcrumb label |
| 16px | Breadcrumb back-arrow glyph |
| 20px | Product tile image-placeholder emoji |

That is 6 distinct sizes in the phase-06 surface. With abstract standards flagging >4 as a concern, this is a borderline situation. However, 10px and 20px are narrow-use single-purpose values (one for the de-emphasized settings button, one for an emoji placeholder). Remove the emoji and this resolves to 4 effective body-text sizes.

**Font weights are disciplined.** Only `700` (bold) is used in phase-06 files, reserved for section headers and avatar initials. No mixed medium/semibold/bold jumble.

**The 20px emoji outlier is the primary concern.** If the product image placeholder is replaced by a plain rectangle, the 20px size disappears and the scale tightens to 10/11/12/13/16px — a clean 5-tier scale with clear purpose at each step.

**No font-family overrides.** All text uses the Slint default, maintaining consistency.

---

### Pillar 5: Spacing (3/4)

**Core spacing values in phase-06:**

| Value | Where |
|-------|-------|
| 6px | ChipBar inter-chip spacing (`chip-bar.slint:25`) |
| 8px | Search layout spacing, tile layout padding |
| 12px | Card grid gaps, tile grid gaps, breadcrumb layout spacing |
| 24px | Tile viewport bottom padding |

The 6/8/12px rhythm is sensible — 6 is half of 12, 8 fits the same binary scale.

**Off-grid chip bar padding.**
`chip-bar.slint:23–24` — `padding-top: 5px; padding-bottom: 5px` uses 5px, which is neither half of 8 (4px) nor part of the 6/8/12 rhythm. The chip height is 26px inside a 36px container, leaving 5px top and bottom. This is a minor vertical centering inconsistency — using `padding-top: 4px; padding-bottom: 4px` or relying on `alignment: center` would be cleaner.

**Tile x/y positioning uses explicit px values.**
`option-grid.slint:36–38` — Recipient tiles use `x: tile-data.grid-col * (tile-w + 12px) + 12px` and `y: tile-data.grid-row * 48px + 12px`. The 48px row stride and 12px margins are consistent with the grid. The 48px stride (36px tile + 12px gap) is intentional but worth documenting.

**Arbitrary section header y-offset.**
`option-grid.slint:49` — Section header Text has `y: -30px`. This is a relative positioning hack to place the letter header above the tile row. Functional, but depends on the 48px row stride holding exactly. If tile height changes, this breaks silently.

---

### Pillar 6: Experience Design (2/4)

**Three-layer Esc is excellently implemented.**
`dashboard.slint:227–229` + `main.rs:1176–1190` — DefocusSearch → ClearSearch → ReturnToOptionGrid → ClearFiltersAndScrollTop → ResetToDefault (double-Esc). This is robust and matches the context spec exactly.

**Per-mode state persistence works correctly.**
`discovery.rs:20–29` — `ModeState` persists `search_text`, `selected_status_filters`, `view_state`, and `selected_tile` per mode. Mode switches restore all state. This is above average for this category.

**Character-key auto-focus works.**
`main.rs:1094–1112` — Printable keys outside search focus append to search text and call `invoke_focus_search_end()`. The cursor-end placement is a UX-positive detail.

**Click-outside defocus works.**
`dashboard.slint:363–373` — Background TouchArea over the content area defocuses search. This handles the common interaction of "I'm done searching, let me scroll."

**Missing: zero-results empty state for card grid.** When both search and chip filters return 0 cards, the main content area is a blank dark rectangle. There is no "No cards match your filters" message and no suggested action (e.g., "Clear search" link). This is a significant gap — a user who over-filters will see nothing and may assume the app broke.

**Missing: zero-results empty state for option grids.** As noted above, both `RecipientGrid` and `ProductGrid` have no `tiles.length == 0` guard. Typing a search that matches no recipient tiles produces a blank scrollable area.

**Missing: disabled state on the "Refresh All" button during search.** The button is present and functional while the user is filtering, but there is no visual feedback that a refresh while filtered applies to all cards and will re-render the filtered view. Low severity, but worth noting.

**No confirmation for mode-reset (double-Esc).** The double-Esc within 500ms resets to the default mode (`DiscoveryMode::ByStatusUpdated`). There is a toast message for this via `toast_message_for_esc`, which mitigates accidental resets. Good.

**Loading states for search results.** Search is synchronous (in-memory filter), so no loading state is needed. This is appropriate, not a gap.

---

## Files Audited

- `crates/app/ui/search-bar.slint`
- `crates/app/ui/chip-bar.slint`
- `crates/app/ui/option-grid.slint`
- `crates/app/ui/dashboard.slint`
- `crates/app/src/main.rs` (search-changed, chip-toggled, character-pressed, tile-clicked, breadcrumb-back wiring)
- `crates/app/src/dashboard/discovery.rs` (ModeState, filter functions, EscAction)
- `.planning/phases/06-advanced-discovery-and-fuzzy-search/06-CONTEXT.md`
- `.planning/phases/06-advanced-discovery-and-fuzzy-search/06-01-SUMMARY.md`
- `.planning/phases/06-advanced-discovery-and-fuzzy-search/06-02-SUMMARY.md`
- `.planning/phases/06-advanced-discovery-and-fuzzy-search/06-03-SUMMARY.md`
