wulf-pulse/.planning/phases/08-engagement-user-profile-new/08-UI-SPEC.md

15 KiB
Raw Blame History

phase slug status shadcn_initialized preset created revised reviewed_at revision
8 engagement-user-profile-new approved true new-york / neutral base / cssVariables: true / Tailwind v4 2026-05-07 2026-05-07 2026-05-07 1

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), activity breakdown metric row vertical padding (py-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
  • Period chip strip: min-h-[44px] touch target floor for WCAG compliance — declared as min-h only, never as padding or gap (matches EngagementPeriodChips existing implementation)
  • Sticky chip bar: -mx-4 px-4 bleed-to-edge pattern (matches Phase 7 component exactly)

D-16 override: CONTEXT.md D-16 specified py-1.5 (6px) for metric row vertical padding. This contract supersedes that to py-2 (8px) — the nearest 4px-grid value. The 2px difference is visually equivalent at row-list scale. Engineers implementing the breakdown card should use py-2 throughout. The 44px min-h touch target on chips is a WCAG floor and is exempt from the 4px grid constraint.

Source: CONTEXT.md D-11, D-16 (overridden as noted above); Phase 7 EngagementSummaryCard.tsx, EngagementUserRow.tsx


Typography

Four sizes, two weights. Matches Phase 7 established token set with the adjustments noted below.

Role Size Weight Line Height Usage
Display 24px (text-2xl) 600 (font-semibold) none (leading-none) Hero metric numbers in 2×2 grid
Title 20px (text-xl) 600 (font-semibold) 1.2 Page H1 (user display name), section H1 (page title)
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, avatar initials, period chip text

Typography revision notes (r1):

  • The user display name in the identity header was previously declared as text-lg (18px). It is now text-xl (20px), unifying with the page H1 role. The 2px upward change strengthens identity hierarchy and eliminates a fifth size.
  • The Heading role (breakdown card subsection headers) was previously font-medium (500). It is now font-semibold (600) to hold to a two-weight system. The text-muted-foreground colour still softens the visual weight so headers do not feel heavy.
  • Avatar initials and period chip text were previously declared as text-[10px] (non-standard). They are promoted to text-xs (12px). The 2px change is imperceptible at that scale and removes a non-standard token.
  • Any Phase 7 component that currently renders text-[10px] (avatar initials in EngagementUserRow.tsx) or text-lg should be updated by the implementing engineer to match this contract.

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
EngagementUserRowgetInitials() 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
│
├── <main class="px-4 pb-safe">   (scrollable, bottom padding for nav)
│   ├── <h1>                      User display name — text-xl font-semibold
│   ├── 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-semibold text-muted-foreground mb-2 label followed by metric rows (flex justify-between text-sm py-2).


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

  • <img src="/api/mobile/engagement/user/[userId]/photo" ...> with onError fallback to initials <span>
  • Initials rendered in h-14 w-14 rounded-full bg-muted with text-xs 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<string> 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 <img> must have alt="{displayName}" when photo loads; aria-hidden="true" on the initials <span> (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 <dl> / <dt> / <dd> semantics or flex justify-between with visible labels — avoid value-only spans
  • Email link: <a href="mailto:{email}" className="text-primary underline-offset-4 hover:underline">
  • Touch targets: all interactive elements (chips, expand triggers, links) meet 44px minimum height via min-h-[44px]

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