diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 007d653..50c1129 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -7,6 +7,14 @@
## [Unreleased]
### Added
+- **Mobile Skeleton Loading:** Implemented skeleton loading state for HomeScreen.
+ - **Features:**
+ - Created `SkeletonGroupCard` component mimicking the actual group card layout.
+ - Implemented pulsing opacity animation (`Animated.loop`) for visual feedback.
+ - Used `useTheme` to ensure placeholders match the active theme (Dark/Light).
+ - Replaced generic `ActivityIndicator` with a list of skeletons.
+ - **Technical:** Created `mobile/components/SkeletonGroupCard.js` and updated `mobile/screens/HomeScreen.js`.
+
- **Mobile Accessibility:** Completed accessibility audit for all mobile screens.
- **Features:**
- Added `accessibilityLabel` to all interactive elements (buttons, inputs, list items).
diff --git a/.Jules/todo.md b/.Jules/todo.md
index de49cb8..5f6b553 100644
--- a/.Jules/todo.md
+++ b/.Jules/todo.md
@@ -57,12 +57,12 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines
-- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- - File: `mobile/screens/HomeScreen.js`
- - Context: Replace ActivityIndicator with skeleton group cards
+- [x] **[ux]** Complete skeleton loading for HomeScreen groups
+ - Completed: 2026-02-04
+ - Files: `mobile/screens/HomeScreen.js`, `mobile/components/SkeletonGroupCard.js`
+ - Context: Replaced ActivityIndicator with pulsing skeleton cards matching list layout
- Impact: Better loading experience, less jarring
- - Size: ~40 lines
- - Added: 2026-01-01
+ - Size: ~70 lines
- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
diff --git a/mobile/components/SkeletonGroupCard.js b/mobile/components/SkeletonGroupCard.js
new file mode 100644
index 0000000..c7d17ab
--- /dev/null
+++ b/mobile/components/SkeletonGroupCard.js
@@ -0,0 +1,87 @@
+import React, { useEffect, useRef } from "react";
+import { View, StyleSheet, Animated } from "react-native";
+import { Card, useTheme } from "react-native-paper";
+
+const SkeletonGroupCard = () => {
+ const theme = useTheme();
+ const opacity = useRef(new Animated.Value(0.3)).current;
+
+ useEffect(() => {
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(opacity, {
+ toValue: 0.7,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacity, {
+ toValue: 0.3,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ ])
+ ).start();
+ }, []);
+
+ const styles = StyleSheet.create({
+ card: {
+ marginBottom: 16,
+ backgroundColor: theme.colors.surface,
+ },
+ row: {
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 16,
+ },
+ avatarPlaceholder: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: theme.colors.surfaceVariant,
+ marginRight: 16,
+ },
+ textContainer: {
+ flex: 1,
+ justifyContent: "center",
+ },
+ titlePlaceholder: {
+ height: 20,
+ width: "60%",
+ backgroundColor: theme.colors.surfaceVariant,
+ borderRadius: 4,
+ },
+ cardContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ },
+ statusPlaceholder: {
+ height: 14,
+ width: "40%",
+ backgroundColor: theme.colors.surfaceVariant,
+ borderRadius: 4,
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SkeletonGroupCard;
diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js
index 373bb0a..93f5b45 100644
--- a/mobile/screens/HomeScreen.js
+++ b/mobile/screens/HomeScreen.js
@@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
- ActivityIndicator,
Appbar,
Avatar,
Button,
@@ -12,6 +11,7 @@ import {
TextInput,
useTheme,
} from "react-native-paper";
+import SkeletonGroupCard from "../components/SkeletonGroupCard";
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -33,7 +33,7 @@ const HomeScreen = ({ navigation }) => {
const showModal = () => setModalVisible(true);
const hideModal = () => setModalVisible(false);
- // Calculate settlement status for a group
+ // Calculate settlement status for a group (owes/owed)
const calculateSettlementStatus = async (groupId, userId) => {
try {
const response = await getOptimizedSettlements(groupId);
@@ -256,8 +256,10 @@ const HomeScreen = ({ navigation }) => {
{isLoading ? (
-
-
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
) : (