Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Splitwiser UI/UX Changelog


## [Unreleased]

### Added
- Created reusable `Skeleton` UI primitive in `mobile/components/ui/Skeleton.js` with pulsing animation.
- Created `GroupListSkeleton` in `mobile/components/skeletons/GroupListSkeleton.js` using the new `Skeleton` component.

### Changed
- Replaced `ActivityIndicator` with `GroupListSkeleton` in `mobile/screens/HomeScreen.js` for a better loading experience.

Comment on lines +3 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate ## [Unreleased] section.

A new ## [Unreleased] block is added at lines 4–11 while another ## [Unreleased] section already exists at line 17. Two sections with the same heading in one changelog are confusing and will also trip MD024 (duplicate headings). Merge the new Added/Changed entries into the existing [Unreleased] section instead of creating a second one.

✏️ Proposed merge
-
-## [Unreleased]
-
-### Added
-- Created reusable `Skeleton` UI primitive in `mobile/components/ui/Skeleton.js` with pulsing animation.
-- Created `GroupListSkeleton` in `mobile/components/skeletons/GroupListSkeleton.js` using the new `Skeleton` component.
-
-### Changed
-- Replaced `ActivityIndicator` with `GroupListSkeleton` in `mobile/screens/HomeScreen.js` for a better loading experience.
-
 > All UI/UX changes made by Jules automated enhancement agent.

 ---

 ## [Unreleased]

 ### Added
+- Created reusable `Skeleton` UI primitive in `mobile/components/ui/Skeleton.js` with pulsing animation.
+- Created `GroupListSkeleton` in `mobile/components/skeletons/GroupListSkeleton.js` using the new `Skeleton` component.
 - **Password Strength Meter:** ...
@@
 ### Changed
+- Replaced `ActivityIndicator` with `GroupListSkeleton` in `mobile/screens/HomeScreen.js` for a better loading experience.
 - **Web App:** ...
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 6-6: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/changelog.md around lines 3 - 12, The changelog contains two
identical "## [Unreleased]" headings; merge the new entries under the existing
"## [Unreleased]" block instead of adding a duplicate heading: move the "Added"
lines referencing Skeleton (mobile/components/ui/Skeleton.js) and
GroupListSkeleton (mobile/components/skeletons/GroupListSkeleton.js) and the
"Changed" line about replacing ActivityIndicator with GroupListSkeleton
(mobile/screens/HomeScreen.js) into the existing "## [Unreleased]" section,
combining them into the appropriate "Added"/"Changed" subsections and removing
the second "## [Unreleased]" header to avoid duplicate headings and MD024
violations.

> All UI/UX changes made by Jules automated enhancement agent.

