Skip to content

Commit cf76bf3

Browse files
Cawlummclaude
andcommitted
feat(mobile): skeleton placeholders for Weight + Home first load
Weight and Home now show content-shaped skeletons on initial load (matching the Programs/Workouts pattern) instead of the full-screen barbell loader — the layout is there immediately and each section fills into its own shape. Adds WeightSkeleton (header · log card · hero · Avg/Low/High · trend chart · history rows) and DashboardSkeleton (header + Start · KPI strip · volume · consistency · last workout · nutrition · muscle balance · weight), both composed from the shared Skeleton primitives. The barbell Loading stays on the Weight detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ccq3rvfb9ArzS2DidghJm5
1 parent 0713394 commit cf76bf3

4 files changed

Lines changed: 164 additions & 4 deletions

File tree

mobile/app/(tabs)/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
displayVolume, displayWeight, weightShort,
1212
type DailyStats, type WeightLog, type WeightStats, type Workout,
1313
} from '@lyftr/shared'
14-
import { AppText, Card, Label, Loading, Screen, SectionHeader, SegmentedControl } from '../../src/components/ui'
14+
import { AppText, Card, Label, Screen, SectionHeader, SegmentedControl } from '../../src/components/ui'
1515
import { ExerciseImage } from '../../src/components/workouts/ExerciseImage'
1616
import {
1717
MuscleDonut, MuscleSparkline, VolumeBarChart, WeightSparkline,
1818
} from '../../src/components/dashboard/DashboardCharts'
1919
import { QuickWeighInSheet } from '../../src/components/dashboard/QuickWeighInSheet'
20+
import { DashboardSkeleton } from '../../src/components/dashboard/DashboardSkeleton'
2021
import { client, useAuthStore, useSettingsStore, useWorkoutSession } from '../../src/lib/lyftr'
2122
import { muscleColor } from '../../src/utils/exerciseUtils'
2223
import { useTheme } from '../../src/theme/useTheme'
@@ -150,7 +151,7 @@ export default function Dashboard() {
150151
setRefreshing(false)
151152
}, [load])
152153

153-
if (loading) return <Loading />
154+
if (loading) return <DashboardSkeleton />
154155

155156
if (error) {
156157
return (

mobile/app/(tabs)/weight/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
weightError, weightShort, type WeightLog, type WeightStats,
1313
} from '@lyftr/shared'
1414
import {
15-
AppText, Button, Card, DateInput, Field, Label, Loading, NumberField, NumericKeyboardAccessory,
15+
AppText, Button, Card, DateInput, Field, Label, NumberField, NumericKeyboardAccessory,
1616
NUMERIC_ACCESSORY_ID, PageHeader, Screen, SegmentedControl, StepperTile,
1717
} from '../../../src/components/ui'
1818
import { ExerciseHistoryChart, type ChartPoint } from '../../../src/components/workouts/ExerciseHistoryChart'
19+
import { WeightSkeleton } from '../../../src/components/weight/WeightSkeleton'
1920
import { useServerInfiniteList } from '../../../src/hooks/useServerInfiniteList'
2021
import { client, useSettingsStore } from '../../../src/lib/lyftr'
2122
import { clampStep } from '../../../src/utils/number'
@@ -155,7 +156,7 @@ export default function Weight() {
155156
}
156157
}
157158

158-
if (initialLoading) return <Loading />
159+
if (initialLoading) return <WeightSkeleton />
159160

