# Phase 20.5: Modal State Architectural Audit — Pattern Map

**Mapped:** 2026-04-16
**Files analyzed:** 1 output file (`.planning/MODAL-STATE.md`) + 10 source files read-only
**Analogs found:** 1 / 1

---

## File Classification

| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
| `.planning/MODAL-STATE.md` | architecture-doc | — | `.planning/RETRO-AGENT-FAILURE-PATTERNS.md` (structured reference doc with taxonomy tables, cross-references, and known-pattern catalogs) | role-match |

All source files (Slint modals, main.rs, live_client.rs) are **read-only** — not modified.

---

## Pattern Assignments

### `.planning/MODAL-STATE.md` (architecture-doc)

**Analog:** `.planning/RETRO-AGENT-FAILURE-PATTERNS.md`

This is the only file being written. The RESEARCH.md contains the complete raw inventory for all 8+ modals. The pattern task is: what structure and formatting conventions should the written document follow?

---

#### Document Header Pattern

**Analog:** `.planning/RETRO-AGENT-FAILURE-PATTERNS.md` lines 1–8, `.planning/DATA-FLOW.md` lines 1–10

```markdown
# Modal State Architecture

**Version:** 1.0
**Last updated:** YYYY-MM-DD
**Phase:** 20.5 — Modal State Architectural Audit
**Status:** Authoritative — update in the same commit as any modal state machine change

> This is the mandatory read checkpoint for all modal modifications. The modal layer accounts
> for 71% of recent gap-closure fixes. Before changing any modal, read the relevant section.
```

**Why this header:** DATA-FLOW.md uses a version + "authoritative" header with an agent rule block up front. RETRO doc uses a datestamp + scope + inputs header. MODAL-STATE.md should combine both: version (for "last updated" tracking), an authoritative declaration, and a one-paragraph danger statement visible without scrolling.

---

#### Agent Rules Block Pattern

**Analog:** `.planning/DATA-FLOW.md` lines 12–90 (AGENT RULES section with RULE-01 through RULE-06)

```markdown
## Agent Rules

**Read the relevant modal section before modifying any modal component.**

### RULE-M-01: State reset on both sides (bac9584 lesson)
**Status:** MANDATORY
Any modal that uses `in-out` forwarding properties on DashboardWindow MUST reset those
properties in Rust before calling `set_*_visible(false)`. Slint `in-out` props stay dirty
across opens. Follow the `reset_lookup_modal_state()` pattern (main.rs:4191).

### RULE-M-02: All dismiss paths are parallel — grep before adding logic
**Status:** MANDATORY
Every modal has 2–4 dismiss paths (Esc, X button, backdrop click, Cancel button). Any
side-effect added to one dismiss path MUST be added to all parallel paths for that modal.
Before changing any close handler, grep for all dismiss paths for the modal (see Pitfall 4).

### RULE-M-03: New settings paths MUST call invoke_sync_cards_updated
**Status:** MANDATORY
See CALLBACK_PIPELINE.md INV-2. Both `on_settings_save_clicked` and
`on_settings_shopify_clear_clicked` are compliant sites. Any new settings action must also
call `invoke_sync_cards_updated()` or cards appear stale post-change.

### RULE-M-04: Esc in TextInputs requires explicit key-pressed handler
**Status:** MANDATORY
FocusScope Esc does NOT fire when a TextInput has keyboard focus. Every TextInput inside a
modal MUST have `key-pressed(event) => { if event.text == Key.Escape { root.close-requested(); return accept; } return reject; }`.
```

**Why this block:** DATA-FLOW.md's RULE-XX pattern is the strongest precedent in this codebase for machine-readable "never do X" rules. MODAL-STATE.md needs the same so future agents get structured rules before reading the state diagrams.

---

#### Per-Modal Section Pattern

**Analog:** `.planning/RETRO-AGENT-FAILURE-PATTERNS.md` section 2 (catalogued failure classes with ID, description, source) combined with the RESEARCH.md per-modal inventory structure.

Each modal section should follow this template:

