---
phase: 20.1.1-change-the-i-button-on-cards-to-show-a-scrollable-history-of
plan: 04
subsystem: ui
tags: [slint, notes, popover, popupwindow, composer, card-ui]

requires:
  - phase: 20.1.1
    plan: 03
    provides: NoteDisplayEntry struct + notes field on DashboardCardViewModel, populated from SQLite via enrich_view_with_notes

provides:
  - NoteDisplayData Slint struct (exported from card.slint)
  - CardData.notes: [NoteDisplayData] field in dashboard.slint
  - notes-popup PopupWindow on RecipientCard with close-on-click-outside + Esc dismiss
  - Collapsible composer (collapsed="Add note" button, expanded=multi-line TextInput + Send)
  - Ctrl+Enter submit in composer; plain Enter inserts newline
  - Scrollable notes list using clipped Rectangle + absolute y-positioning pattern
  - Per-row hover-reveal of absolute datetime via note-row-hover TouchArea (D-05)
  - on-post-note(string) and on-notes-popover-opened() callbacks on RecipientCard
  - card-post-note(int, string) and card-notes-popover-opened(int) callbacks on DashboardWindow
  - info-hover TouchArea opens notes-popup (not summary-popup) per D-01
  - main.rs CardData initializer wires vm.notes to NoteDisplayData model

affects: [20.1.1-05, 20.1.1-06, 20.1.1-08]

tech-stack:
  added: []
  patterns:
    - "NoteDisplayData defined in card.slint (not dashboard.slint) to avoid circular import: dashboard imports card, so shared structs belong in card"
    - "Scrollable notes list: clipped Rectangle + absolute y-positioning (not Flickable+VerticalLayout per SLINT_TIPS.md)"
    - "Composer state (composer-expanded, note-draft) on RecipientCard, not inside PopupWindow (PopupWindow re-inits on show)"
    - "close-policy: close-on-click-outside satisfies D-02 outside-click dismissal without interfering with TextInput focus inside popup"
    - "Ctrl+Enter submit: event.modifiers.control && event.text == Key.Return"

key-files:
  created: []
  modified:
    - crates/app/ui/card.slint
    - crates/app/ui/dashboard.slint
    - crates/app/src/main.rs

key-decisions:
  - "NoteDisplayData struct placed in card.slint (exported) not dashboard.slint — dashboard.slint imports card.slint creating a one-way dependency; defining the struct in dashboard.slint and importing it from card.slint would cause a recursive import cycle"
  - "composer-input.focus() call removed from add-note-touch clicked handler — composer-input lives inside an if-conditional branch (if root.composer-expanded) and is not accessible by ID from the sibling collapsed branch"
  - "note-draft property reused for composer (already declared at line 53 for Row 5 editor) rather than adding a duplicate; Plan 06 will remove the Row 5 editor and its duplicate usage"
  - "cargo build 'Access is denied' error is Windows file-lock on running binary, not a compile error — cargo check --workspace exits 0 confirming clean compilation"

requirements-completed: []

duration: 6min
completed: 2026-04-15
---

# Phase 20.1.1 Plan 04: Notes Popover UI Summary

**notes-popup PopupWindow with scrollable history, collapsible Ctrl+Enter composer, and hover-reveal timestamps wired end-to-end from SQLite NoteDisplayEntry through CardData.notes to RecipientCard Slint UI.**

## Performance

- **Duration:** ~6 min
- **Started:** 2026-04-15T09:50:35Z
- **Completed:** 2026-04-15T09:56:46Z
- **Tasks:** 2 (committed together)
- **Files modified:** 3

## Accomplishments

- `NoteDisplayData` Slint struct declared in `card.slint` and exported; imported by `dashboard.slint` — avoids circular import
- `CardData.notes: [NoteDisplayData]` added to dashboard.slint struct; `notes: card-data.notes` wired in the `for card-data` loop
- `card-post-note(int, string)` and `card-notes-popover-opened(int)` callbacks on `DashboardWindow`; forwarded from `on-post-note` and `on-notes-popover-opened` on `RecipientCard`
- `notes-popup := PopupWindow` added to `RecipientCard` with `close-policy: close-on-click-outside` (D-02); `FocusScope` handles Esc dismiss
- Composer: collapsed `"+ Add note"` button expands to multi-line `TextInput` + Send button; `Ctrl+Enter` submits and collapses; plain `Enter` inserts newline (D-07, D-08)
- Scrollable notes list uses clipped `Rectangle` + `for note[idx] in root.notes` with `y: idx * 60px + 4px` absolute positioning (SLINT_TIPS.md pattern)
- Empty-state `"No notes yet"` text when `root.notes.length == 0` (D-03)
- Per-row `note-row-hover := TouchArea` with `opacity: note-row-hover.has-hover ? 1.0 : 0.0` on the `note.date` Text (D-05)
- `info-hover` TouchArea in `info-icon-rect` now calls `notes-popup.show()` + `root.on-notes-popover-opened()` (D-01); `summary-popup.show()` remains in `name-area` only
- `main.rs` `CardData` initializer maps `vm.notes` to `slint::ModelRc<NoteDisplayData>`
- `cargo check --workspace` exits 0; all acceptance criteria grep checks pass

