---
phase: 12.1.1
slug: shopify-first-card-source-pipeline-with-recipient-matching-and-unassigned-card-ux
status: draft
shadcn_initialized: false
preset: none
created: 2026-03-21
---

# Phase 12.1.1 — UI Design Contract

> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none — Slint UI framework (not React/Next.js; shadcn not applicable) |
| Preset | not applicable |
| Component library | Slint built-in widgets + custom components |
| Icon library | Unicode glyphs inlined as Text elements (project convention from STATE.md) |
| Font | Slint default (system sans-serif) |

**Source:** RESEARCH.md Standard Stack, CONTEXT.md code context, existing `.slint` file audit.

---

## Spacing Scale

Declared values (multiples of 4, matching existing Slint component padding patterns):

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline element padding |
| sm | 8px | Compact element spacing, row padding within modal list items |
| md | 16px | Default element spacing, modal internal padding |
| lg | 24px | Section padding, modal body padding |
| xl | 32px | Layout gaps between major modal regions |
| 2xl | 48px | Reserved (not used in this phase) |
| 3xl | 64px | Reserved (not used in this phase) |

Exceptions: Modal search input height 32px (matches existing LookupModal input height from lookup-modal.slint line 148). List item row height 36px (matches existing result-touch row pattern). Modal panel min-width 400px.

**Source:** Derived from existing card.slint and lookup-modal.slint layout measurements.

---

## Typography

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 13px | 400 | 1.4 |
| Label | 11px | 400 | 1.3 |
| Heading | 16px | 600 | 1.2 |

**Body (13px/400):** Card text, modal list item names, search input text, picker list recipient names.
**Label (11px/400):** Modal helper text, status labels, warning sub-labels. If 11px proves too visually close to body (13px) during implementation, step up to 12px.
**Heading (16px/600):** Modal title "Pick Recipient for {Name}", section headers.

**Source:** Extracted from card.slint (13px body, 11px label), lookup-modal.slint (16px/600 modal title at line 83-84).

### Unassigned Name — Special Typography Treatment

The unassigned card's name field (currently renders recipient name at 13px/400 in #e0e4ef) must render differently:

| Property | Value |
|----------|-------|
| Font size | 13px (same as body — no change to card layout) |
| Font weight | 400 (unchanged) |
| Font style | italic |
| Color | #f0a030 (warning amber — matches existing stale-badge and amber warning color from card.slint line 407 and dashboard.slint line 258) |

**Source:** CONTEXT.md locked decision: "Name field shows the Shopify customer first+last name in italics + warning color." Warning color #f0a030 confirmed from existing usage in card.slint (stale badge, line 407) and dashboard.slint (connection-status amber, line 258).

---

## Color

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | #1a1e2a | Window background, search input backgrounds, modal overlay base |
| Secondary (30%) | #242838 | Card surfaces, modal panel background |
| Accent (10%) | #4a7cff | Interactive buttons (Save/Create), focused input borders, add icon |
| Warning | #f0a030 | Unassigned card name (italic), "Pick Recipient" menu item text |
| Destructive | #e05050 / #ef5350 | Destructive actions only (clear token, remove item) |

Accent reserved for: Save button, Create New Recipient button, focused input border highlight. Not used for unassigned state (warning color is used instead).