```markdown
## [N]. [ModalName]

**File:** `path/to/component.slint`
**Visibility control:** `property-name: bool` ([direction] on [owner])
**Architecture:** [Separate component / Inline in dashboard] — [Overlay / Mounted sidebar / PopupWindow]

### State Machine

```mermaid
stateDiagram-v2
    [*] --> Closed
    Closed --> Open : [trigger — e.g., card [+] click → main.rs handler → set_*_visible(true)]
    Open --> Confirmed : [action — e.g., state button click → state-selected callback]
    Open --> Closed : [dismiss — Esc / backdrop / X button / Cancel]
    Confirmed --> Closed : [Rust handler sets visible=false after SQLite write]
    note right of Open
        Sub-states: [list in-out sub-state props]
    end note
```

### State Properties

| Property | Direction | Type | Owner | Purpose |
|---|---|---|---|---|
| `property-name` | in / in-out / out | bool/string/int | DashboardWindow / component | Description |

### Callback Chains

| Callback Name | Direction | Source | Handler | Side Effects |
|---|---|---|---|---|
| `callback-name(args)` | out | `component.slint` trigger | `dashboard.slint` → `main.rs:on_handler` (line N) | SQLite write / set_visible / apply_filters / etc. |

### Esc Handling

[One paragraph describing the Esc path(s) for this modal, or "NONE — no FocusScope and no TextInput key-pressed handlers."]

### Dismiss Paths (Parallel Path Inventory)

| Path | Source | Handler | Cleanup |
|---|---|---|---|
| Esc | FocusScope / TextInput key-pressed | close-requested() → ... | [what state is reset] |
| Backdrop click | TouchArea clicked | inline / discard-clicked() → ... | [what state is reset] |
| X button | TouchArea clicked | close-requested() / discard-clicked() | [what state is reset] |
| Cancel button | TouchArea clicked | cancel-clicked() → ... | [what state is reset] |

### Fragile Patterns

> **[RETRO-ID] [Pattern name]:** [One-sentence description of the hazard.]
> [One sentence on how to avoid it.]
```

**Why this structure:** The RESEARCH.md already produced exactly this data for all 8 modals. The section template above maps 1:1 to the RESEARCH.md sections. The Dismiss Paths table is new (not in RESEARCH.md) but is the most direct operationalization of D-05 (known fragile patterns with cross-references) and the parallel-path-drift finding (F1 in RETRO).

---

#### Mermaid State Diagram Pattern

**Analog:** The MODAL-STATE.md placeholder already establishes `stateDiagram-v2` syntax (line 26–32 of current placeholder). RESEARCH.md architectural diagrams (lines 411–439) show the ASCII flow pattern.

For the full document, use per-modal `stateDiagram-v2` blocks using nested states for sub-views. Example for LookupModal (most complex):

```mermaid
stateDiagram-v2
    [*] --> Closed
    Closed --> SearchView : card [+] click → main.rs → set_lookup_modal_open(true)
    state SearchView {
        [*] --> Searching
        Searching --> Expanded : toggle-product-expanded(pid) [inline dashboard]
        Searching --> ReassignPrompt : unit [+] click (unit already assigned)
        Expanded --> Searching : toggle-product-expanded same pid
        ReassignPrompt --> Searching : "Cancel"
        ReassignPrompt --> Closed : "Move it" → unit-force-reassign → main.rs
        Searching --> CreatingSN : [+] on product row
        CreatingSN --> Searching : Esc in SN TextInput (clears creating-unit-product-id)
        CreatingSN --> Searching : create-unit-confirmed(pid, sn) → main.rs
    }
    SearchView --> CreateForm : "Create New Product" button (show-create-form = true)
    state CreateForm {
        [*] --> Editing
        Editing --> FetchingImage : valid Shopify URL entered
        FetchingImage --> Editing : image fetched (shopify-fetched-image-url set)
    }
    CreateForm --> SearchView : "Back" button (show-create-form = false)
    CreateForm --> Closed : create-confirmed(name, url) → main.rs → creates product → resets state
    SearchView --> Closed : close-requested() → main.rs → reset_lookup_modal_state() → set_lookup_modal_open(false)
    CreateForm --> Closed : Esc / close-requested()
```

