Skip to content

Commit 8ecb66b

Browse files
Raoof128claude
andcommitted
fix: critical UX audit fixes — scroll, animations, colors, feedback
Batch 1 of full audit fixes (10 critical issues): 1. Auto-scroll only when at bottom — stops hijacking scroll during streaming 2. Staggered pulsing dots — 150ms delay between each dot for wave effect 3. Follow-up chips press feedback — haptics + hover + pressed scale 4. Error banner dismiss button — X button to clear errors 5. Search no-results state — "No books match" message 6. Hardcoded colors replaced with theme tokens across 6 files: - Onboarding: all 8 hex values + SafeAreaView wrapper - VerseCard: expandToggle, actionButton hover - HistoryDrawer: editInput, newChatButton hover - read.tsx: search, bookCard, bookAbbr - chapter reader: 12 hardcoded values replaced Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c83343 commit 8ecb66b

7 files changed

Lines changed: 127 additions & 65 deletions

File tree

app/(tabs)/read.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ export default function ReadScreen() {
168168
columnWrapperStyle={styles.row}
169169
contentContainerStyle={styles.listContent}
170170
showsVerticalScrollIndicator={false}
171+
ListEmptyComponent={
172+
search.trim().length > 0 ? (
173+
<View style={styles.emptySearch}>
174+
<Text style={styles.emptySearchText}>No books match "{search}"</Text>
175+
</View>
176+
) : null
177+
}
171178
/>
172179
</Animated.View>
173180
</SafeAreaView>
@@ -266,11 +273,11 @@ const styles = StyleSheet.create({
266273
},
267274
bookCard: {
268275
flex: 1,
269-
backgroundColor: "rgba(255,255,255,0.06)",
276+
backgroundColor: colors.glass,
270277
borderRadius: 14,
271278
padding: 16,
272279
borderWidth: 1,
273-
borderColor: "rgba(255,255,255,0.10)",
280+
borderColor: colors.glassBorder,
274281
minHeight: 80,
275282
},
276283
bookCardHovered: {
@@ -284,23 +291,23 @@ const styles = StyleSheet.create({
284291
searchContainer: {
285292
flexDirection: "row",
286293
alignItems: "center",
287-
backgroundColor: "rgba(255,255,255,0.06)",
294+
backgroundColor: colors.glass,
288295
borderWidth: 1,
289-
borderColor: "rgba(255,255,255,0.10)",
296+
borderColor: colors.glassBorder,
290297
borderRadius: 14,
291298
marginHorizontal: 20,
292299
marginBottom: 16,
293300
paddingHorizontal: 14,
294301
paddingVertical: 10,
295302
},
296303
searchIcon: { marginRight: 10 },
297-
searchInput: { flex: 1, color: "#F0F0F5", fontSize: 14 },
304+
searchInput: { flex: 1, color: colors.textPrimary, fontSize: 14, fontFamily: fonts.ui },
298305
searchContainerFocused: {
299306
borderColor: "rgba(138, 43, 226, 0.35)",
300307
backgroundColor: "rgba(255, 255, 255, 0.08)",
301308
},
302309
bookAbbr: {
303-
color: "#A855F7",
310+
color: colors.purpleGlow,
304311
fontSize: 10,
305312
fontWeight: "700",
306313
letterSpacing: 1.5,
@@ -318,4 +325,13 @@ const styles = StyleSheet.create({
318325
fontFamily: fonts.ui,
319326
color: colors.textGhost,
320327
},
328+
emptySearch: {
329+
alignItems: "center",
330+
paddingTop: 40,
331+
},
332+
emptySearchText: {
333+
color: colors.textMuted,
334+
fontSize: 14,
335+
fontFamily: fonts.ui,
336+
},
321337
});

app/chat/[id].tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { View, Text, FlatList, KeyboardAvoidingView, Platform, Pressable, StyleS
33
import { useLocalSearchParams, useRouter } from "expo-router";
44
import { SafeAreaView } from "react-native-safe-area-context";
55
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
6-
import { ChevronLeft, Share2, ChevronDown, Sparkles } from "lucide-react-native";
6+
import { ChevronLeft, Share2, ChevronDown, Sparkles, X } from "lucide-react-native";
7+
import * as Haptics from "expo-haptics";
78
import { supabase } from "../../lib/supabase";
89
import { useChat } from "../../lib/chat";
910
import { ChatBubble } from "../../components/ChatBubble";
@@ -37,8 +38,10 @@ export default function ChatScreen() {
3738

3839
const [messages, setMessages] = useState<DisplayMessage[]>([]);
3940
const [showScrollButton, setShowScrollButton] = useState(false);
41+
const [errorDismissed, setErrorDismissed] = useState(false);
4042
const flatListRef = useRef<FlatList>(null);
4143
const hasSentInitial = useRef(false);
44+
const isAtBottom = useRef(true);
4245

4346
const isNewChat = id === "new" || id.startsWith("new-");
4447

@@ -52,6 +55,10 @@ export default function ChatScreen() {
5255
}
5356
}, [id]);
5457

58+
useEffect(() => {
59+
if (error) setErrorDismissed(false);
60+
}, [error]);
61+
5562
useEffect(() => {
5663
if (initialMessage && !hasSentInitial.current) {
5764
hasSentInitial.current = true;
@@ -205,12 +212,17 @@ export default function ChatScreen() {
205212
/>
206213
)}
207214
contentContainerStyle={styles.messageList}
208-
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })}
215+
onContentSizeChange={() => {
216+
if (isAtBottom.current) {
217+
flatListRef.current?.scrollToEnd({ animated: true });
218+
}
219+
}}
209220
onScroll={(e) => {
210221
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
211222
const distanceFromBottom =
212223
contentSize.height - layoutMeasurement.height - contentOffset.y;
213224
setShowScrollButton(distanceFromBottom > 100);
225+
isAtBottom.current = distanceFromBottom <= 100;
214226
}}
215227
scrollEventThrottle={100}
216228
ListEmptyComponent={
@@ -226,7 +238,18 @@ export default function ChatScreen() {
226238
displayMessages.length > 0 && !isStreaming ? (
227239
<View style={styles.followUps}>
228240
{FOLLOW_UPS.map((text) => (
229-
<Pressable key={text} onPress={() => handleSend(text)} style={styles.followUpChip}>
241+
<Pressable
242+
key={text}
243+
onPress={() => {
244+
if (Platform.OS !== "web") Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
245+
handleSend(text);
246+
}}
247+
style={({ pressed, hovered }: any) => [
248+
styles.followUpChip,
249+
hovered && styles.followUpChipHovered,
250+
pressed && styles.followUpChipPressed,
251+
]}
252+
>
230253
<Text style={styles.followUpText}>{text}</Text>
231254
</Pressable>
232255
))}
@@ -235,13 +258,16 @@ export default function ChatScreen() {
235258
}
236259
/>
237260

238-
{error && (
261+
{error && !errorDismissed && (
239262
<View
240263
style={styles.errorBanner}
241264
accessibilityRole="alert"
242265
accessibilityLabel={`Error: ${error}`}
243266
>
244267
<Text style={styles.errorText}>{error}</Text>
268+
<Pressable onPress={() => setErrorDismissed(true)} style={styles.errorDismiss}>
269+
<X size={14} color={colors.error} />
270+
</Pressable>
245271
</View>
246272
)}
247273

@@ -373,6 +399,9 @@ const styles = StyleSheet.create({
373399
shadowRadius: 8,
374400
},
375401
errorBanner: {
402+
flexDirection: "row",
403+
alignItems: "center",
404+
justifyContent: "space-between",
376405
backgroundColor: colors.errorBg,
377406
borderWidth: 1,
378407
borderColor: colors.error,
@@ -382,10 +411,14 @@ const styles = StyleSheet.create({
382411
marginBottom: 4,
383412
borderRadius: 10,
384413
},
414+
errorDismiss: {
415+
padding: 4,
416+
},
385417
errorText: {
386418
color: colors.error,
387419
fontSize: 13,
388420
fontFamily: fonts.ui,
421+
flex: 1,
389422
},
390423
followUps: {
391424
flexDirection: "row",
@@ -402,6 +435,14 @@ const styles = StyleSheet.create({
402435
paddingHorizontal: 12,
403436
paddingVertical: 6,
404437
},
438+
followUpChipHovered: {
439+
backgroundColor: colors.purpleAccent,
440+
borderColor: colors.purple,
441+
},
442+
followUpChipPressed: {
443+
backgroundColor: colors.purpleAccent,
444+
transform: [{ scale: 0.97 }],
445+
},
405446
followUpText: {
406447
color: colors.purpleGlow,
407448
fontSize: 12,

app/reader/[bookId]/[chapter].tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ const styles = StyleSheet.create({
480480
justifyContent: "center",
481481
},
482482
headerButtonHovered: {
483-
backgroundColor: "rgba(255,255,255,0.06)",
483+
backgroundColor: colors.glass,
484484
borderRadius: 10,
485485
},
486486
headerNav: {
@@ -499,8 +499,8 @@ const styles = StyleSheet.create({
499499
justifyContent: "center",
500500
},
501501
navArrowHovered: {
502-
backgroundColor: "rgba(138, 43, 226, 0.10)",
503-
borderColor: "rgba(138, 43, 226, 0.20)",
502+
backgroundColor: colors.purpleMist,
503+
borderColor: colors.purpleBorder,
504504
},
505505
navArrowDisabled: {
506506
opacity: 0.4,
@@ -512,11 +512,11 @@ const styles = StyleSheet.create({
512512
},
513513
progressBarBg: {
514514
height: 2,
515-
backgroundColor: "rgba(255,255,255,0.05)",
515+
backgroundColor: colors.glass,
516516
},
517517
progressBarFill: {
518518
height: 2,
519-
backgroundColor: "#8A2BE2",
519+
backgroundColor: colors.purple,
520520
},
521521
centered: {
522522
flex: 1,
@@ -558,18 +558,18 @@ const styles = StyleSheet.create({
558558
headingLine: {
559559
flex: 1,
560560
height: 1,
561-
backgroundColor: "rgba(255,255,255,0.06)",
561+
backgroundColor: colors.glass,
562562
},
563563
headingText: {
564-
color: "#9494A8",
564+
color: colors.textSecondary,
565565
fontSize: 11,
566566
fontWeight: "700",
567567
letterSpacing: 3,
568568
textTransform: "uppercase",
569569
paddingHorizontal: 12,
570570
},
571571
chapterNumLarge: {
572-
color: "#A855F7",
572+
color: colors.purpleGlow,
573573
fontSize: 48,
574574
fontWeight: "200",
575575
textAlign: "center",
@@ -640,8 +640,8 @@ const styles = StyleSheet.create({
640640
borderColor: colors.glassBorder,
641641
},
642642
bottomButtonHovered: {
643-
backgroundColor: "rgba(138, 43, 226, 0.06)",
644-
borderColor: "rgba(138, 43, 226, 0.20)",
643+
backgroundColor: colors.purpleMist,
644+
borderColor: colors.purpleBorder,
645645
},
646646
bottomButtonPressed: {
647647
backgroundColor: colors.purpleAccent,
@@ -659,7 +659,7 @@ const styles = StyleSheet.create({
659659
color: colors.textGhost,
660660
},
661661
verseLineSelected: {
662-
backgroundColor: "rgba(138, 43, 226, 0.08)",
662+
backgroundColor: colors.purpleMist,
663663
borderRadius: 8,
664664
marginHorizontal: -4,
665665
paddingHorizontal: 8,
@@ -679,35 +679,35 @@ const styles = StyleSheet.create({
679679
paddingHorizontal: 12,
680680
paddingVertical: 6,
681681
borderRadius: 8,
682-
backgroundColor: "rgba(255,255,255,0.06)",
682+
backgroundColor: colors.glass,
683683
borderWidth: 1,
684-
borderColor: "rgba(255,255,255,0.10)",
684+
borderColor: colors.glassBorder,
685685
},
686686
verseActionPrimary: {
687-
backgroundColor: "rgba(138, 43, 226, 0.12)",
688-
borderColor: "rgba(138, 43, 226, 0.25)",
687+
backgroundColor: colors.purpleMist,
688+
borderColor: colors.purpleAccent,
689689
},
690690
verseActionRow: {
691691
flexDirection: "row",
692692
alignItems: "center",
693693
},
694694
verseActionText: {
695-
color: "#9494A8",
695+
color: colors.textSecondary,
696696
fontSize: 12,
697697
},
698698
verseActionPrimaryText: {
699-
color: "#A855F7",
699+
color: colors.purpleGlow,
700700
},
701701
copiedBadge: {
702-
color: "#A855F7",
702+
color: colors.purpleGlow,
703703
fontSize: 12,
704704
fontStyle: "italic",
705705
},
706706
verseActionActive: {
707-
backgroundColor: "rgba(138, 43, 226, 0.12)",
708-
borderColor: "rgba(138, 43, 226, 0.25)",
707+
backgroundColor: colors.purpleMist,
708+
borderColor: colors.purpleBorder,
709709
},
710710
verseActionActiveText: {
711-
color: "#A855F7",
711+
color: colors.purpleGlow,
712712
},
713713
});

components/ChatBubble.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { useEffect, useState } from "react";
22
import { View, Text, Pressable, Platform, StyleSheet } from "react-native";
33
import { Copy, RotateCcw, ThumbsUp, ThumbsDown } from "lucide-react-native";
44
import Animated, {
5-
65
FadeInLeft,
76
FadeInRight,
87
useAnimatedStyle,
98
useSharedValue,
9+
withDelay,
1010
withRepeat,
1111
withTiming,
1212
} from "react-native-reanimated";
@@ -16,32 +16,35 @@ import { VerseCard } from "./VerseCard";
1616
import { colors, fonts } from "../lib/theme";
1717
import type { Verse } from "../lib/types";
1818

19-
function PulsingDot() {
19+
function PulsingDotItem({ delay }: { delay: number }) {
2020
const opacity = useSharedValue(0.3);
2121

2222
useEffect(() => {
23-
opacity.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
23+
opacity.value = withDelay(
24+
delay,
25+
withRepeat(withTiming(1, { duration: 600 }), -1, true)
26+
);
2427
}, []);
2528

2629
const style = useAnimatedStyle(() => ({
2730
opacity: opacity.value,
2831
}));
2932

33+
return (
34+
<Animated.View
35+
style={[
36+
{ width: 6, height: 6, borderRadius: 3, backgroundColor: colors.purple },
37+
style,
38+
]}
39+
/>
40+
);
41+
}
42+
43+
function PulsingDot() {
3044
return (
3145
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingVertical: 8 }}>
3246
{[0, 1, 2].map((i) => (
33-
<Animated.View
34-
key={i}
35-
style={[
36-
{
37-
width: 6,
38-
height: 6,
39-
borderRadius: 3,
40-
backgroundColor: colors.purple,
41-
},
42-
style,
43-
]}
44-
/>
47+
<PulsingDotItem key={i} delay={i * 150} />
4548
))}
4649
</View>
4750
);
@@ -169,9 +172,9 @@ const styles = StyleSheet.create({
169172
marginVertical: 6,
170173
},
171174
userBubble: {
172-
backgroundColor: "rgba(138, 43, 226, 0.08)",
175+
backgroundColor: colors.purpleMist,
173176
borderWidth: 1,
174-
borderColor: "rgba(138, 43, 226, 0.15)",
177+
borderColor: colors.purpleBorder,
175178
borderRadius: 20,
176179
borderBottomRightRadius: 4,
177180
paddingHorizontal: 16,

0 commit comments

Comments
 (0)