wulf-pulse/.planning/phases/07-engagement-overview-new/07-02-SUMMARY.md
lorentz ccd2177977 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
2026-05-03 22:52:04 -04:00

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 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

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.