docs(04): UI design contract

This commit is contained in:
lorentz 2026-05-03 17:45:42 -04:00
parent 16bc4aa5c9
commit bf780790d5

View file

@ -50,7 +50,8 @@ Declared values (multiples of 4). Pulled from existing mobile page patterns and
|-------|-------|---------------------|
| xs | 4px | Icon gaps (`gap-1`), badge padding (`px-1.5 py-0.5`) |
| sm | 8px | Row internal gaps (`gap-2`), filter chip gaps (`gap-2`) |
| md | 16px | Horizontal page padding (`px-4`), filter panel `px-4` |
| sm+ | 12px (3 × 4) | Secondary stacking, filter strip `pb-3` |
| md | 16px | Horizontal page padding (`px-4`), filter panel `px-4`, list row vertical padding (`py-4`) |
| lg | 24px | Vertical section breaks between filter header and list |
| xl | 32px | Empty-state vertical padding (`py-8`) |
| 2xl | 48px | Empty-state with no-results full-screen centering (`py-12`) |
@ -65,13 +66,15 @@ Exceptions: 4px-wide priority stripe is a visual mark, not an interactive target
All sizes are Tailwind defaults read from `DESIGN.md §2` and matched to the existing `app/mobile/tickets/page.tsx` patterns.
Two weights only: `font-normal` (400) and `font-semibold` (600). `font-medium` (500) is not used in this phase.
| Role | Size class | Weight | Line Height | Font | Usage |
|------|-----------|--------|-------------|------|-------|
| Row title | `text-sm` (14px) | `font-semibold` (600) | `leading-snug` (1.375) | IBM Plex Sans | Ticket title in list row — 1-line truncate |
| Body / secondary | `text-xs` (12px) | `font-normal` (400) | `leading-normal` (1.5) | IBM Plex Sans | Company name, age, assignee in row |
| Ticket number / ID | `text-[10px]` (10px) | `font-mono` / `font-normal` (400) | `leading-normal` | IBM Plex Mono | Ticket # badge in row (`font-mono`) |
| Detail breadcrumb | `text-sm` (14px) | `font-medium` (500) | `leading-normal` | IBM Plex Sans | "Tickets / #T20250001" in detail header |
| Filter label | `text-xs` (12px) | `font-medium` (500) | `leading-none` | IBM Plex Sans | Chip labels (Open, High, etc.) |
| Detail breadcrumb | `text-sm` (14px) | `font-semibold` (600) | `leading-normal` | IBM Plex Sans | "Tickets / #T20250001" in detail header |
| Filter label | `text-xs` (12px) | `font-semibold` (600) | `leading-none` | IBM Plex Sans | Chip labels (Open, High, etc.) |
| Count / status line | `text-xs` (12px) | `font-normal` (400) | `leading-normal` | IBM Plex Sans | "N open tickets" beneath filter strip |
Heading note: No `text-2xl` page title is rendered by the list page — the shell HeaderBar owns the brand mark; the list page opens directly with the filter strip. The detail page retains its existing `text-base font-bold` ticket title (body is out of scope).
@ -85,7 +88,7 @@ All colors use CSS variable tokens from `app/globals.css` + `app/styles/brand.cs
| Role | Token / Class | Usage |
|------|--------------|-------|
| Dominant surface (60%) | `bg-background` | Page background, filter strip background, row background |
| Secondary surface (30%) | `bg-muted` / `bg-muted/50` | Ticket number badge, filter chip hover, row hover (`hover:bg-accent`) |
| Secondary surface (30%) | `bg-muted` / `bg-muted/50` | Ticket number badge, filter chip hover, row hover (`hover:bg-muted/50`) |
| Primary accent (10%) | `text-primary` / `bg-primary` / `bg-primary/15` | Active filter chip selected state (`bg-primary text-primary-foreground`) |
| Muted text | `text-muted-foreground` | Company, age, assignee — all secondary row fields |
| Border | `border-border` | Row dividers, filter strip border-b, filter chip borders |
@ -108,6 +111,10 @@ These are direct Tailwind palette references (not CSS variable tokens) per the D
## Component Inventory
### Primary Visual Anchor
The primary focal point on the list page is the ticket title row. The `text-sm font-semibold` title creates a clear hierarchy against the `text-xs font-normal` secondary metadata (company, age, queue) beneath it. Every row is visually anchored to this title line — readers land there first, then scan down to context.
### Filter Strip [D-01 through D-04] — Collapsible
**Container:** `sticky top-0 bg-background z-10 border-b px-4 pt-4 pb-3 space-y-2`
@ -131,13 +138,13 @@ These are direct Tailwind palette references (not CSS variable tokens) per the D
| Queue | `Select` (shadcn) | Sourced from existing `/api/mobile/tickets` queue list | `queue` (int) |
| Assigned to me | `Switch` or `Toggle` (shadcn) | on/off | `mine` (`1`/absent) |
Filter chips: `shrink-0 px-3 py-1 rounded-full text-xs font-medium border transition-colors` — active: `bg-primary text-primary-foreground border-primary`; inactive: `border-border hover:bg-accent`
Filter chips: `shrink-0 px-3 py-1 rounded-full text-xs font-semibold border transition-colors` — active: `bg-primary text-primary-foreground border-primary`; inactive: `border-border hover:bg-muted/50`
"Clear all" button: `text-xs text-muted-foreground underline`, appears only when ≥1 filter is active [D-04]
### Ticket List Row [D-15 through D-17]
Outer wrapper: `<Link>``flex items-start border-l-4 {priority-border-class} px-4 py-3.5 hover:bg-accent transition-colors active:bg-accent`
Outer wrapper: `<Link>``flex items-start border-l-4 {priority-border-class} px-4 py-4 hover:bg-muted/50 transition-colors active:bg-muted/50`
Row interior structure:
```
@ -157,7 +164,7 @@ Row dividers: `divide-y` on the list container — matches existing pattern.
- Sentinel: `<div ref={sentinelRef} aria-hidden="true" />` at list end
- `IntersectionObserver` with `rootMargin: '200px'` fires `fetchNextPage()` when sentinel enters viewport
- Guard: no-op if `loadingMore || !hasMore`
- Load more button: `w-full py-3 rounded-xl border text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50` — always rendered when `hasMore`, focusable, triggers same fetch path
- Load more button: `w-full py-3 rounded-xl border text-sm font-semibold hover:bg-muted/50 transition-colors disabled:opacity-50` — always rendered when `hasMore`, focusable, triggers same fetch path
- Loading more indicator: small `Loader2` icon (`w-4 h-4 animate-spin text-muted-foreground`) centered above the Load more button during in-flight requests
### Detail Page In-Page Header [D-18]
@ -168,7 +175,7 @@ New header bar: `flex items-center justify-between px-4 py-3 border-b`
Left slot: `<button>` with `ArrowLeft` icon (h-4 w-4) + "Tickets" text, `router.back()`, `text-sm text-muted-foreground hover:text-foreground`
Center slot: breadcrumb — `"Tickets / #" + ticket_number` in `text-sm font-medium` (truncate if needed)
Center slot: breadcrumb — `"Tickets / #" + ticket_number` in `text-sm font-semibold` (truncate if needed)
Right slot: `<a>` to desktop ticket URL (`/analyzer/ticket/{id}` or Autotask direct URL) with `ExternalLink` icon (h-4 w-4), `text-muted-foreground hover:text-foreground`, `target="_blank" rel="noopener noreferrer"`, `aria-label="Open on desktop"`
@ -234,6 +241,7 @@ Error state: `toast.error()` via sonner + Load more button label changes to "Ret
- Detail back button: `aria-label="Back to Tickets"`
- Detail external link: `aria-label="Open ticket on desktop"`
- Priority stripe: decorative only; priority is also conveyed textually in the ticket number badge row context
- Empty state refresh affordance: icon-only button (`RefreshCw` h-4 w-4) with `aria-label="Refresh ticket list"` — no visible text label
---
@ -252,7 +260,7 @@ All copy locked from D-20, D-21, and REQUIREMENTS.md.
| Empty state — filters active | "No tickets match your filters" | D-20 |
| Empty state CTA — filters active | "Clear filters" (button) | D-20 |
| Empty state — no filters, no tickets | "No tickets to triage right now" | D-20 |
| Empty state refresh affordance | "Refresh" (icon button) | D-20 |
| Empty state refresh affordance | icon-only (`RefreshCw`) with `aria-label="Refresh ticket list"` — no visible text | D-20 |
| Initial load state | 5 skeleton rows (no text) | D-21 |
| Load more button (idle) | "Load more" | D-14 |
| Load more button (loading) | "Loading…" (Loader2 spinner) | D-21 |
@ -355,4 +363,5 @@ Per D-19 and phase boundary:
*Phase: 04-tickets-restyle*
*UI-SPEC created: 2026-05-03*
*UI-SPEC revised: 2026-05-03 (checker blocking fixes — typography weights, spacing, color contradiction, visual anchor, copywriting, spacing scale)*
*Source decisions: 04-CONTEXT.md D-01 through D-21 (all locked)*