Polish mobile and web experience

This commit is contained in:
2026-08-17 00:18:55 -04:00
parent 6c74436092
commit 9929d7321d
28 changed files with 835 additions and 688 deletions
+57 -19
View File
@@ -4,9 +4,15 @@ import { Pressable, type PressableProps, StyleSheet, Text, View } from "react-na
import Swipeable, {
type SwipeableMethods,
} from "react-native-gesture-handler/ReanimatedSwipeable";
import Animated, {
interpolate,
useAnimatedStyle,
type SharedValue,
} from "react-native-reanimated";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { resolveActionForeground } from "@/lib/action-contrast";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
@@ -29,6 +35,38 @@ type SwipeableRowProps = {
contentStyle?: PressableProps["style"];
};
type SwipeActionsProps = {
actions: SwipeAction[];
progress: SharedValue<number>;
onActionPress: (action: SwipeAction) => void;
};
function SwipeActions({ actions, progress, onActionPress }: SwipeActionsProps) {
const styles = useThemedStyles(createSwipeableRowStyles);
const revealStyle = useAnimatedStyle(() => ({
opacity: interpolate(progress.value, [0, 0.02, 0.15], [0, 0, 1], "clamp"),
}));
return (
<Animated.View style={[styles.actions, revealStyle]}>
{actions.map((action) => {
const foreground = resolveActionForeground(action.backgroundColor, action.color);
return (
<Pressable
key={action.key}
style={[styles.actionButton, { backgroundColor: action.backgroundColor }]}
onPress={() => onActionPress(action)}
>
<Ionicons name={action.icon} size={20} color={foreground} />
<Text style={[styles.actionLabel, { color: foreground }]}>{action.label}</Text>
</Pressable>
);
})}
</Animated.View>
);
}
export function SwipeableRow({
children,
actions,
@@ -92,25 +130,20 @@ export function SwipeableRow({
);
}
function renderRightActions() {
function handleActionPress(action: SwipeAction) {
suppressContentPress();
swipeRef.current?.close();
rowOpenRef.current = false;
action.onPress();
}
function renderRightActions(progress: SharedValue<number>) {
return (
<View style={styles.actions}>
{actions.map((action) => (
<Pressable
key={action.key}
style={[styles.actionButton, { backgroundColor: action.backgroundColor }]}
onPress={() => {
suppressContentPress();
swipeRef.current?.close();
rowOpenRef.current = false;
action.onPress();
}}
>
<Ionicons name={action.icon} size={20} color={action.color} />
<Text style={[styles.actionLabel, { color: action.color }]}>{action.label}</Text>
</Pressable>
))}
</View>
<SwipeActions
actions={actions}
progress={progress}
onActionPress={handleActionPress}
/>
);
}
@@ -121,6 +154,7 @@ export function SwipeableRow({
return (
<Swipeable
ref={swipeRef}
containerStyle={styles.container}
renderRightActions={renderRightActions}
overshootRight={false}
onSwipeableOpenStartDrag={suppressContentPress}
@@ -141,11 +175,15 @@ export function SwipeableRow({
const createSwipeableRowStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: {
borderRadius: radii.lg,
overflow: "hidden",
marginBottom: spacing.xs,
},
row: {
backgroundColor: colors.background,
borderRadius: radii.lg,
overflow: "hidden",
marginBottom: spacing.xs,
},
actions: {
flexDirection: "row",