---
Expand Down
4 changes: 3 additions & 1 deletion .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-04-16
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- File: `mobile/screens/HomeScreen.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
Expand Down
41 changes: 41 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeletonItem = () => (
<Card style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<View {...props}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
Comment on lines +8 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

react-native-paper Card.Title title prop accepts ReactNode or only string in v5

💡 Result:

In react-native-paper v5, Card.Title’s title prop accepts React.ReactNode (not only a string). Specifically, the CardTitle docs state: “title (required) Type: React.ReactNode” for the title text. So you can pass a React element/node (e.g., JSX that results in a renderable node), not just a plain string.

Citations:


🏁 Script executed:

# Check react-native-paper version
fd -type f -name "package.json" | xargs grep -l "react-native-paper" 2>/dev/null | head -5

Repository: Devasy/splitwiser

Length of output: 231


🏁 Script executed:

# Look for other Card.Title usages in the codebase
rg "Card\.Title" --type js --type jsx --type ts --type tsx -A 5 | head -100

Repository: Devasy/splitwiser

Length of output: 88


🏁 Script executed:

# Check the full context of the file mentioned in the review
fd -type f -name "GroupListSkeleton.js" -o -name "GroupListSkeleton.jsx" -o -name "GroupListSkeleton.ts" -o -name "GroupListSkeleton.tsx" | xargs cat -n 2>/dev/null

Repository: Devasy/splitwiser

Length of output: 231


🏁 Script executed:

# Find package.json and check react-native-paper version
find . -name "package.json" -type f | head -1 | xargs cat | grep -A 2 -B 2 "react-native-paper"

Repository: Devasy/splitwiser

Length of output: 241


🏁 Script executed:

# Search for Card.Title usages in JS/TS files
rg "Card\.Title" -A 5 --max-count 20

Repository: Devasy/splitwiser

Length of output: 3631


🏁 Script executed:

# Find and display GroupListSkeleton.js
fd "GroupListSkeleton" -e js -e jsx -e ts -e tsx | xargs cat -n

Repository: Devasy/splitwiser

Length of output: 1250


🌐 Web query:

react-native-paper 5.14.5 Card.Title JSX element rendering Text component

💡 Result:

In react-native-paper v5.14.5, Card.Title’s title prop is intended to be a ReactNode “text” value (practically: a string or a Text-based node), and it is implemented/rendered such that you generally cannot pass a React Native component as you would normally in JSX. 1) What Card.Title expects (types/docs) - Card.Title’s title prop type is React.ReactNode and the docs explicitly note it “will only accept a string or …-based node” (i.e., it’s constrained to text-like children rather than arbitrary JSX/React Native components). 2) Why your JSX like ... fails - Community reports show that passing a react-native Text (or paper Text) directly to Card.Title’s title often triggers errors such as “Text cannot be used as a JSX component…” after upgrading paper. - The underlying limitation has been discussed historically: even if the prop type is React.ReactNode, Card.Title is rendered inside paper’s styled text implementation, which effectively limits what you can nest inside. 3) What to do instead - Pass a string (recommended): <Card.Title title={Top ${sites.length} Sights in ${country.name}} /> - If you need rich formatting (e.g., inline icons), prefer the Card.Title left/right props for non-text elements: <Card.Title title="Card Title" left={(props) => <Avatar.Icon {...props} icon="folder" />} right={(props) => <IconButton {...props} icon="dots-vertical" onPress={...} />} /> Bottom line: for v5.14.5, avoid wrapping Card.Title title in elements; use a plain string for title, and use left/right to add other UI next to the title.

Citations:


🌐 Web query:

react-native-paper Card.Title rendering ReactNode behavior issues v5

💡 Result:

In react-native-paper v5, Card.Title’s title/subtitle props accept React.ReactNode, but the component renders them inside a react-native-paper StyledText/Text context. As a result, not all ReactNode types behave well: passing components that internally render native can trigger runtime errors like “Nesting of within is not currently supported”, or formatting/layout issues because they’re effectively treated as “text-like” content. What to do instead 1) Pass plain strings (most reliable): <Card.Title title={Top ${sites.length} Sights} /> 2) If you need inline styling/icons, use the paper Text component (not react-native’s Text), and keep it compatible with the parent Text nesting rules. Many people hit errors by using react-native’s inside Card.Title’s title prop. 3) Prefer the left/right slots for non-text elements (avatar/icons). Card.Title exposes left and right callbacks (with leftStyle/rightStyle wrappers in v5) specifically for element-side customization, rather than trying to inject icons inside the title node. Evidence from v5 docs and related issues - The official CardTitle docs state that title and subtitle are React.ReactNode but “will only accept a string or -based node” (i.e., effectively Text-based content). This matches the observed nesting constraints. - A reported issue documents the root cause: even though the type says React.ReactNode, Card.Title is wrapped in StyledText, meaning it can only accept text nodes; attempting to pass a node that uses results in “Nesting of within is not currently supported”. - The docs also show left/right props as the supported way to render elements on the sides, and v5 includes leftStyle/rightStyle to help with wrapper styling. Related practical footgun (separate from ReactNode nesting) - If you increase title/subtitle fontSize dramatically, there have been bug reports that card height/line-height may not adjust correctly on Android/iOS; suggestions in v5 point to theme variant lineHeight and/or setting lineHeight explicitly in styles. If you share your exact Card.Title usage snippet and the error message/stack trace, I can tell you which of the above cases you’re in and the minimal change to fix it.

Citations:


Avoid passing JSX elements to Card.Title's title prop; use the left prop instead.

In react-native-paper v5.14.5, Card.Title's title prop is rendered inside a StyledText component, which constrains it to text-like content only. Passing JSX elements (like <Skeleton />) can trigger "Nesting of within is not currently supported" errors or layout/height clipping issues, even though the prop accepts React.ReactNode at the type level. The left and right props are the intended way to render custom elements around the title.

Apply the suggested fix: use a plain string or placeholder for title, and render the skeleton via left:

Suggested change
    <Card.Title
-     title={<Skeleton width={120} height={20} />}
+     title=" "
+     titleStyle={{ height: 20 }}
      left={(props) => (
        <View {...props}>
          <Skeleton width={40} height={40} borderRadius={20} />
        </View>
      )}
    />

Verify the output renders correctly on both iOS and Android without layout clipping or warnings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<View {...props}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
<Card.Title
title=" "
titleStyle={{ height: 20 }}
left={(props) => (
<View {...props}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/skeletons/GroupListSkeleton.js` around lines 8 - 15,
Card.Title is receiving a JSX Skeleton via its title prop which can cause Text
nesting and clipping; change GroupListSkeleton.js so Card.Title uses a plain
string (e.g., an empty string or placeholder) for the title prop and move the
Skeleton rendering into the left prop (replace the current left render with the
avatar Skeleton) so the Skeleton component is rendered as a left element rather
than inside the title; update any props passed to the left render function (the
arrow function currently using (props) => <View {...props}>...) to continue
forwarding props to the container for proper layout.

