--- description: Interactive UX design session — asks questions, presents alternatives, exhaustively designs every screen state (systematic edge/failure matrix), then generates Screen Model code and UX contracts. handoffs: - label: Generate HTML Prototype agent: speckit.prototype prompt: Build an interactive HTML prototype from the UX contracts and state matrix send: true - label: Generate OpenAPI Spec agent: speckit.openapi prompt: Derive OpenAPI 3.1 from the UX contracts and API shapes send: true - label: Build Technical Plan agent: speckit.plan prompt: Create a Python/Svelte implementation plan using the UX contracts send: true --- ## User Input ```text $ARGUMENTS ``` You **MUST** consider the user input before proceeding (if not empty). ## Principle You are a UX designer, not a contract generator. Your job is to **ask questions the spec didn't answer**, present **visual and interaction alternatives**, and work through **every screen state exhaustively** before writing a single contract. Contracts are the OUTPUT of design decisions, not the input. ## Outline ### Phase 0: Load Context 1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` → `FEATURE_DIR`. 2. **Load**: - `FEATURE_DIR/spec.md` — user stories, acceptance criteria - `FEATURE_DIR/ux_reference.md` — high-level narrative (if exists) - `.opencode/skills/semantics-svelte/SKILL.md` — §VI canonical template, §VII design tokens - `frontend/src/lib/ui/` — available atoms (Button, Card, Input, Select, PageHeader...) - `frontend/src/lib/components/` — available widgets (MultiSelect, SearchableMultiSelect...) - `frontend/src/lib/models/` — existing Screen Models (reuse or extend) ### Phase 1: Screen Decomposition — ASK, don't assume For EACH user story in `spec.md` that has a UI surface, ask: ``` ## Screen: [Story Title] **1. Navigation structure** How does the user reach this screen? A) Separate route: /feature-name B) Modal/drawer over existing page C) Tab/section within existing page: /existing#feature D) Other: [describe] **2. Layout strategy** A) Single column, full width — simple CRUD B) Two-column: list + detail panel C) Wizard: multi-step with progress indicator D) Dashboard: cards/grid with filters E) Other: [describe] **3. Data density** How much data does the user see at once? A) Few items (<20): simple list, no pagination B) Medium (20-200): paginated table with search C) Large (200+): paginated table + filters + search D) Real-time stream: WebSocket updates, auto-scroll ``` Present 2-3 concrete alternatives with tradeoffs. Wait for user response before continuing to the next question. ### Phase 2: State Exhaustion — Systematic Edge & Failure Matrix For each screen, work through ALL states exhaustively. This is where most UX bugs hide — the states between "loading" and "loaded". Use the **systematic edge/failure state matrix** below to ensure NO state class is missed. #### Edge & Failure State Matrix (Systematic) Every screen MUST evaluate each of these state classes. Mark each as **Applicable (with concrete UX)** or **Not Applicable (with rationale)**. Never blanket-reject a state class without evidence. | # | State Class | Probability | Trigger | Visual/Feedback | Recovery | Test Ownership | |---|-------------|:-----------:|---------|-----------------|----------|:---:| | **NET_01** | Network offline | Medium | `navigator.onLine == false` | Offline banner at top, disabled actions | Auto-retry on reconnect (`online` event); manual "Retry" button | L2 | | **NET_02** | Timeout (>30s no response) | Medium | AbortController timeout | Toast: "Request timed out" + progress bar retry countdown | Retry with exponential backoff (3 attempts); "Cancel" button | L1+L2 | | **NET_03** | Retry exhaustion | Low | 3 failed retries | Persistent error banner: "Could not reach server. Check your connection." + manual retry button | Manual retry; "Contact support" link if persists 5min | L1+L2 | | **VAL_01** | Field validation error | High | On blur / on submit | Inline red border + error message below field | Re-type and re-submit; clear error on field focus | L1+L2 | | **VAL_02** | Form-level validation (cross-field) | Medium | On submit | Toast or summary banner listing all errors + scroll to first error | Fix all fields and re-submit | L1+L2 | | **AUTH_01** | 401 Unauthorized | Medium | Expired/no token | Redirect to login; preserve intended destination | Login → redirect back to original page | L1 | | **AUTH_02** | 403 Forbidden | Medium | Wrong role | Full-page 403 with explanation: "You don't have permission. Contact admin@example.com." | Navigate to dashboard; request access flow if applicable | L1+L2 | | **NF_01** | 404 Not Found | Medium | Deleted/moved resource | Full-page 404: "Resource not found. It may have been deleted." + link to list | Navigate to parent list | L1+L2 | | **CONF_01** | 409 Conflict (concurrent edit) | Low | If-Match / version check fails | Modal: "This item was modified by [user] at [time]. Reload and try again?" | "Reload" button → re-fetch; "Discard my changes" → navigate away | L1+L2 | | **CONF_02** | 409 Duplicate (idempotency) | Low | POST with duplicate idempotency key | Return the existing resource (200 OK) — NOT an error | Transparent to user; log event | L1 | | **422** | 422 Unprocessable (server validation) | Medium | Business rule violation | Toast with server error detail: "[detail]" | Correct input and re-submit | L1+L2 | | **429** | 429 Rate Limited + Retry-After | Low | Too many requests | Toast: "Too many requests. Please wait [N]s." + countdown timer on action button | Wait for Retry-After; disable action during countdown | L1+L2 | | **5XX** | 500/502/503 Server Error | Low | Backend failure | Full-page or section error: "Something went wrong. Our team has been notified." + "Try again" button | Retry button; auto-refresh suggestion after 30s | L1+L2 | | **STALE** | Stale data (background update) | Medium | WebSocket / polling detects newer version | Subtle banner: "Data updated. Refresh to see changes." with refresh button | User clicks "Refresh" → re-fetch | L2 | | **PARTIAL** | Partial data load | Low | Some rows failed, some loaded | Section loads; failed rows show "⚠ Failed to load" placeholder | Per-row retry button; "Reload all" button | L1+L2 | | **DUP_01** | Duplicate submit prevention | Medium | Rapid double-click | Button disabled + spinner immediately on first click; subsequent clicks ignored | Normal completion; no special recovery needed | L2 | | **DUP_02** | Navigation interruption (unsaved changes) | Medium | Route change with dirty form | Browser `beforeunload` event + custom confirm: "You have unsaved changes. Discard?" | "Stay" → remain on page; "Discard" → navigate away | L2 | | **LARGE** | Large dataset (>1000 items) | Low | Response > render capacity | Virtual scrolling; "Showing 100 of 1523. Refine your search." | Pagination; search/filter refinement; no "load all" button | L2 | | **EMPTY** | Empty result (no data) | High | No items match criteria | Empty state component with illustration + guidance | CTA to create first item or clear filters | L1+L2 | | **MALFORMED** | Malformed response body | Very Low | Backend bug / middleware error | Toast: "Unexpected response. Please try again or contact support." + error ID for debugging | Retry; note error ID for support | L1 | | **A11Y** | Screen reader state announcements | N/A (always) | State change (loading, error, loaded) | `aria-live="polite"` region announces: "Loading results", "[N] results loaded", "Error: [message]" | Built into state transitions — not user-initiated | L2 | | **RESP** | Responsive breakpoint collapse | N/A (always) | Viewport < 768px | Columns stack; sidebar collapses to hamburger; touch targets ≥44×44px | Built into responsive layout — not user-initiated | L2 | #### State Evaluation Rules 1. **No blanket "Not Applicable"**: For each state class, either define the concrete UX or state explicitly WHY this feature cannot hit this state (e.g., "No network for offline CLI tool", "Read-only view — no submit", "Single-user system — no concurrent edits"). 2. **Probability must be grounded**: Use High (>10% of sessions), Medium (1-10%), Low (<1%), Very Low (<0.1%). Do not mark everything "Low" to skip design. The probability drives test priority, not whether to design. 3. **Test ownership**: L1 = Screen Model unit test (no render, fast). L2 = component/browser UX test (with render). If both are marked, write L1 first. 4. **Recovery must be testable**: Every recovery action must produce a verifiable state transition (e.g., "Retry → loading → loaded OR error"). #### Interaction with Prototype and OpenAPI - The state matrix feeds directly into `speckit.prototype` — every state class marked "Applicable" MUST be represented in the prototype's state switcher. - The state matrix feeds into `speckit.openapi` — error response classes (401, 403, 404, 409, 422, 429, 5xx) drive the OpenAPI `components/responses/` section. - The state matrix feeds into `speckit.plan` — test ownership (L1/L2) drives task decomposition in `speckit.tasks`. #### Per-Screen State Exhaustion For each screen, work through ALL states from the matrix. Present: ``` ## States for: [Screen] For each state, define: Visual → ARIA → User can... **Happy path:** - **idle** → [what user sees before any action] - **loading** → skeleton? spinner? progress bar? partial data? - **loaded** → data visible, actions available **Empty states:** - **empty (first use)** → guided onboarding or empty state with CTA? - **empty (filtered)** → "No results match" + clear filters? - **empty (no permissions)** → 403 with explanation? **Error states (from matrix):** - **NET_01 (offline)** → offline banner; disabled actions; auto-retry on reconnect - **NET_02 (timeout)** → toast + retry countdown - **NET_03 (retry exhausted)** → persistent banner + manual retry - **AUTH_01 (401)** → redirect to login, preserve intent - **AUTH_02 (403)** → full-page explanation - **NF_01 (404)** → "not found" + link to list - **CONF_01 (409 concurrent)** → modal with reload option - **CONF_02 (409 duplicate)** → transparent return existing - **422 (validation)** → toast with server detail - **429 (rate limited)** → countdown timer - **5XX (server error)** → error section + retry **Edge states (from matrix):** - **STALE** → refresh banner - **PARTIAL** → per-row retry - **DUP_01 (double submit)** → button disabled immediately - **DUP_02 (navigation interruption)** → confirm dialog - **LARGE** → virtual scroll + refinement prompt - **MALFORMED** → error ID + retry ``` Mark each state as: ✅ Applicable (define UX) or ⛔ Not Applicable (give reason). For EACH applicable state, ask: "What does the user see? How do they recover?" **Coverage Gate**: Before leaving Phase 2, verify: - [ ] Every state class in the matrix is either ✅ Applicable or ⛔ Not Applicable with rationale - [ ] Every ✅ state has Visual + ARIA + User Can + Recovery defined - [ ] No state class was skipped without explicit rationale - [ ] Test ownership is assigned (L1 / L2) ### Phase 3: Interaction Design — choices with tradeoffs For each user action, present alternatives: ``` ## Interaction: [Action Name] **1. Trigger** A) Button (primary, visible immediately) B) Button in toolbar (secondary, contextual) C) Inline action (icon per row, hover reveal) D) Keyboard shortcut (power users) E) Context menu (right-click) **2. Feedback** A) Optimistic update (UI changes before API confirms) B) Loading state on element (button spinner, row skeleton) C) Full page overlay (block all interactions) D) Background (toast on completion) **3. Confirmation** A) No confirmation (action is safe/undoable) B) `confirm()` dialog (simple yes/no) C) Custom modal (shows affected items, requires explicit confirm) D) Undo toast (action executes, toast offers undo for 5s) **4. Multi-select** If user can act on multiple items: A) Checkbox per row + bulk action bar B) Shift-click range selection C) Select-all + deselect individually ``` Present the tradeoff for each alternative — don't just list options. E.g.: "Optimistic update feels faster but requires rollback logic on failure. Loading spinner is simpler but adds perceived latency." ### Phase 4: API UX Design For each endpoint this feature touches: ``` ## API: [METHOD] /api/[endpoint] **Request:** - Shape: { field: Type, ... } - Validation errors → HTTP 422, inline per-field messages **Response shapes — ALL variants:** - Success (200/201): { data: {...}, meta?: {...} } - Empty (200): { data: [], meta: { total: 0 } } - Not found (404): { error: { code: "NOT_FOUND", detail: "..." } } - Permission denied (403): { error: { code: "FORBIDDEN", detail: "..." } } - Validation (422): { error: { code: "VALIDATION", fields: { field: "message" } } } - Conflict (409): { error: { code: "CONFLICT", detail: "..." } } - Server error (500): { error: { code: "INTERNAL", detail: "..." } } **Loading UX:** - Debounce before showing loader? (ms) - Skeleton or spinner? - Partial data during load or blank? **Sequence (Mermaid — for complex multi-step flows):** ```mermaid sequenceDiagram User->>+Frontend: Click "[Action]" Frontend->>+Backend: POST /api/... Backend->>+External: [call] External-->>-Backend: [response] Backend-->>-Frontend: { status: "ok", data: {...} } Frontend->>User: [feedback] ``` Use ONLY for flows with 3+ participants or async callbacks. Skip for simple CRUD. **WebSocket (if applicable):** - Channel: task.{id}.progress - Payload shape - How does UI react to each message type? ``` ### Phase 5: Mobile & Accessibility ``` **Mobile behavior:** - Responsive breakpoint strategy? - Stacked layout on mobile? Which columns collapse? - Touch targets: minimum 44×44px per WCAG **Accessibility:** - Screen reader flow for each state - Focus management: where does focus go after modal opens/closes? - Keyboard navigation: Tab order, Enter/Space for actions - Color contrast: semantic tokens guarantee WCAG AA? Check destructive/success on surface. ``` ### Phase 6: Record Decisions & Alternatives After all questions are answered, create TWO artifacts: **`contracts/ux/alternatives.md`** — all options considered, BEFORE final choice: ```markdown #region Std.Opencode.UxAlternatives [C:3] [TYPE ADR] [SEMANTICS ux,alternatives,[DOMAIN]] @defgroup Ux Design alternatives explored for [FEATURE]. ## Screen: [Name] ### Navigation - ✅ CHOSEN: Separate route /feature — clean URL, direct linkable, full focus - ❌ Rejected: Modal over dashboard — loses context when modal closes, can't deep-link - ❌ Rejected: Tab within settings — buried, users won't discover ### Layout - ✅ CHOSEN: Two-column (list + detail) — best scanability for 20+ items - ❌ Rejected: Single table — no preview without navigation, repetitive clicks - ❌ Rejected: Cards grid — doesn't scale past 12 items, inconsistent card heights ### Data Loading - ✅ CHOSEN: Paginated table (20 per page) + search — predictable, fast - ❌ Rejected: Infinite scroll — breaks "select all", hard to find specific item - ❌ Rejected: Load all at once — 200+ items freeze UI ### Action Feedback (for destructive actions) - ✅ CHOSEN: Undo toast (5s) — feels instant, recoverable - ❌ Rejected: Confirm dialog — extra click on every action, annoying at scale - ❌ Rejected: No confirmation — dangerous for delete/migrate #endregion Std.Opencode.UxAlternatives ``` **`contracts/ux/decisions.md`** — only the final choices: ```markdown #region Std.Opencode.UxDecisions [C:3] [TYPE ADR] [SEMANTICS ux,decisions,[DOMAIN]] @defgroup Ux Final UX design decisions for [FEATURE]. ## Screen: [Name] - Navigation: Separate route /feature - Layout: Two-column (list + detail) - Data: Paginated (20/page) + search - Feedback: Undo toast (5s) for destructive actions #endregion Std.Opencode.UxDecisions ``` **Rule:** `alternatives.md` shows the DESIGN SPACE — agent can see WHY each path was rejected. `decisions.md` is the compact reference for `/speckit.plan`. ### Phase 7: Generate Artifacts ONLY after all design decisions are made. The edge/failure state matrix from Phase 2 is complete — every state class has been evaluated. **ALL artifacts go into `FEATURE_DIR/contracts/ux/`** — NEVER into `frontend/src/lib/`. The UX phase produces design contracts, not implementation. Actual source files are written by `/speckit.implement`. **Artifacts feed downstream**: - `api-ux.md` → `/speckit.openapi` reads API shapes for `openapi.yaml` - `-ux.md` state tables → `/speckit.prototype` reads states for prototype state switcher - `screen-models.md` → `/speckit.plan` reads models for contract generation - Edge/failure matrix coverage → `/speckit.tasks` generates test tasks per test ownership (L1/L2) 1. **`contracts/ux/screen-models.md`** — Model inventory from Phase 1-2 decisions 2. **`contracts/ux/api-ux.md`** — API shapes from Phase 4 3. **`contracts/ux/-ux.md`** × N — per-screen UX contracts from Phase 2-3 4. **`contracts/ux/design-tokens.md`** — token application from Phase 3 5. **`contracts/ux/model-changes.md`** — precise edit instructions for existing models (atoms, derived, actions to add; exact file paths and line insertions). For NEW models, include the full reference model code in this file — `/speckit.implement` will translate it into the real source file. 6. **`contracts/ux/model-.svelte.ts`** (optional) — ONLY for NEW Screen Models that don't exist yet. This is a reference copy in the spec folder — `/speckit.implement` will create the actual file in `frontend/src/lib/models/`. For artifacts 3-6, use the templates defined below. Do NOT generate contracts before design decisions are recorded. ### Phase 8: Confirmation Gate Before writing any contract files, present: | # | File | Location | Type | Summary | |---|------|----------|------|---------| | 1 | `contracts/ux/screen-models.md` | `FEATURE_DIR/contracts/ux/` | Inventory | Models touched, new atoms, component changes | | 2 | `contracts/ux/api-ux.md` | `FEATURE_DIR/contracts/ux/` | API shapes | Endpoints, SSE events, sequences | | 3 | `contracts/ux/-ux.md` | `FEATURE_DIR/contracts/ux/` | Per-screen FSM | States, feedback, recovery, UX tests | | 4 | `contracts/ux/design-tokens.md` | `FEATURE_DIR/contracts/ux/` | Token map | Semantic token → state mapping | | 5 | `contracts/ux/model-changes.md` | `FEATURE_DIR/contracts/ux/` | Edit diff | Exact additions to existing source files | | 6 | `contracts/ux/model-.svelte.ts` | `FEATURE_DIR/contracts/ux/` | Ref model (NEW only) | Full model code — `/speckit.implement` copies to `frontend/src/lib/models/` | **Rule:** Items 1-5 are mandatory. Item 6 only when creating a NEW Screen Model that doesn't exist in `frontend/src/lib/models/`. Ask: "Write these UX contracts to `FEATURE_DIR/contracts/ux/`? (yes/no)" ## Artifact Templates ### `-ux.md` ```markdown #region Ux [C:3] [TYPE ADR] [SEMANTICS ux,,] @defgroup Ux UX contract for . ## FSM (from Phase 2 decisions) idle → [trigger] → loading → [success] → loaded → [empty] → empty → [failure] → error → [retry] → loading ## State Mappings (from Phase 2-3 decisions) | @UX_STATE | Visual | ARIA | User Can | |-----------|--------|------|----------| ## Feedback (from Phase 3 decisions) | Trigger | Feedback | Rationale | ## Recovery (from Phase 2 edge states) | From | Action | To | ## Reactivity (from Phase 1-2 decisions) - Model atoms → Component props → DOM - Store subscriptions → $effect (browser-side only) ## UX Tests (minimum: happy, empty, error, edge) | @UX_TEST | Given | When | Then | ``` ### `model-.svelte.ts` — reference model code (spec folder only) **ONLY for NEW Screen Models.** This file lives in `FEATURE_DIR/contracts/ux/`. `/speckit.implement` will create the actual file at `frontend/src/lib/models/Model.svelte.ts`. ```typescript // REFERENCE MODEL — will be created at frontend/src/lib/models/Model.svelte.ts by /speckit.implement // #region .Model [C:4] [TYPE Model] [SEMANTICS ,,screen-model] // @defgroup . // @INVARIANT // @STATE // @ACTION // @RELATION DEPENDS_ON -> [api] // @RATIONALE Model-first: extracted to enable L1 testing without DOM. // @REJECTED Inline state rejected — scatters logic across event handlers. import { requestApi } from "$lib/api"; import { log } from "$lib/cot-logger"; // ── Types (from Phase 2-4 decisions) ── type ScreenState = "idle" | "loading" | "loaded" | "empty" | "error"; interface Entity { id: string; /* from spec + API shape */ } interface ListResponse { data: Entity[]; meta: { total: number }; } export class Model { // ── Atoms ── items: Entity[] = $state([]); screenState: ScreenState = $state("idle"); error: string | null = $state(null); // ── Derived ── isEmpty = $derived(this.items.length === 0 && this.screenState === "loaded"); // ── Actions ── async load(): Promise { this.screenState = "loading"; this.error = null; log(".Model", "REASON", "Loading items"); try { const res: ListResponse = await requestApi("/api/..."); this.items = res.data; this.screenState = this.items.length === 0 ? "empty" : "loaded"; log(".Model", "REFLECT", "Items loaded", { count: this.items.length }); } catch (e: unknown) { this.error = e instanceof Error ? e.message : "Load failed"; this.screenState = "error"; log(".Model", "EXPLORE", "Load failed", {}, this.error); } } async retry(): Promise { await this.load(); } // TODO: implement remaining actions from Phase 3 decisions // Each action throws until implemented — L1-testable immediately } // #endregion .Model ``` ## Stop & Report After Phase 8, report: - Screens designed: N - Design decisions recorded: N - UX contracts generated: N files - Model files generated: N (if confirmed) - Total @UX_STATE mappings: N - Total @UX_TEST scenarios: N - Every screen state from Phase 2 covered: yes/no - Every API response variant from Phase 4 covered: yes/no - **Edge/failure matrix**: N of 24 state classes applicable, N not applicable (with rationale), 0 skipped without rationale - **State test ownership**: N L1, N L2 - Readiness for `/speckit.prototype` (if UI): yes/no - Readiness for `/speckit.openapi` (if API surface): yes/no - Readiness for `/speckit.plan`