---
phase: 17
slug: gh-issues-client-and-product-catalog
status: draft
shadcn_initialized: false
preset: none
created: 2026-03-24
---

# Phase 17 — 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 web) |
| Preset | not applicable |
| Component library | Slint built-ins (Rectangle, Text, TouchArea, Image, PopupWindow, TextInput) |
| Icon library | Unicode characters inline in Text elements (existing project pattern) |
| Font | System default via Slint (no explicit font declaration in tokens.slint) |

> Source: tokens.slint, option-grid.slint, card.slint — confirmed existing pattern. shadcn gate skipped: this is a Rust/Slint desktop app, not React/Next.js/Vite.

---

## Spacing Scale

Declared values — all multiples of 4, matching existing Slint usage:

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, avatar text padding, inline icon offsets |
| sm | 8px | Tile internal padding (HorizontalLayout padding), spacing between image + label in product tile |
| md | 16px | Default padding between sections, form field spacing |
| lg | 24px | Section padding within panels, modal content padding |
| xl | 32px | Layout-level gaps between major sections |
| 2xl | 48px | Product image thumbnail size (48x48 within tile) |
| 3xl | 64px | Not used in this phase |

Exceptions:
- Product tile height: 80px (existing ProductGrid value — not a spacing token, it is a tile dimension constant; source: option-grid.slint lines 139–190)
- Tile inter-tile gap: 12px (not a spacing token — this is a dimension constant matching the existing RecipientGrid and ProductGrid gutter; source: option-grid.slint lines 139–190; excluded from spacing token system)
- Ellipsis button touch target: 24px wide x 24px tall (matches existing card dots button dimensions — not a spacing token, it is a component dimension constant)
- Product detail panel width: 320px (fills right side of discovery area — not a spacing token, it is a layout dimension constant)

> Source: option-grid.slint lines 139–190 (ProductGrid: 80px tile height, 12px gutter, 8px padding, 48px image square)

---

## Typography

All sizes from `tokens.slint` `Typography` global — use existing tokens, do not introduce new sizes.

| Role | Size | Weight | Line Height | Usage in This Phase |
|------|------|--------|-------------|---------------------|
| Body | 13px (`Typography.size-md`) | 400 (normal) | 1.4 | Product detail panel body text, form field values |
| Label | 12px (`Typography.size-sm`) | 400 (normal) | 1.3 | Product tile name, ellipsis menu item labels, form field labels |
| Caption | 11px (`Typography.size-xs`) | 700 (bold) | 1.2 | Section headers above grid (existing pattern), recipient count badge, serial ID labels |
| Heading | 18px (`Typography.size-lg`) | 700 (bold) | 1.2 | Product detail panel title (product name), "Add Product" modal title |

> Source: tokens.slint lines 33–37. No new sizes introduced — phase uses all four existing tokens.

---

## Color

All values from `tokens.slint` `Colors` global — do not introduce new color values.

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `#1a1e2a` (`Colors.background`) | App background, discovery pane background |
| Secondary (30%) | `#242838` (`Colors.surface`) | Product tile background (default), detail panel background, form field background |
| Elevated surface | `#2a3050` (`Colors.surface-elevated`) | Product detail panel header, modal background |
| Popup surface | `#2d3348` (`Colors.surface-popup`) | Product tile hover state, ellipsis menu dropdown, Shopify auto-suggest dropdown |
| Accent (10%) | `#4a7cff` (`Colors.accent`) | Product tile hover border, "Add Product" primary button, active selection ring on product tile |
| Destructive | `#e05050` (`Colors.error`) | Archive action confirmation text, destructive menu items |

Accent reserved for:
- Product tile hover border (1px, same as RecipientGrid pattern)
- "Add Product" primary CTA button background
- Active/selected product tile ring when product is currently selected for card filtering
- Inline `+` button (create new unit) active state

Secondary semantic:
- `#c0a060` (`Colors.archive-action`) — "Archive" menu item text color (matches existing card archive color)
- `#8a92a8` (`Colors.text-muted`) — Product recipient count badge text, disabled form field text
- `#6b7590` (`Colors.text-dim`) — Empty state text, placeholder text in Shopify URL field

> Source: tokens.slint lines 1–30. 60/30/10 split approximated from existing surface hierarchy.

---

## Component Inventory

Components introduced or extended in this phase:

### 1. ProductTileData (extend existing struct in option-grid.slint)

Current fields: `name: string`, `image-hint: string`, `dimmed: bool`

Add:
- `recipient-count: int` — shown as badge below name ("3 recipients")
- `has-image: bool` — controls whether to show image placeholder or real image
- `image: image` — actual product image (empty when `has-image` is false)
- `shopify-linked: bool` — controls "View on Shopify" menu item visibility

### 2. ProductGrid (extend existing in option-grid.slint)

Current behavior: 4-column grid, 80px tiles, image placeholder + name.

Add:
- Ellipsis button (3 vertical dots "...") in top-right corner of each tile — appears on hover
- Recipient count label below product name: `{N} recipient{s}` at `Typography.size-xs`, `Colors.text-muted`
- Ellipsis menu (PopupWindow) with four items:
  1. "Set Image" — native file picker trigger
  2. "View on Shopify" — visible only when `shopify-linked` is true
  3. "Product Detail" — opens detail panel
  4. "Archive" — colored `Colors.archive-action`, triggers confirm row
- "Add Product" button — positioned below the grid, left-aligned, accent-colored
- Callback additions: `ellipsis-set-image(string)`, `ellipsis-view-shopify(string)`, `ellipsis-detail(string)`, `ellipsis-archive(string)`