**Why nested states:** LookupModal has two top-level sub-views (`show-create-form`) and three sub-states within the search view. Simple flat diagrams obscure the branching that causes sub-state bleed bugs. Nested `stateDiagram-v2` directly encodes this.

---

#### Cross-Modal Interaction Map Pattern

**Analog:** No direct codebase analog — this is a "Claude's Discretion" item from CONTEXT.md. Closest structural analog is the "Architectural Responsibility Map" table from RESEARCH.md lines 52–61.

```markdown
## Cross-Modal Interaction Map

Modals that can trigger or interact with other modals:

| Modal A | Interaction | Modal B | Mechanism |
|---|---|---|---|
| ProductDetailPanel | `change-unit-state-clicked` opens | StateTransitionModal | `on_product_change_unit_state` (main.rs:5248) sets `state-modal-visible=true` |
| LookupModal | close path calls | global-keys FocusScope | `unit-selected` / `close-requested` → `global-keys.focus()` to restore global Esc |
| RecipientDetailPanel | Esc (no edit active) → | dashboard global nav | `navigate-back()` dismisses sidebar via mode navigation |
| SettingsModal | save/clear → | sync pipeline | `invoke_sync_cards_updated()` triggers sync which updates card data |

**Mutual exclusion:** No two floating overlays (LookupModal, SettingsModal, StateTransitionModal, AddProductForm, delete-item, start-return) should be open simultaneously. There is no code enforcement of this — it relies on each modal's open trigger calling `set_*_visible(false)` for others first.
```

---

#### Esc Priority Chain Pattern

**Analog:** RESEARCH.md lines 442–467 (Esc Priority Chain section). This should be reproduced verbatim in MODAL-STATE.md as a standalone section because it is the highest-value reference for the C-3 / D-4 failure class.

```markdown
## Esc Priority Chain

Priority is determined by which FocusScope currently holds focus (z-order, highest to lowest):

1. **SettingsModal FocusScope** — grabs focus on `init` when `modal-open=true`
   → fires `cancel-clicked()` → `main.rs:on_settings_cancel_clicked`

2. **LookupModal TextInput key-pressed** — when search input is focused (normal state)
   → fires `close-requested()` directly

3. **LookupModal FocusScope** — when no TextInput is focused
   → fires `close-requested()`

4. **RecipientDetailPanel FocusScope**
   → first Esc: cancels active field edit (returns `accept`)
   → second Esc: fires `navigate-back()` → dashboard mode navigation

5. **global-keys FocusScope** (dashboard.slint) — always active when above are not focused
   → fires `esc-pressed()` → `main.rs` handler → mode navigation / sidebar dismiss

**GAP — Esc does nothing for these modals:**
- StateTransitionModal (no FocusScope, no TextInput key-pressed handlers)
- AddProductForm (no FocusScope, no TextInput key-pressed handlers)
- Delete-item confirmation inline (no FocusScope)
- Start-return confirmation inline (no FocusScope)
- ProductDetailPanel mounted sidecar (no FocusScope)

**FRAGILE (C-3, D-4):** When a modal closes, no code explicitly returns focus to the
global-keys FocusScope. Focus is returned implicitly when the modal's FocusScope is
destroyed. If focus does not return, global Esc stops working until the user clicks the
main window area. The `global-keys.focus()` call in `unit-selected` (LookupModal) is the
only explicit focus-restore in the codebase.
```

---

#### Known Pitfalls Section Pattern

**Analog:** RESEARCH.md lines 492–523 (Common Pitfalls section). The pattern in `code_tips/SLINT_TIPS.md` and `code_tips/CALLBACK_PIPELINE.md` uses: heading, "What goes wrong", "Why it happens", "How to avoid", "Warning signs".