Warning (#f0a030) reserved for: Unassigned card name text (italic), "Pick Recipient" ellipsis menu item text. These are the only two elements using warning color in this phase.

**Source:** All hex values confirmed from existing `.slint` file audit. CONTEXT.md decision: "'Pick Recipient' appears in the ellipsis menu with warning color text (matching the italic name color)."

### Recipient Picker Modal Colors

| Element | Color | Source |
|---------|-------|--------|
| Backdrop overlay | #00000066 | Matches lookup-modal.slint line 41 |
| Modal panel background | #242838 | Matches lookup-modal.slint line 69 |
| Search input background | #1a1e2a | Matches lookup-modal.slint line 148 |
| Search input border (default) | #3a4060 | Matches lookup-modal.slint line 150 |
| Search input border (focused) | #4a7cff | Matches settings-modal.slint line 94 |
| List item hover background | #2d3348 | Matches lookup-modal.slint line 201 |
| List item text | #e0e4ef | Matches card.slint line 114 |
| Placeholder text | #6b7590 | Matches lookup-modal.slint line 181 |
| "Create New Recipient" text | #4a7cff | Matches lookup-modal.slint line 260 |
| "Create New Recipient" icon | #4a7cff | Matches lookup-modal.slint line 248 |
| Close button icon | #8a92a8 | Matches lookup-modal.slint line 112 |

---

## Component Inventory

### New Components This Phase

#### 1. RecipientPickerModal (new — `recipient-picker.slint`)

Full-overlay modal following the LookupModal pattern (Rectangle backdrop, not PopupWindow — per RESEARCH.md Pattern 5 rationale: PopupWindow lacks semi-transparent dimming).

**Layout:**
- Full-screen semi-transparent backdrop: #00000066
- Centered panel: min-width 400px, max-height 480px, background #242838, border-radius 8px
- Panel sections top-to-bottom:
  1. Header row (modal title + close button)
  2. Search input
  3. Scrollable recipient list
  4. "Create New Recipient" row (pinned to bottom of list)

**Modal title:** "Pick Recipient for {unassigned-customer-name}" — 16px/600/#ffffff

**Close button:** Icon-only (Unicode "x" glyph, #8a92a8). No visible text label. Accessibility note: Slint does not support ARIA labels; the button's visual proximity to the modal title and its standard top-right position provide sufficient context. Consistent with the existing LookupModal close button pattern.

**Search input:**
- Height 32px, background #1a1e2a, border #3a4060 (default), #4a7cff (focused)
- Placeholder: "Search recipients..." — 13px/400/#6b7590
- Clears on open (deterministic clean state, per Phase 9 pattern)
- Filters list by case-insensitive substring match on recipient name

**Recipient list:**
- Each row: 36px height, horizontal padding 8px
- Hover: background #2d3348
- Recipient name text: 13px/400/#e0e4ef
- List shows ONLY recipients where Shopify Profile URL is empty/unset (per CONTEXT.md)

**"Create New Recipient" row (pinned bottom):**
- Separator line above: 1px #3a4060
- Row height 36px, hover: background #2d3348
- Icon: "+" in 18px/#4a7cff, text: "Create New Recipient" in 13px/400/#4a7cff
- Opens Create form inline (same modal, replaces list view — matching LookupModal show-create-form pattern)

**Create form fields:**
- Name field: pre-filled with Shopify customer first+last name, editable
- Shopify Profile URL field: pre-filled, read-only display (not editable by user)
- "Create & Assign" button: background #4a7cff, hover #3a6aee, text "Create & Assign" 13px/600/#ffffff
- "Back to List" button: background #2d3348, hover #3a4060, text "Back to List" 13px/400/#8a92a8

**Empty search state (no matches):**
- Text: "No unlinked recipients found." — 12px/400/#8a92a8, centered in list area
- "Create New Recipient" row still visible below

**Callbacks declared on DashboardWindow:**
- `pick-recipient-clicked(card-index: int)` — opens modal for card at index
- `assign-recipient(card-index: int, recipient-key: string)` — user selected existing recipient
- `create-and-assign-recipient(card-index: int, name: string)` — user created new recipient

#### 2. Unassigned Card Name Treatment (modification — `card.slint`)

The existing recipient-name Text element gains conditional styling:

- `font-italic: root.is-unassigned` (Slint property — use `font-italic` if available, else use a separate italic Text element)
- `color: root.is-unassigned ? #f0a030 : #e0e4ef`
- Text value: `root.is-unassigned ? root.unassigned-customer-name : root.recipient-name`

Note: Slint does not support `font-italic` as a direct property on Text. Implementation must use a conditional Text element swap: one normal Text (existing) and one italic Text (new), with only one visible at a time based on `is-unassigned` bool. This matches the Slint constraint pattern documented in MEMORY.md (no per-corner border-radius, no font-italic shorthand).

**Workaround for Slint italic:** Slint `Text` element supports `font-italic: bool`. Verify support in the project's Slint version before conditional Text swap approach. Use `font-italic: root.is-unassigned;` directly first; fall back to two-element visibility swap only if compiler rejects it.

#### 3. "Pick Recipient" Ellipsis Menu Item (modification — `card.slint`)

Added as the first item in the ellipsis menu (before DM, Shopify links), visible only when `root.is-unassigned == true`.

- Text: "Pick Recipient" — 12px/400/#f0a030 (warning color per CONTEXT.md)
- Hover background: #3a4060 (matches other menu items)
- Triggers `pick-recipient-clicked` callback
- Separator line (1px #4a5578 per existing separator pattern) after this item when visible

#### 4. CardData Struct Extension (modification — `dashboard.slint`)

Two new fields added to `CardData` struct:
- `is-unassigned: bool` — default false
- `unassigned-customer-name: string` — default "" (empty string)

---

## Interaction States

### Recipient Picker Modal — State Machine

| State | Visual |
|-------|--------|
| Closed | Modal not rendered (is-visible: false) |
| Open (list view) | Backdrop visible, list showing filtered/all recipients |
| Open (empty search) | "No unlinked recipients found." text, Create row still shown |
| Open (create form) | Create form with pre-filled fields replaces list |
| Assigning (in-flight) | Modal stays open, list non-interactive (no spinner required — assignment is synchronous for local update; GH write-back is fire-and-forget) |
| Success | Modal closes, card name updates immediately to assigned recipient name |
| Write-back error | Modal already closed; toast shown: "Assignment saved locally. Failed to update GitHub Project." |

### Assignment Write-Back Error Toast

- Text: "Assignment saved locally. Failed to update GitHub Project."
- Duration: 5000ms (matches archive toast duration, per STATE.md Phase 07-02 decision)
- Position: same as existing toast (top-right overlay)
- Color: #f0a030 background (warning, not destructive — the assignment succeeded locally)

### Unassigned Card Persistent State

- Unassigned cards sort into the same status groups as assigned cards (CONTEXT.md decision — no separate section)
- "Pick Recipient" menu item only appears when `is-unassigned == true`
- After assignment: card immediately re-renders with assigned recipient name (non-italic, #e0e4ef)
- After assignment: "Pick Recipient" menu item disappears from ellipsis menu

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Primary CTA (assign existing) | "Pick Recipient" |
| Primary CTA (create + assign) | "Create & Assign" |
| Modal title | "Pick Recipient for {Shopify Customer Name}" |
| Search placeholder | "Search recipients..." |
| Empty search state | "No unlinked recipients found." |
| Create form name label | "Recipient Name" |
| Create form URL label | "Shopify Profile URL" |
| Create form URL hint | "Auto-filled from order" |
| Back button | "Back to List" |
| Success (no toast needed) | — (card updates silently) |
| Write-back error toast | "Assignment saved locally. Failed to update GitHub Project." |
| Unassigned card name | {Shopify customer first + last name} (italic, #f0a030) |
| Ellipsis menu item | "Pick Recipient" (warning color) |
| Empty card grid (no Shopify orders) | "No orders found. Check your Shopify connection in Settings." |
| Sync error state (Shopify unreachable) | Existing connection-status indicator handles this — no new card-level copy needed |

**Destructive actions in this phase:** None. Assignment is reversible via next sync. Create New Recipient creates a GH Project row (not destructive). No delete/archive actions introduced.

**Source:** CONTEXT.md locked decisions for modal title, menu item text, and card name treatment. Remaining copy derived from existing patterns (settings-modal, lookup-modal).

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — project uses Slint, not React |
| third-party | none | not applicable |

No component registries used. All UI is Slint custom components extending existing project patterns.

---

## Phase-Specific Constraints

### Slint Limitations to Account For

Per MEMORY.md and existing codebase patterns:

1. **No `font-italic` shorthand:** Verify Slint version support before using `font-italic: bool`. If unsupported, use two Text elements with `visible` toggle.
2. **No per-corner border-radius:** Use uniform `border-radius` on all modal/card rectangles.
3. **Full-card TouchArea z-order:** In RecipientPickerModal list rows, declare TouchArea last in each row component so it receives hover events above all child Text elements.
4. **PopupWindow not used:** RecipientPickerModal uses full Rectangle backdrop (not PopupWindow), matching the LookupModal pattern, to support semi-transparent dimming.
5. **Modal state reset:** All modal state (search text, list scroll position, create-form visibility) must be cleared at every open/close site — matching the 5-site clearing rule from Phase 9 decisions.

### Sync Pipeline UI Coupling

The Shopify-first pipeline changes what data arrives via `SyncUpdateCallback`. The UI contract for card rendering remains identical to existing cards — only the `is-unassigned` bool and `unassigned-customer-name` string are new. All other CardData fields (status, date, items, note, image) behave identically for unassigned cards.

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