<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
);

const GroupListSkeleton = () => {
return (
<View style={styles.container}>
{[1, 2, 3, 4, 5].map((key) => (
<GroupListSkeletonItem key={key} />
))}
Comment on lines +25 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer stable keys / avoid magic count.

Using an inline array [1,2,3,4,5] is fine but slightly opaque. A small helper (Array.from({ length: 5 })) with key={index} is clearer and makes the count easy to tune. Purely stylistic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/skeletons/GroupListSkeleton.js` around lines 25 - 27,
Replace the inline literal [1,2,3,4,5] used to render GroupListSkeletonItem with
a clearer, tunable helper like Array.from({ length: 5 }) and use the map index
as the stable key (key={index}) when mapping to GroupListSkeletonItem; update
the map callback that currently renders GroupListSkeletonItem to accept (_,
index) and pass index as the key to make the count explicit and keys stable.

</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 16,
},
card: {
marginBottom: 16,
},
});

export default GroupListSkeleton;
54 changes: 54 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius = 4, style }) => {
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;
Comment on lines +5 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing defaults for width / height.

width and height have no defaults, so consumers who forget to pass them get an Animated.View sized to content (effectively 0×0) with no warning. Given this is a reusable primitive, either add sensible defaults or document the required props via PropTypes/JSDoc.

🛡️ Proposed defensive default
-const Skeleton = ({ width, height, borderRadius = 4, style }) => {
+const Skeleton = ({ width = '100%', height = 16, borderRadius = 4, style }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` around lines 5 - 7, The Skeleton component
lacks defaults for width/height so callers who omit them render a 0×0 animated
view; update the Skeleton function to provide sensible defaults for width and
height (e.g., width = '100%' and height = 16) in the parameter destructuring or
add explicit PropTypes/defaultProps or JSDoc to require them; modify the
destructuring in Skeleton (and update any defaultProps or PropTypes for
Skeleton) so consumers get predictable sizing and add a brief JSDoc comment
above the Skeleton declaration documenting the props.


useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 0.7,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);
loop.start();

return () => {
loop.stop();
};
}, [opacity]);

return (
<Animated.View
style={[
styles.skeleton,
{
width,
height,
borderRadius,
backgroundColor: theme.colors.surfaceVariant,
opacity,
},
style,
]}
/>
);
};

const styles = StyleSheet.create({
skeleton: {
overflow: 'hidden',
},
});
Comment on lines +48 to +52
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

overflow: 'hidden' on an empty view is unused.

The skeleton has no children, so overflow: 'hidden' doesn't do anything today. Harmless, but can be removed unless you intend to add a shimmer gradient child later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` around lines 48 - 52, The styles object
contains an unused overflow property on the skeleton style; remove the line
"overflow: 'hidden'," from the const styles = StyleSheet.create({ skeleton: {
... } }) block (or, if you plan to add a shimmer child later, add a clarifying
comment above styles.skeleton explaining why overflow is kept) so the Skeleton
component's style no longer includes an unnecessary property.


export default Skeleton;
5 changes: 2 additions & 3 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency, getCurrencySymbol } from "../utils/currency";
import GroupListSkeleton from "../components/skeletons/GroupListSkeleton";

const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
Expand Down Expand Up @@ -257,9 +258,7 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
</View>
<GroupListSkeleton />
) : (
<FlatList
data={groups}
Expand Down
Loading