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 (