docs(07-02): complete engagement component primitives plan summary

- 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
This commit is contained in:
lorentz 2026-05-03 22:52:04 -04:00
parent 00d0102168
commit ccd2177977

View file

@ -0,0 +1,161 @@
---
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 to `D7` / `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 `Input` with leading `Search` icon (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` + `CardContent` wrapper
- 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)h` format
- 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 `Skeleton` placeholders 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
```ts
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 only
- `text-sm` — user display name, hours value (row primary)
- `text-xs` — labels, role, sparkline text, search placeholder
- `text-[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.