160161
// Period stats computed from chartLogs (period-scoped server fetch). For "All" prefer
161162
// the server-computed aggregate since it isn't capped at 1000.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { ScrollView, View } from 'react-native'
2+
import { Card, Screen, Skeleton, SkeletonStatRow } from '../ui'
3+
4+
// A card section-header placeholder: brand-dot + title line, optional right control.
5+
function CardHeaderSkel({ right }: { right?: boolean }) {
6+
return (
7+
<View className="mb-3 flex-row items-center justify-between">
8+
<View className="flex-row items-center gap-2">
9+
<Skeleton width={16} height={16} radius={5} />
10+
<Skeleton width={110} height={15} />
11+
</View>
12+
{right ? <Skeleton width={90} height={22} radius={10} /> : null}
13+
</View>
14+
)
15+
}
16+
17+
// Initial-load skeleton for the Home/Dashboard — content-shaped placeholders laid out
18+
// like the real screen (greeting + Start · KPI strip · volume trend · consistency ·
19+
// last workout · nutrition · muscle balance · weight) so each section fills into its
20+
// own shape instead of popping after a blank spinner. Mirrors WorkoutsSkeleton etc.
21+
export function DashboardSkeleton() {
22+
return (
23+
<Screen>
24+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 32 }}>
25+
<View className="gap-4 py-4">
26+
{/* Header: date + greeting, Start button */}
27+
<View className="flex-row items-start justify-between gap-3">
28+
<View className="flex-1 gap-2">
29+
<Skeleton width={150} height={11} />
30+
<Skeleton width={210} height={26} radius={8} />
31+
</View>
32+
<Skeleton width={92} height={36} radius={8} />
33+
</View>
34+
35+
{/* KPI strip */}
36+
<SkeletonStatRow count={3} />
37+
38+
{/* Volume Trend */}
39+
<Card>
40+
<CardHeaderSkel right />
41+
<Skeleton height={130} radius={12} />
42+
</Card>
43+
44+
{/* Consistency heatmap */}
45+
<Card>
46+
<CardHeaderSkel right />
47+
<Skeleton height={92} radius={12} />
48+
</Card>
49+
50+
{/* Last workout: title + exercise rows */}
51+
<Card className="gap-3">
52+
<Skeleton width={180} height={15} />
53+
{Array.from({ length: 3 }).map((_, i) => (
54+
<View key={i} className="flex-row items-center gap-2.5">
55+
<Skeleton width={44} height={44} radius={12} />
56+
<View className="flex-1 gap-2">
57+
<Skeleton width="60%" height={13} />
58+
<Skeleton width={70} height={16} radius={5} />
59+
</View>
60+
<Skeleton width={56} height={12} />
61+
</View>
62+
))}
63+
</Card>
64+
65+
{/* Today's Nutrition: big number + macro bars */}
66+
<Card className="gap-3">
67+
<Skeleton width={140} height={15} />
68+
<Skeleton width={120} height={30} radius={8} />
69+
{Array.from({ length: 3 }).map((_, i) => (
70+
<Skeleton key={i} height={14} radius={7} />
71+
))}
72+
</Card>
73+
74+
{/* Muscle Balance: donut + legend rows */}
75+
<Card>
76+
<CardHeaderSkel right />
77+
<View className="items-center gap-4">
78+
<Skeleton width={168} height={168} radius={999} />
79+
<View className="w-full gap-3">
80+
{Array.from({ length: 4 }).map((_, i) => (
81+
<Skeleton key={i} height={14} radius={7} />
82+
))}
83+
</View>
84+
</View>
85+
</Card>
86+
87+
{/* Weight card: number + sparkline */}
88+
<Card className="gap-3">
89+
<CardHeaderSkel right />
90+
<View className="flex-row items-center justify-between">
91+
<Skeleton width={90} height={26} radius={8} />
92+
<Skeleton width={32} height={32} radius={10} />
93+
</View>
94+
<Skeleton height={48} radius={10} />
95+
</Card>
96+
</View>
97+
</ScrollView>
98+
</Screen>
99+
)
100+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { View } from 'react-native'
2+
import { Card, PageHeader, Screen, Skeleton, SkeletonList, SkeletonStatRow } from '../ui'
3+
4+
// Initial-load skeleton for the Weight page — content-shaped placeholders laid out
5+
// 1:1 with the real screen (log card · current-weight hero · Avg/Low/High stats ·
6+
// trend chart · history rows) so the data fills into its own shape instead of popping
7+
// after a blank spinner. The PageHeader is the real, live title. Mirrors the pattern
8+
// of WorkoutsSkeleton / ProgramsSkeleton.
9+
export function WeightSkeleton() {
10+
return (
11+
<Screen>
12+
<View className="gap-5 py-4">
13+
<PageHeader
14+
title="Weight"
15+
subtitle="Track your body weight over time"
16+
action={<Skeleton width={44} height={24} radius={999} />}
17+
/>
18+
19+
{/* Log card: title row · stepper input · add date/note · button */}
20+
<Card className="gap-3">
21+
<View className="flex-row items-center justify-between">
22+
<Skeleton width={110} height={18} />
23+
<Skeleton width={70} height={12} />
24+
</View>
25+
<Skeleton height={86} radius={12} />
26+
<Skeleton height={44} radius={12} />
27+
<Skeleton height={48} radius={8} />
28+
</Card>
29+
30+
{/* Current-weight hero */}
31+
<Card>
32+
<Skeleton width={110} height={12} />
33+
<View className="mt-2">
34+
<Skeleton width={130} height={40} radius={8} />
35+
</View>
36+
</Card>
37+
38+
{/* Avg / Low / High */}
39+
<SkeletonStatRow count={3} />
40+
41+
{/* Trend chart: title + period pill + plot */}
42+
<Card>
43+
<View className="mb-3 flex-row items-center justify-between">
44+
<Skeleton width={60} height={16} />
45+
<Skeleton width={132} height={28} radius={12} />
46+
</View>
47+
<Skeleton height={180} radius={12} />
48+
</Card>
49+
50+
{/* History heading */}
51+
<Skeleton width={72} height={14} />
52+
</View>
53+
54+
{/* History rows */}
55+
<SkeletonList count={4} />
56+
</Screen>
57+
)
58+
}

0 commit comments

Comments
 (0)