From 16bc4aa5c98a1c5ae5cd0cd452569eb9f90540b4 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 17:40:16 -0400 Subject: [PATCH] docs(04): UI design contract for tickets-restyle phase Co-Authored-By: Claude Sonnet 4.6 --- .../phases/04-tickets-restyle/04-UI-SPEC.md | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 .planning/phases/04-tickets-restyle/04-UI-SPEC.md diff --git a/.planning/phases/04-tickets-restyle/04-UI-SPEC.md b/.planning/phases/04-tickets-restyle/04-UI-SPEC.md new file mode 100644 index 0000000..14619a4 --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-UI-SPEC.md @@ -0,0 +1,358 @@ +--- +phase: 4 +slug: tickets-restyle +status: draft +shadcn_initialized: true +preset: new-york / neutral base / CSS variables +created: 2026-05-03 +--- + +# Phase 4 — UI Design Contract: Tickets Restyle + +> Visual and interaction contract for the mobile Tickets list page and detail header reskin. +> Generated by gsd-ui-researcher. Consumed by gsd-ui-checker, gsd-planner, gsd-executor. + +All decisions tagged `[D-NN]` are LOCKED in `04-CONTEXT.md` and must not be re-litigated. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | shadcn/ui (new-york style) | +| Preset | `components.json` — new-york, neutral base, CSS variables, lucide icons | +| Component library | Radix UI (via shadcn) | +| Icon library | lucide-react | +| Font | IBM Plex Sans (sans), IBM Plex Mono (numeric/ID fields) | + +Source: `components.json` (confirmed present), `DESIGN.md §2`, `app/styles/brand.css`. + +--- + +## Viewport Contract + +| Property | Value | +|----------|-------| +| Reference device | iPhone 15 Pro — 393 × 852 CSS pixels | +| Max width constraint | `max-w-lg mx-auto` (from `app/mobile/layout.tsx`) | +| Shell chrome | HeaderBar (sticky, h-14 + pt-safe) + BottomNav (fixed h-16 + pb-safe) | +| Scrollable content area | `
` in layout — bottom padding = `calc(theme(spacing.16)+env(safe-area-inset-bottom))` | +| In-page sticky zone | Filter strip header row sticks below the HeaderBar at `top-0 z-10` | + +--- + +## Spacing Scale + +Declared values (multiples of 4). Pulled from existing mobile page patterns and DESIGN.md §3. + +| Token | Value | Usage in this phase | +|-------|-------|---------------------| +| 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` | +| 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`) | + +Touch-target exception: interactive controls (toggle, select, filter chips, Load more button) must reach a minimum 44 × 44 px tap target — use `min-h-[44px]` or `py-2.5`/`py-3` padding to satisfy this on compact elements. + +Exceptions: 4px-wide priority stripe is a visual mark, not an interactive target — `border-l-4` exactly as specified in D-15. + +--- + +## Typography + +All sizes are Tailwind defaults read from `DESIGN.md §2` and matched to the existing `app/mobile/tickets/page.tsx` patterns. + +| 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.) | +| 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). + +--- + +## Color + +All colors use CSS variable tokens from `app/globals.css` + `app/styles/brand.css`. Direct Tailwind palette references are used only for semantic status colors per the recipe in `DESIGN.md §2`. + +| 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`) | +| 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 | +| Destructive | `text-destructive` | Error toast, retry button label | + +Accent reserved for: active filter chip selected state only. Not used for row hover, icons, or decorative elements. + +### Priority Stripe Colors [D-15] — LOCKED + +| Priority | Border class | Semantic | +|----------|-------------|----------| +| 1 — Critical | `border-red-500` | Red | +| 2 — High | `border-orange-400` | Orange | +| 3 — Medium | `border-amber-400` | Amber | +| 4 — Low | `border-slate-300` | Slate | + +These are direct Tailwind palette references (not CSS variable tokens) per the DESIGN.md §2 recipe for status hues outside the core token set. The `border-l-4` stripe is the exclusive carrier of priority color — the old `bg-{color}` priority dot is removed [D-17]. + +--- + +## Component Inventory + +### 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` + +**Search row (always visible):** +- `Input` (shadcn) with `Search` icon left (`pl-9`), `X` clear button right +- Placeholder: "Search tickets, company…" +- 400ms debounce [D-03] + +**Collapsible toggle row (always visible):** +- Left: "N open tickets" count (`text-xs text-muted-foreground`) +- Right: "Filters" button (`Button variant="ghost" size="sm"`) + `SlidersHorizontal` or `ChevronDown`/`ChevronUp` icon; shows active filter count badge when ≥1 filter active +- Uses `Collapsible` from `components/ui/collapsible.tsx` with `open` state managed in component [D-01] + +**Collapsible expanded panel [D-02]:** + +| Control | Component | Options | URL param | +|---------|-----------|---------|-----------| +| Status | chip group | Open / In Progress / Waiting (multi-select) | `status` (comma-int) | +| Priority | chip group | Critical / High / Medium / Low (multi-select) | `priority` (comma-int) | +| 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` + +"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: `` — `flex items-start border-l-4 {priority-border-class} px-4 py-3.5 hover:bg-accent transition-colors active:bg-accent` + +Row interior structure: +``` +[4px priority stripe via border-l-4] +[content area flex-1 min-w-0] + [title row]: ticket title (1-line truncate, text-sm font-semibold) + ChevronRight (right, shrink-0) + [company]: text-xs text-muted-foreground truncate + [metadata row]: ticket# badge (font-mono text-[10px] bg-muted) | queue label (text-[10px] text-muted-foreground) | age (text-[10px] + Clock icon, ml-auto) +``` + +Note: assignee initials avatar (D-16) — render as a small circular `span` with `bg-primary/15 text-primary` when assigned_to is present. Keep `relTime()` helper as-is [D-17]. + +Row dividers: `divide-y` on the list container — matches existing pattern. + +### Infinite Scroll Sentinel + Load More [D-12 through D-14] + +- Sentinel: `