### 3. ProductDetailPanel (new component — discretionary layout)

Recommended layout (right-panel or overlay in discovery area):
- Header: product image (64px x 64px) + product name at `Typography.size-lg` weight 700
- Row: Shopify URL (linked text or "Not linked")
- Row: GH Issue link (formatted as `owner/repo#N`)
- Section: "Serial Units" — list of `ww-product-unit` instances with columns: Serial ID, State, Assigned Card
- Close button top-right: "x" at `Typography.size-sm`, `Colors.text-muted`
- Empty unit list state: "No units created yet. Use + to add the first unit."

### 4. AddProductForm (new component — modal or inline panel)

Fields:
- Name (required): TextInput, placeholder "Product name"
- Shopify URL (optional): TextInput with auto-suggest dropdown
  - Auto-suggest dropdown: `Colors.surface-popup` background, items at `Typography.size-sm`
  - Each suggestion formatted: "Product Name (order #XXXX)"
- Image (optional): shown after "Set Image" is used — thumbnail preview 48x48

Buttons:
- Primary: "Add Product" — `Colors.accent` background, white text, `Typography.size-sm`
- Secondary: "Discard" — `Colors.surface-elevated` background, `Colors.text-secondary` text

### 5. ProductAddToCardPicker (new component — within existing add-item flow)

Layout: grouped list within existing add-item popup/modal:
- Each parent product: name row at `Typography.size-sm`, `Colors.text-primary`
  - Indented child rows: serial ID + state at `Typography.size-xs`, `Colors.text-muted`
  - "In Stock" units: full opacity; assigned units: 0.5 opacity, not selectable
  - Inline `+` button (right of parent row): creates new unit; colored `Colors.accent`
- Select behavior: clicking in-stock unit assigns it; clicking parent product adds generically

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Primary CTA | "Add Product" |
| Product tile empty state heading | "No products yet" |
| Product tile empty state body | "Add your first product to start tracking inventory." |
| No products match (filtered) | "No products match" (existing string in ProductGrid — keep) |
| Product detail empty units | "No units created yet. Use + to add the first unit." |
| Archive confirmation (inline on tile) | "Archive?" with "Yes" / "Cancel" — same pattern as card confirm-remove |
| Archive menu item label | "Archive product" |
| Set Image menu item label | "Set image" |
| View on Shopify menu item label | "View on Shopify" |
| Product Detail menu item label | "Product detail" |
| Shopify URL auto-suggest placeholder | "Shopify URL or product name" |
| Shopify URL auto-suggest item format | "{Product Name} (order #{N})" |
| Create new unit inline button label | "+" (bare plus, no text — consistent with existing add-item pattern) |
| AddProductForm dismiss button | "Discard" (secondary button — dismisses product creation form without saving) |
| Error: GH Issues create failed | "Could not save product to GitHub. Check your connection and try again." |
| Error: image upload failed | "Image upload failed. Check your connection and try again." |
| Sync loading state | Not user-visible — background sync, no spinner in this phase |
| Product name form validation | "Product name is required" — shown inline below field in `Colors.error` at `Typography.size-xs` |

---

## Interaction Contracts

### Product Tile Hover
- Trigger: mouse enters tile
- Show: 1px `Colors.accent` border, `Colors.surface-popup` background, ellipsis button visible
- Leave: border and ellipsis button hide, background returns to `Colors.surface`
- Source: matches existing RecipientGrid and ProductGrid hover pattern (option-grid.slint lines 158–160)

### Ellipsis Menu
- Trigger: click ellipsis button on hovered tile
- Behavior: PopupWindow appears anchored to tile top-right, z-order above all tiles
- Items listed top-to-bottom: "Set image", "View on Shopify" (conditional), "Product detail", "Archive product"
- "Archive product" text color: `Colors.archive-action`
- Dismiss: click outside popup (Slint PopupWindow default)
- Pattern source: card ellipsis menu in card.slint

### Archive Confirmation
- Trigger: "Archive product" selected from ellipsis menu
- Behavior: tile transitions to show inline "Archive?" + "Yes" + "Cancel" (same as `remove-confirming` pattern in RecipientCard)
- "Yes" fires `ellipsis-archive` callback and triggers archive animation (tile fades to 0.5 opacity then disappears on next render)
- "Cancel" returns tile to normal state

### Product Detail Panel
- Trigger: "Product detail" selected from ellipsis menu, OR click on product tile (if product is already selected/filtered)
- Behavior: panel slides in from right OR overlay appears within discovery content area — at Claude's discretion
- Close: "x" button top-right or Esc key

### Add Product Flow
- Trigger: "Add Product" button below product grid
- Behavior: modal or inline form appears (at Claude's discretion for layout)
- Shopify URL field: auto-suggest fires after user stops typing for 300ms (debounce)
- Submit: fires `create-product` callback with name + shopify_url + image_path
- Success: modal closes, new tile appears in grid
- Error: error message shown inline, form stays open
- Dismiss: "Discard" button closes the form without saving

### Product Add-to-Card Picker
- Trigger: existing "add item" flow on a card (replaces or extends current add-item UI)
- Behavior: shows grouped product list — parent products with nested in-stock units
- Select parent: adds product generically (no specific unit)
- Select in-stock unit: assigns that unit to card
- Click "+": inline unit creation — fires `create-unit` callback, new unit appears selected
- Assigned units shown at 0.5 opacity, not clickable

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — Slint project, not web |
| third-party | none | not applicable |

> No component registry used. This project uses Slint (.slint files) with hand-written components. Registry safety gate is not applicable.

---

## 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