```markdown
## Known Pitfalls

### Pitfall 1: Esc Swallowed by TextInput (C-3, D-4)
**What goes wrong:** Modal has a FocusScope for Esc. When a TextInput has keyboard focus,
the FocusScope never sees the Esc key. Modal cannot be dismissed via keyboard.
**Why it happens:** Slint routes keyboard events to the focused element first.
**How to avoid:** Add `key-pressed(event) => { if event.text == Key.Escape { root.close-requested(); return accept; } return reject; }` to EVERY TextInput in a modal.
**Warning signs:** Esc works clicking the modal backdrop area but not when cursor is in a text field.

### Pitfall 2: State Not Reset on Close (bac9584)
...
```

**Why this structure:** The pitfall format is already established in RESEARCH.md with 6 pitfalls fully written out. Transcribing them into MODAL-STATE.md with the same numbered structure ensures they persist as a living reference after Phase 20.5 is closed.

---

## Shared Patterns

### Table Column Conventions
**Source:** RESEARCH.md (callback table headers), `.planning/RETRO-AGENT-FAILURE-PATTERNS.md` (failure taxonomy tables)
**Apply to:** All callback tables in MODAL-STATE.md

D-03 requires these exact columns in callback tables:
```
| Callback Name | Direction (in/out) | Source File | Handler File | Side Effects |
```

RESEARCH.md uses a slightly extended form that also captures line numbers:
```
| Callback Name | Direction | Source | Handler | Side Effects |
```
where "Handler" cell includes `main.rs:on_handler_name (line NNNN)`.

Use RESEARCH.md column format (with line numbers) — it is more actionable than the D-03 minimum.

### Mermaid Code Fence
**Source:** Existing MODAL-STATE.md placeholder (lines 26–32)
**Apply to:** All state diagrams
Use triple-backtick `mermaid` fences, not indented code blocks. GitHub renders these; indented blocks do not.

### Fragile Pattern Cross-Reference Format
**Source:** RESEARCH.md lines 527–536 (Known Fragile Patterns Cross-Reference table)
**Apply to:** Per-modal "Fragile Patterns" subsections

Cross-reference format: `(C-3, D-4)` inline in the warning text, plus the full table at document level for grep-ability. The table columns are:
```
| Pattern | Modals Affected | Retro ID | Description |
```

### Section Ordering (Organization by Modal)
**Source:** RESEARCH.md "Primary recommendation" (line 47–48)
**Apply to:** Document top-level structure

Organize by modal component (one `##` section per overlay), not by concern. This serves the most common edit pattern: "I'm changing modal X — what do I need to know?" Cross-cutting concerns (Esc priority chain, shared pitfalls, Don't-Hand-Roll table) go at the end as `##` sections.

Recommended top-level order:
1. Agent Rules
2. Complete Inventory Table (one-row-per-modal quick reference)
3. Per-modal sections (1–9, ordered by complexity: LookupModal → SettingsModal → StateTransitionModal → AddProductForm → RecipientDetailPanel → ProductDetailPanel → Delete-item → Start-return → RecipientPickerModal)
4. Esc Priority Chain
5. Cross-Modal Interaction Map
6. Known Pitfalls
7. Don't Hand-Roll table
8. Update Log

---

## No Analog Found

| File | Role | Reason |
|---|---|---|
| None | — | All patterns are drawn from existing `.planning/` docs and RESEARCH.md raw material |

The RecipientPickerModal (9th overlay, referenced at dashboard.slint:1362) has no entry in RESEARCH.md (marked as Assumption A1 — not yet audited). The planner should include reading `crates/app/ui/recipient-detail.slint` (or whichever file defines RecipientPickerModal) as an explicit task before writing that section of MODAL-STATE.md. If no separate file exists, it is inline in dashboard.slint and should be documented as such (same pattern as delete-item and start-return inline modals).

---

## Metadata

**Analog search scope:** `.planning/RETRO-AGENT-FAILURE-PATTERNS.md`, `.planning/DATA-FLOW.md`, `.planning/MODAL-STATE.md` (placeholder), `code_tips/CALLBACK_PIPELINE.md`, `.planning/phases/20.1.1.1.1-*/20.1.1.1.1-PATTERNS.md`
**Files scanned:** 6 planning/doc files
**Source files with raw modal data (read-only):** 10 (all listed in CONTEXT.md canonical_refs)
**Pattern extraction date:** 2026-04-16