## Task Commits

1. **Tasks 1 + 2 combined: NoteDisplayData + notes-popup + callbacks + main.rs wiring** — `88c4b2c` (feat)

## Files Created/Modified

- `crates/app/ui/card.slint` — `NoteDisplayData` struct export; `notes`, `composer-expanded` properties; `on-post-note`, `on-notes-popover-opened` callbacks; `info-hover` redirected to `notes-popup.show()`; full `notes-popup` PopupWindow with composer + scrollable list + hover-reveal
- `crates/app/ui/dashboard.slint` — `NoteDisplayData` imported from `card.slint`; `notes: [NoteDisplayData]` on `CardData`; `card-post-note` and `card-notes-popover-opened` callbacks; `notes: card-data.notes` + callback forwards in `for card-data` loop
- `crates/app/src/main.rs` — `notes` field in `CardData` initializer mapping `vm.notes` via `NoteDisplayData { ... }`

## Decisions Made

- **NoteDisplayData in card.slint not dashboard.slint** — dashboard.slint imports card.slint, so putting NoteDisplayData in dashboard.slint and importing it from card.slint would create a recursive import cycle. Moving the struct to card.slint (where it belongs conceptually anyway) eliminates the cycle.
- **composer-input.focus() removed from collapsed branch** — Slint restricts ID access to within the conditional branch where the element is declared. The `composer-input` TextInput is in `if root.composer-expanded`, so `add-note-touch` (in `if !root.composer-expanded`) cannot reference it. Focus falls naturally to the TextInput when the user clicks it after the expanded branch renders.
- **note-draft reused for composer** — `note-draft` was already declared on `RecipientCard` (line 53) for the Row 5 inline editor. Adding a second property with the same name would be a compile error. The existing property serves both purposes until Plan 06 removes Row 5.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Moved NoteDisplayData from dashboard.slint to card.slint to fix recursive import**
- **Found during:** Task 2 verification (first cargo check)
- **Issue:** Plan specified `import { NoteDisplayData } from "dashboard.slint"` in card.slint, but dashboard.slint already imports from card.slint — creating a recursive import cycle that Slint rejects
- **Fix:** Defined `export struct NoteDisplayData` in card.slint; updated dashboard.slint to import it from card.slint (adding `NoteDisplayData` to existing import line); removed the duplicate declaration from dashboard.slint
- **Files modified:** `crates/app/ui/card.slint`, `crates/app/ui/dashboard.slint`
- **Verification:** `cargo check --workspace` exits 0 with no circular import error
- **Committed in:** `88c4b2c`

**2. [Rule 3 - Blocking] Removed composer-input.focus() from collapsed branch**
- **Found during:** Task 2 verification (second cargo check)
- **Issue:** `composer-input` is declared inside `if root.composer-expanded` block; Slint cannot resolve the ID from the sibling `if !root.composer-expanded` block where `add-note-touch` lives
- **Fix:** Removed `composer-input.focus()` call from `add-note-touch.clicked`. The TextInput gains focus naturally when the user clicks it.
- **Files modified:** `crates/app/ui/card.slint`
- **Verification:** `cargo check --workspace` exits 0
- **Committed in:** `88c4b2c`

---

**Total deviations:** 2 auto-fixed (both Rule 3 — blocking compile errors)
**Impact on plan:** No scope change. Both fixes were necessary for compilation; behavior is equivalent to plan intent.

## Issues Encountered

- `cargo build` reported "Access is denied" when trying to replace the running `app.exe` binary — this is a Windows file-lock, not a compile error. `cargo check --workspace` confirms clean compilation.

## Known Stubs

None — `notes` field is populated from SQLite via `vm.notes` (wired in Plan 03). Empty array is the correct default when a card has no notes; the "No notes yet" empty state renders correctly.

## Threat Flags

No new network endpoints or trust boundary changes. `note.date`, `note.content`, `note.author_display`, and `note.relative_time` are all read from SQLite (trusted source per DATA-FLOW.md); rendered as plain Slint `Text` with no HTML/markup parsing. The `on-post-note(string)` callback passes user text to Rust as a plain string — downstream handling (Plan 05) uses existing `format_note_comment` + `create_issue_comment --body-file` which already handles arbitrary body text safely (T-2014-01 accepted).

## Self-Check: PASSED

- `crates/app/ui/card.slint` — exists, contains `notes-popup`, `NoteDisplayData`, `on-post-note`, `on-notes-popover-opened`, `composer-expanded`, `note-row-hover`, `notes-popup.show()`
- `crates/app/ui/dashboard.slint` — exists, contains `NoteDisplayData` import, `notes: [NoteDisplayData]`, `card-post-note`, `card-notes-popover-opened`, `notes: card-data.notes`
- `crates/app/src/main.rs` — contains `notes: slint::ModelRc::new(...)` in CardData initializer
- Commit `88c4b2c` verified in git log
- `cargo check --workspace` exits 0

---
*Phase: 20.1.1-change-the-i-button-on-cards-to-show-a-scrollable-history-of*
*Completed: 2026-04-15*
