From 1fa849de257c3ee748eeb09c4273ad99afa5cefa Mon Sep 17 00:00:00 2001 From: Ronald Roy Date: Sat, 24 Jan 2026 21:22:34 -0800 Subject: [PATCH] fix: Add skeleton loading states and entrance animations to HomeView widgets Widgets now show skeleton placeholders during load instead of popping into existence when data arrives. Changes include: - HomeView: Track loading states for goals, activity, and inspiration - InspirationCard: Optional quote prop, skeleton when loading or no data - GoalsCard: Loading skeleton with shimmer animation - SpacedRepetitionWidget: Show loading spinner immediately, idle state for no cards due, distinguish API errors from empty results - RecentActivity: Loading skeleton, staggered entrance animation (50ms delay) All animations respect prefers-reduced-motion. Tests updated accordingly. Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/home/GoalsCard.css | 82 ++++++++++++++ frontend/src/components/home/GoalsCard.tsx | 28 ++++- frontend/src/components/home/HomeView.tsx | 96 +++++++++------- .../src/components/home/InspirationCard.css | 20 ++++ .../src/components/home/InspirationCard.tsx | 11 +- .../src/components/home/RecentActivity.css | 103 ++++++++++++++++++ .../src/components/home/RecentActivity.tsx | 57 +++++++++- .../home/SpacedRepetitionWidget.css | 49 +++++++++ .../home/SpacedRepetitionWidget.tsx | 58 ++++++++-- .../home/__tests__/HomeView.test.tsx | 5 +- .../__tests__/SpacedRepetitionWidget.test.tsx | 8 +- 11 files changed, 450 insertions(+), 67 deletions(-) diff --git a/frontend/src/components/home/GoalsCard.css b/frontend/src/components/home/GoalsCard.css index 27fcd414..7155a52d 100644 --- a/frontend/src/components/home/GoalsCard.css +++ b/frontend/src/components/home/GoalsCard.css @@ -153,3 +153,85 @@ backdrop-filter: blur(4px); } } + +/* Loading skeleton */ +.goals-card--loading { + cursor: default; + pointer-events: none; +} + +.goals-card__skeleton { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.goals-card__skeleton-header { + width: 40%; + height: 0.9em; + background: linear-gradient( + 90deg, + var(--color-surface) 25%, + var(--glass-border) 50%, + var(--color-surface) 75% + ); + background-size: 200% 100%; + border-radius: var(--radius-sm); + animation: goals-shimmer 1.5s infinite; +} + +.goals-card__skeleton-line { + width: 100%; + height: 0.9em; + background: linear-gradient( + 90deg, + var(--color-surface) 25%, + var(--glass-border) 50%, + var(--color-surface) 75% + ); + background-size: 200% 100%; + border-radius: var(--radius-sm); + animation: goals-shimmer 1.5s infinite; +} + +.goals-card__skeleton-line--short { + width: 70%; +} + +@keyframes goals-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* Entrance animation */ +@keyframes goals-enter { + from { + opacity: 0; + transform: scale(0.98); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.goals-card--enter { + animation: goals-enter 0.2s ease backwards; +} + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + .goals-card__skeleton-header, + .goals-card__skeleton-line { + animation: none; + background: var(--glass-border); + } + + .goals-card--enter { + animation: none; + } +} diff --git a/frontend/src/components/home/GoalsCard.tsx b/frontend/src/components/home/GoalsCard.tsx index b49ce793..cced4aaa 100644 --- a/frontend/src/components/home/GoalsCard.tsx +++ b/frontend/src/components/home/GoalsCard.tsx @@ -11,6 +11,14 @@ import remarkGfm from "remark-gfm"; import { useSession } from "../../contexts/SessionContext"; import "./GoalsCard.css"; +/** + * Props for GoalsCard component. + */ +export interface GoalsCardProps { + /** Whether goals are still loading */ + isLoading?: boolean; +} + /** * GoalsCard displays goals.md content as rendered markdown. * @@ -19,7 +27,7 @@ import "./GoalsCard.css"; * - Returns null if no goals file exists in the vault * - Clicking the card triggers /review-goals command */ -export function GoalsCard(): React.ReactNode { +export function GoalsCard({ isLoading = false }: GoalsCardProps): React.ReactNode { const { goals, setDiscussionPrefill, setMode } = useSession(); const handleClick = useCallback(() => { @@ -27,7 +35,21 @@ export function GoalsCard(): React.ReactNode { setMode("discussion"); }, [setDiscussionPrefill, setMode]); - // Don't render if no goals content + // Show skeleton during load + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + // Don't render if no goals content (vault might not have goalsPath) if (!goals) { return null; } @@ -35,7 +57,7 @@ export function GoalsCard(): React.ReactNode { return (