15 KiB
| 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 asmin-honly, never as padding or gap (matchesEngagementPeriodChipsexisting implementation) - Sticky chip bar:
-mx-4 px-4bleed-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 topy-2(8px) — the nearest 4px-grid value. The 2px difference is visually equivalent at row-list scale. Engineers implementing the breakdown card should usepy-2throughout. The 44pxmin-htouch 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 nowtext-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 nowfont-semibold(600) to hold to a two-weight system. Thetext-muted-foregroundcolour 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 totext-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 inEngagementUserRow.tsx) ortext-lgshould 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:
- Active period chip background (
bg-primary text-primary-foreground) - Hours-bar fill in identity header or compact display bars (
bg-primary) mailto:email link text (text-primary)- Billable badge accent (if using
Badgewithvariant="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
│
├── <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" ...>withonErrorfallback to initials<span>- Initials rendered in
h-14 w-14 rounded-full bg-mutedwithtext-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
Skeletonprimitives 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-localuseState - Collapsed default: shows one-line summary row
- Expanded:
CollapsibleContentreveals detail rows below the summary - Animation:
transition-all duration-200onCollapsibleContent(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
Linkto/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: truereturns to overview at previous scroll position (D-04) - No custom
sessionStorageworkaround 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 havealt="{displayName}"when photo loads;aria-hidden="true"on the initials<span>(decorative fallback) - Period chips use
role="button"+aria-pressed={isActive}(inherits fromEngagementPeriodChipsexactly) - Collapsible trigger buttons include visible text label (entry date + hours) — no icon-only triggers
- Metric rows in breakdown card: use
<dl>/<dt>/<dd>semantics orflex justify-betweenwith 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