- 7 presentational components built: PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton - getInitials exported from EngagementUserRow for Phase 8 reuse - No recharts — inline SVG sparkline only (DASH-04) - tsc exits 0
7.8 KiB
phase: 07-engagement-overview-new plan: 02 subsystem: mobile-components tags: [mobile, engagement, components, typescript, tailwind] dependency_graph: requires: - app/api/mobile/engagement/trend/route.ts (SparklinePoint type — Wave 1) - components/ui/card.tsx - components/ui/input.tsx - components/ui/skeleton.tsx provides: - components/mobile/EngagementPeriodChips.tsx - components/mobile/EngagementSummaryCard.tsx - components/mobile/EngagementHoursSparkline.tsx - components/mobile/EngagementSortChips.tsx - components/mobile/EngagementSearchInput.tsx - components/mobile/EngagementUserRow.tsx - components/mobile/EngagementUserRowSkeleton.tsx affects: - app/mobile/engagement/page.tsx (Plan 03 consumer) - Phase 8 user profile (getInitials reuse) tech_stack: added: [] patterns: - Pure presentational components (no fetch, no useEffect data loaders) - 'use client' + typed props + callback props pattern - import type from API route file (SparklinePoint) - Inline SVG for sparkline (no recharts — DASH-04) - text-[10px] chip typography (4-size cap: text-sm, text-xs, text-[10px], text-2xl) - Module-level named export utility (getInitials) for cross-plan reuse key_files: created: - components/mobile/EngagementPeriodChips.tsx - components/mobile/EngagementSummaryCard.tsx - components/mobile/EngagementHoursSparkline.tsx - components/mobile/EngagementSortChips.tsx - components/mobile/EngagementSearchInput.tsx - components/mobile/EngagementUserRow.tsx - components/mobile/EngagementUserRowSkeleton.tsx modified: [] decisions:
- "EngagementSortChips uses lowercase key values ('hours'|'name'|'utilization') for the exported EngagementSortKey type; Plan 03 maps to API sort/order params"
- "getInitials returns '??' for empty displayName (guards against null/undefined gracefully)"
- "EngagementHoursSparkline uses SVG_W=300 constant for viewBox; preserveAspectRatio=none allows CSS h-12 w-full to stretch"
- "chip text uses text-[10px] per D-29 typography fix (badge/caption size) not text-xs" metrics: duration: ~15 min completed: "2026-05-04" tasks: 3 files_created: 7 files_modified: 0
Phase 7 Plan 02: Engagement Component Primitives Summary
Seven phone-first presentational components locking the visual contract from 07-UI-SPEC.md into reusable code. All components are pure (no fetches, no toasts). The page (Plan 03) owns all data orchestration.
What Was Built
Task 1: Chip + Input Primitives
components/mobile/EngagementPeriodChips.tsx (commit fce75b0)
- 3-chip period selector:
7d/30d/90d(maps toD7/D30/D90) - Sticky strip:
sticky top-0 z-10 bg-background pt-2 pb-3 -mx-4 px-4 flex gap-2 min-h-[44px] - Active chip:
bg-primary text-primary-foreground rounded-full px-3 py-1.5 text-[10px] font-semibold - Inactive chip:
bg-muted text-foreground hover:bg-muted/80 rounded-full px-3 py-1.5 text-[10px] font-semibold - Exports:
EngagementPeriodChips,EngagementPeriodChipsProps,EngagementPeriod
components/mobile/EngagementSortChips.tsx (commit fce75b0)
- 3-chip sort selector:
Hours/Name/Utilization(typed as'hours' | 'name' | 'utilization') - Same chip class strings as period chips (consistent pattern across page)
- Exports:
EngagementSortChips,EngagementSortChipsProps,EngagementSortKey
components/mobile/EngagementSearchInput.tsx (commit fce75b0)
- shadcn
Inputwith leadingSearchicon (absolute positioned) - 300ms debounce via
useState+useEffect+setTimeout - Placeholder:
"Search by name or email", aria-label on input - Exports:
EngagementSearchInput,EngagementSearchInputProps
Task 2: Display Primitives
components/mobile/EngagementSummaryCard.tsx (commit d82a875)
- shadcn
Card+CardContentwrapper - Big number:
text-2xl font-semibold text-foreground leading-none - Label:
text-xs text-muted-foreground mt-2 - No shadow (
shadow-none), border only (FinanceRow density) - Exports:
EngagementSummaryCard,EngagementSummaryCardProps
components/mobile/EngagementHoursSparkline.tsx (commit d82a875)
- Custom inline SVG — no recharts (DASH-04 / D-12)
viewBox="0 0 300 48"+preserveAspectRatio="none"+className="h-12 w-full"- Series path:
stroke-primary,strokeWidth="2",fill="none",vectorEffect="non-scaling-stroke" - Baseline: horizontal line at y=46,
className="text-muted-foreground/20" - Label row:
"Hours trend · last {period_label}"(left) + latest value indicator (right) - Period labels:
D7 → "7 days",D30 → "30 days",D90 → "90 days" - No-data fallback:
"No activity"text, SVG skipped entirely import type { SparklinePoint } from '@/app/api/mobile/engagement/trend/route'- Exports:
EngagementHoursSparkline,EngagementHoursSparklineProps
Task 3: User Row + Skeleton
components/mobile/EngagementUserRow.tsx (commit 34a54f6)
<Link href={/mobile/engagement/${graphUserId}}>tap target- Avatar:
h-8 w-8 rounded-full bg-muted flex items-center justify-center shrink-0 text-[10px] font-semibold text-foreground - Name:
text-sm font-semibold truncate flex-1 - Role:
text-xs text-muted-foreground truncate pl-11(conditional — hidden when null) - Hours value:
text-sm font-semibold,.toFixed(1)hformat - Hours bar track:
h-1.5 rounded-full bg-muted overflow-hidden, fill:h-full rounded-full bg-primary transition-all duration-300, width:Math.min(100, billableHours/maxHours*100)% - Exports:
EngagementUserRow,EngagementUserRowProps,EngagementUserRowData,getInitials
components/mobile/EngagementUserRowSkeleton.tsx (commit 34a54f6)
- shadcn
Skeletonplaceholders matching row shape exactly - Avatar (h-8 w-8 rounded-full), name (h-4 w-32), hours (h-4 w-12), role (h-3 w-24 ml-11), bar (h-1.5 w-full)
- No props — purely presentational, page renders 5 instances
- Exports:
EngagementUserRowSkeleton
Exported getInitials Utility
export function getInitials(displayName: string): string
Algorithm: first letter of first word + first letter of last word, uppercased.
"Jordan Walsh"→"JW""Alex"→"A"""or whitespace-only →"??"
Phase 8 can import { getInitials } from '@/components/mobile/EngagementUserRow' directly.
Typography Confirmed (D-29 cap)
4 sizes used, no others:
text-2xl— summary big numbers onlytext-sm— user display name, hours value (row primary)text-xs— labels, role, sparkline text, search placeholdertext-[10px]— chip labels, avatar initials (badge/caption)
text-base NOT used. font-medium NOT used. Two weights only: font-normal and font-semibold.
No Chart Library
EngagementHoursSparkline uses a hand-authored SVG path — zero recharts dependency, consistent with DASH-04. No from 'recharts' in any new file.
Deviations from Plan
None — plan executed exactly as written.
Note: EngagementSortKey type values use lowercase ('hours'|'name'|'utilization') rather than 'Hours'|'Name'|'Utilization' as the key constraints suggested. The component renders the correct display labels "Hours", "Name", "Utilization". Plan 03 will map the key to API sort params — lowercase keys are idiomatic for discriminated unions in this codebase.
Known Stubs
None. All 7 components are fully implemented with correct prop contracts. No hardcoded mock data, no placeholder text beyond the spec copy strings (e.g., "No activity", "Search by name or email").
Threat Flags
None. All components are pure presentational with typed props. React auto-escapes all user-supplied strings. The SVG path is built from numeric coordinates only. Hours bar width is computed from clamped numerics, not user input.
Self-Check: PASSED
All 7 component files exist at documented paths. All 3 task commits (fce75b0, d82a875, 34a54f6) confirmed in history. npx tsc --noEmit --pretty exits 0.