---
phase: 16-gh-project-new-columns-ingestion
plan: "02"
subsystem: ui-rendering
tags: [slint, avatar-ring, purpose-filter, chip-bar, discovery]
dependency_graph:
  requires: [purpose-field-pipeline, v002-migration]
  provides: [avatar-ring-rendering, purpose-chip-filtering, purpose-tooltip]
  affects: [card.slint, chip-bar.slint, dashboard.slint, discovery.rs, main.rs]
tech_stack:
  added: []
  patterns: [slint-ring-wrapper, chip-color-extension, purpose-filter-chain]
key_files:
  created: []
  modified:
    - crates/app/ui/card.slint
    - crates/app/ui/chip-bar.slint
    - crates/app/ui/dashboard.slint
    - crates/app/src/dashboard/discovery.rs
    - crates/app/src/dashboard/mod.rs
    - crates/app/src/dashboard/projection.rs
    - crates/app/src/main.rs
    - crates/app/src/live_client.rs
decisions:
  - "Avatar ring uses 30x30 outer Rectangle wrapping 26x26 inner circle at x=2 y=2 (no Slint border-image)"
  - "Purpose tooltip uses conditional element on avatar-touch.has-hover — disappears automatically on hover exit"
  - "sync_chips now takes all_cards parameter to collect distinct purpose labels and their colors for chip building"
  - "chip-toggled handler at index >= STATUS_LABELS.len()+1 reads chip label from Slint model via row_data to avoid SharedString/String boundary issues"
  - "Purpose filter is conjunctive with status and archive filters (applied after status, before archive)"
metrics:
  duration_minutes: 20
  tasks_completed: 1
  tasks_total: 2
  files_modified: 8
  files_created: 0
  completed_date: "2026-03-22"
---

# Phase 16 Plan 02: Purpose Avatar Ring, Tooltip, and Chip Filtering Summary

Purpose-colored 2px avatar ring on every card, conditional tooltip on avatar hover showing Purpose text, ChipData extended with chip-color for per-chip selected colors, Purpose filter chips in chip bar with multi-select OR filtering composing with status and archive filters.

## Tasks Completed

| # | Task | Commit | Status |
|---|------|--------|--------|
| 1 | Avatar ring, tooltip, ChipData extension, Purpose chips, and Purpose filtering | 71364cf | Done |
| 2 | Verify Purpose ring, tooltip, and chip filtering | — | Awaiting human verify |

## What Was Built

### Task 1: Full avatar ring + purpose filtering pipeline

**chip-bar.slint:**
- Added `chip-color: color` field to `ChipData` struct
- Chip selected background now uses `chip.chip-color` instead of hardcoded `#4a7cff`
- All existing non-Purpose chips receive `#4a7cff` (accent) as chip_color from Rust

**card.slint:**
- Added `in property <color> purpose-color: #ffffff` and `in property <string> purpose-label: ""` to RecipientCard
- Replaced bare 26x26 avatar Rectangle with 30x30 outer ring Rectangle (background = purpose-color)
- Inner 26x26 circle at x=2 y=2 with existing avatar-bg and initial text
- Conditional tooltip Rectangle shown when `avatar-touch.has-hover && purpose-label != ""`
- Tooltip uses `Colors.surface-popup` background, `Colors.border-muted` border, `Colors.text-primary` text at `Typography.size-xs`

**dashboard.slint:**
- Wired `purpose-color` and `purpose-label` from CardData to RecipientCard instantiation
- (Linter also pre-wired vision-rx-od/os, recipient-product-names, and Rx callbacks for Plan 03)

**discovery.rs:**
- Added `selected_purpose_filters: HashSet<String>` to `ModeState`
- Added `pub fn filter_cards_by_purpose` — empty set passes all; non-empty excludes cards with no purpose and filters by label

**mod.rs + projection.rs:**
- Re-exported `filter_cards_by_purpose` following existing `filter_cards_by_status` pattern

**main.rs:**
- `sync_chips` signature extended with `all_cards: &[CardData]` parameter
- Collects unique non-empty purpose labels alphabetically from all_cards, builds Purpose chips with per-purpose colors
- `build_initial_chips` updated to include `chip_color: accent` on all initial chips
- `apply_filters` now applies purpose filter between status and archive stages
- `chip-toggled` handler extended: Purpose chip indices >= `STATUS_LABELS.len() + 1`; reads label from chips model via `row_data`; toggles in `selected_purpose_filters`

**live_client.rs (Rule 3 fix):**
- Added missing `use integrations::github::project_client::GithubProjectClient as _` import that was preventing compilation

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Missing GithubProjectClient trait import in live_client.rs**
- **Found during:** Task 1 verification (cargo build)
- **Issue:** `fetch_field_id` and `update_field_text` methods not found because the trait wasn't imported
- **Fix:** Added `use integrations::github::project_client::GithubProjectClient as _;` to live_client.rs
- **Files modified:** `crates/app/src/live_client.rs`
- **Commit:** 71364cf

**2. [Linter pre-add] card.slint and dashboard.slint had Plan 03 properties added by linter**
- Properties `vision-rx-od`, `vision-rx-os`, `recipient-product-names`, `editing-rx-*`, `rx-*-draft`, `save-rx-*`, `copy-rx` were pre-added to card.slint and dashboard.slint by the linter before execution
- These are Plan 03 properties; they compile cleanly without handler stubs in Rust
- Kept as-is (no Rust handlers needed for Plan 02; Plan 03 will wire them up)

## Self-Check: PASSED

- Commit 71364cf exists
- `cargo build --workspace` exits 0
- `cargo test --workspace` shows all unit tests passing (147 in app, plus service/integration suites)
- Logging integration tests fail with Windows file permission errors (pre-existing, unrelated to this plan)
