From 725fe731cadd23c8704453ba9a259b84d7001110 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 7 May 2026 17:44:42 -0400 Subject: [PATCH] docs(08): UI design contract --- .../08-UI-SPEC.md | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 .planning/phases/08-engagement-user-profile-new/08-UI-SPEC.md diff --git a/.planning/phases/08-engagement-user-profile-new/08-UI-SPEC.md b/.planning/phases/08-engagement-user-profile-new/08-UI-SPEC.md new file mode 100644 index 0000000..39b340e --- /dev/null +++ b/.planning/phases/08-engagement-user-profile-new/08-UI-SPEC.md @@ -0,0 +1,282 @@ +--- +phase: 8 +slug: engagement-user-profile-new +status: draft +shadcn_initialized: true +preset: "new-york / neutral base / cssVariables: true / Tailwind v4" +created: 2026-05-07 +--- + +# Phase 8 — UI Design Contract +## Engagement User Profile (NEW) + +> Visual and interaction contract for `/mobile/engagement/[userId]`. +> Generated by gsd-ui-researcher. Consumed by gsd-ui-checker, gsd-planner, gsd-executor, gsd-ui-auditor. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | shadcn/ui | +| Preset | new-york style, neutral base color, cssVariables: true | +| Component library | Radix UI (via shadcn) | +| Icon library | lucide-react | +| Font | IBM Plex Sans (`--font-sans`), IBM Plex Mono (`--font-mono`) | + +Source: `components.json` + `npx shadcn info` (detected, not assumed) + +--- + +## Spacing Scale + +Declared values (multiples of 4 only). Reuses Phase 7 established tokens. + +| Token | Value | Usage | +|-------|-------|-------| +| xs | 4px | Inline gaps (`gap-1`, `mt-1`), icon-to-text padding | +| sm | 8px | Compact element spacing (`gap-2`, `mt-2`, `space-y-2`) | +| md | 16px | Default card padding (`px-4`, `py-4`), section padding | +| lg | 24px | Section separation (`space-y-6`), card-to-card gap | +| xl | 32px | Major vertical rhythm between page sections | +| 2xl | 48px | Not used in this phase (no full-page top padding) | +| 3xl | 64px | Not used in this phase | + +Exceptions: +- Avatar: `h-14 w-14` (56px) for identity header — larger than standard row avatar (`h-8 w-8`) to carry header weight +- Row list items: `py-1.5` (6px) vertical padding inside breakdown card metric rows (matches CONTEXT.md D-16 spec) +- Period chip strip: `min-h-[44px]` touch target (matches `EngagementPeriodChips` existing implementation) +- Sticky chip bar: `-mx-4 px-4` bleed-to-edge pattern (matches Phase 7 component exactly) + +Source: CONTEXT.md D-11, D-16; Phase 7 `EngagementSummaryCard.tsx`, `EngagementUserRow.tsx` + +--- + +## Typography + +Four sizes, two weights. Matches Phase 7 established token set exactly. + +| Role | Size | Weight | Line Height | Usage | +|------|------|--------|-------------|-------| +| Display | 24px (`text-2xl`) | 600 (`font-semibold`) | none (leading-none) | Hero metric numbers in 2×2 grid | +| Body | 14px (`text-sm`) | 400 (`font-normal`) | 1.5 | Activity breakdown metric rows, recent-entry row text | +| Label | 12px (`text-xs`) | 400 (`font-normal`) | 1.5 | Card section sub-labels, metric labels under hero numbers, relative timestamps | +| Heading | 14px (`text-sm`) | 500 (`font-medium`) | 1.5 | Breakdown card subsection headers (`text-sm font-medium text-muted-foreground`) | + +Additional fixed sizes (from existing components, do not change): +- Avatar initials: `text-[10px] font-semibold` — inherits from `EngagementUserRow` pattern +- Period chips: `text-[10px] font-semibold` — inherits from `EngagementPeriodChips` +- User display name in identity header: `text-lg font-semibold` (one step above body, carries identity weight) +- Section H1 (page title): `text-xl font-semibold` (matches Phase 7 overview H1 pattern) + +Source: CONTEXT.md D-11, D-16; `EngagementSummaryCard.tsx`, `EngagementUserRow.tsx`, `EngagementPeriodChips.tsx` + +--- + +## Color + +All values are CSS custom property tokens from `app/globals.css`. Do not use raw Tailwind color utilities (no `bg-blue-500`, no `text-gray-400`). Use semantic tokens only. + +| Role | Token | Light Value | Dark Value | Usage | +|------|-------|-------------|------------|-------| +| Dominant (60%) | `bg-background` / `text-foreground` | `oklch(1 0 0)` / `oklch(0.145 0 0)` | inverse | Page background, scrollable content area | +| Secondary (30%) | `bg-card` / `text-card-foreground` + `bg-muted` | `oklch(1 0 0)` / `oklch(0.97 0 0)` | `oklch(0.205 0 0)` / `oklch(0.269 0 0)` | All `Card` containers, skeleton fills, avatar backgrounds | +| Accent (10%) | `bg-primary` / `text-primary` | `oklch(0.55 0.16 220)` logo blue | `oklch(0.62 0.17 220)` | See reserved list below | +| Muted text | `text-muted-foreground` | `oklch(0.556 0 0)` | `oklch(0.708 0 0)` | Metric labels, subsection headers, secondary identity rows | +| Destructive | `text-destructive` / `bg-destructive` | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | Not used in this phase (no destructive actions) | +| Border | `border-border` | `oklch(0.922 0 0)` | `oklch(1 0 0 / 14%)` | Card borders, dividers, row separators | + +**Accent (`text-primary` / `bg-primary`) reserved for:** +1. Active period chip background (`bg-primary text-primary-foreground`) +2. Hours-bar fill in identity header or compact display bars (`bg-primary`) +3. `mailto:` email link text (`text-primary`) +4. Billable badge accent (if using `Badge` with `variant="default"`) + +Inactive period chips use `bg-muted text-foreground`. Do NOT apply `text-primary` to general body text or section headers. + +Source: CONTEXT.md D-08, D-11; `app/globals.css`; `EngagementPeriodChips.tsx` + +--- + +## Component Inventory + +All components are either reused from prior phases or are new Phase 8 components built on shadcn primitives. + +### Reused from Phase 7 (no modification) + +| Component | File | Usage in Phase 8 | +|-----------|------|-----------------| +| `EngagementPeriodChips` | `components/mobile/EngagementPeriodChips.tsx` | Period selector (D7/D30/D90), default D30 | +| `EngagementUserRow` → `getInitials()` | `components/mobile/EngagementUserRow.tsx` | Initials computation for header avatar | + +### Reused shadcn primitives + +| Primitive | Import | Usage | +|-----------|--------|-------| +| `Card`, `CardContent` | `@/components/ui/card` | Identity header card, 2×2 metric grid cards, activity breakdown card, recent-items sections | +| `Skeleton` | `@/components/ui/skeleton` | All loading states (avatar, metric cards, breakdown rows, recent lists) | +| `Collapsible`, `CollapsibleContent`, `CollapsibleTrigger` | `@/components/ui/collapsible` | Tap-to-expand recent time entries and recent meetings (D-20) | +| `Badge` | `@/components/ui/badge` | Billable badge on time entry rows | + +### New components for Phase 8 + +| Component | File | Purpose | +|-----------|------|---------| +| `EngagementProfileHeader` | `components/mobile/EngagementProfileHeader.tsx` | Identity card: avatar (photo or initials) + name + job title + department + email mailto link + last active | +| `EngagementProfileMetricGrid` | `components/mobile/EngagementProfileMetricGrid.tsx` | 2×2 grid of 4 hero metrics using Card + `text-2xl font-semibold` pattern | +| `EngagementProfileBreakdown` | `components/mobile/EngagementProfileBreakdown.tsx` | Single card with 3 subsections: Time / Communication / Meetings (D-14..D-17) | +| `EngagementRecentEntries` | `components/mobile/EngagementRecentEntries.tsx` | Collapsible list of up to 10 recent time entries (D-18..D-21) | +| `EngagementRecentMeetings` | `components/mobile/EngagementRecentMeetings.tsx` | Collapsible list of up to 10 recent Teams meetings (D-18..D-21) | +| `EngagementProfileSkeleton` | `components/mobile/EngagementProfileSkeleton.tsx` | Full loading skeleton: header + 4 metric cards + breakdown card + 2 list skeletons (D-23) | + +--- + +## Layout Structure + +Single-column, `'use client'`, phone-first. No sidebars, no multi-column layouts at any breakpoint in this phase. + +``` +app/mobile/engagement/[userId]/page.tsx +│ +├──
(scrollable, bottom padding for nav) +│ ├──

User display name — page title +│ ├── EngagementPeriodChips sticky top-0, z-10, edge-to-edge bleed +│ ├── EngagementProfileHeader identity card (avatar + fields) +│ ├── EngagementProfileMetricGrid 2×2 grid, gap-3 +│ ├── EngagementProfileBreakdown single card, 3 subsections +│ ├── EngagementRecentEntries collapsible list section +│ └── EngagementRecentMeetings collapsible list section +``` + +**Sticky chip bar class** (exact, inherited from Phase 7): +`sticky top-0 z-10 bg-background pt-2 pb-3 -mx-4 px-4` + +**Card spacing** (between sections): +`space-y-4` between identity header, metric grid, breakdown card, and each recent section. + +**2×2 metric grid**: `grid grid-cols-2 gap-3` + +**Activity breakdown subsections**: separated by `border-t border-border` inside the single card, no extra padding headers — just `text-sm font-medium text-muted-foreground mb-2` label followed by metric rows. + +--- + +## Interaction Contracts + +### Period chip selection +- Chips: 7d / 30d / 90d mapping to `D7` / `D30` / `D90` +- Default: `D30` +- On change: re-fetch `/api/engagement/user/[userId]?period={D7|D30|D90}` +- Period change resets metric grid + breakdown card + does NOT reset recent-items count (always 10, per D-19) +- Active chip: `bg-primary text-primary-foreground`; inactive: `bg-muted text-foreground` + +### Avatar/photo loading +- `` with `onError` fallback to initials `` +- Initials rendered in `h-14 w-14 rounded-full bg-muted` with `text-base font-semibold text-foreground` +- Photo rendered as `h-14 w-14 rounded-full object-cover` +- No FOUC: render initials immediately, upgrade to photo on load success + +### Loading skeleton +- Shown while initial fetch is in flight +- Skeleton layout mirrors final: header-skeleton → 4 metric-card skeletons (2×2) → breakdown-card-skeleton → 2 list-skeletons +- Use `Skeleton` primitives at matching heights/widths +- Period chips render immediately (not skeleton) — they drive the fetch + +### Tap-to-expand (recent items) +- State: `Set` of expanded IDs held in component-local `useState` +- Collapsed default: shows one-line summary row +- Expanded: `CollapsibleContent` reveals detail rows below the summary +- Animation: `transition-all duration-200` on `CollapsibleContent` (shadcn default behavior) +- No URL state, no router push, no scroll jump on expand + +### Error states +- 404 from endpoint: render inline "User not found" message with back `Link` to `/mobile/engagement` (no toast) +- 500 / network error: `toast.error(...)` (sonner) + inline retry button on the page body (D-24) +- Photo 404 / 503: silently fall back to initials (no toast, no error message) + +### Navigation +- Back gesture / browser back: Next.js App Router default `scrollRestoration: true` returns to overview at previous scroll position (D-04) +- No custom `sessionStorage` workaround unless execution phase confirms it's broken +- No explicit "Back" button required in the page body — the mobile shell header already provides back navigation via the standard iOS/Android gesture + App Router prefetch + +--- + +## Copywriting Contract + +| Element | Copy | +|---------|------| +| Page H1 | `{displayName}` (the user's full name — not a generic title) | +| Period chip labels | `7d` / `30d` / `90d` (lowercase, concise) | +| Hero metric labels | "Hours worked" / "Billable hours" / "Days worked" / "Meetings attended" | +| Activity section heading — Time | "Time" | +| Activity section heading — Communication | "Communication" | +| Activity section heading — Meetings | "Meetings" | +| After-hours row | "After-hours · {X}% messages, {Y}% meetings" | +| Utilization row | "Utilization · {Z}%" (billable ÷ hours worked) | +| Zoom row label | "Zoom calls" | +| Recent time entries section header | "Recent time entries" | +| Recent meetings section header | "Recent meetings" | +| Billable badge | "Billable" (shadcn `Badge variant="secondary"`) | +| Empty state — time entries | "No time entries in the last 30 days" (hardcoded copy; period context implicit from chips) | +| Empty state — meetings | "No meetings recorded" | +| Empty state — both empty, user found | Section headers still render; each section shows its inline empty message | +| Error state — 404 | "User not found" (heading) + "This profile is no longer available." (body) + "Back to Engagement" (link) | +| Error state — 500 | Toast: `"Failed to load profile — tap to retry"` + inline `"Retry"` button below skeleton | +| Last active — relative (≤7 days) | "Active {N} hours ago" / "Active {N} days ago" | +| Last active — absolute (>7 days) | "Last active {MMM D, YYYY}" (formatted via `useUserTimezone()`) | +| Email link | `{email}` — the address itself as the link text; `mailto:` href | +| Department row | `{department}` — raw value, no prefix label | + +No destructive actions in this phase. No confirmation dialogs. + +Source: CONTEXT.md D-06, D-12, D-14, D-15, D-21, D-24; REQUIREMENTS.md ENG-06..08 + +--- + +## Date/Time Formatting + +All date display goes through `useUserTimezone()` hook from `lib/hooks/use-user-timezone.ts`. + +| Value | Format | +|-------|--------| +| `entry_date` (time entry) | `MMM d` if current year; `MMM d, yyyy` if prior year | +| `recentTeamsMeetings.date` | Same as above | +| Last active — relative | Use `date-fns` `formatDistanceToNow(date, { addSuffix: true })` with TZ option | +| Last active — absolute (>7d) | `{ month: 'short', day: 'numeric', year: 'numeric', timeZone: tz }` via `Intl.DateTimeFormat` | +| Threshold for relative vs absolute | ≤7 days = relative; >7 days = absolute | + +Source: CONTEXT.md D-06; `lib/hooks/use-user-timezone.ts` (Phase 7.1 pattern) + +--- + +## Registry Safety + +| Registry | Blocks Used | Safety Gate | +|----------|-------------|-------------| +| shadcn official | `card`, `skeleton`, `collapsible`, `badge` | not required | + +No third-party registries. `registries: {}` confirmed in `components.json`. + +--- + +## Accessibility Notes + +- Avatar `` must have `alt="{displayName}"` when photo loads; `aria-hidden="true"` on the initials `` (decorative fallback) +- Period chips use `role="button"` + `aria-pressed={isActive}` (inherits from `EngagementPeriodChips` exactly) +- Collapsible trigger buttons include visible text label (entry date + hours) — no icon-only triggers +- Metric rows in breakdown card: use `
` / `
` / `
` semantics or `flex justify-between` with visible labels — avoid value-only spans +- Email link: `` +- Touch targets: all interactive elements (chips, expand triggers, links) meet 44px minimum height + +--- + +## 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