1
.gitignore
vendored
@@ -193,3 +193,4 @@ google-services.json
|
||||
# ---------------------------
|
||||
*.orig.*
|
||||
app-example
|
||||
newDeps/
|
||||
2
app.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Study-Sprint",
|
||||
"name": "Study Sprint",
|
||||
"slug": "Study-Sprint",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Easing,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
const colors = {
|
||||
@@ -15,147 +19,689 @@ const colors = {
|
||||
text: '#ffffff',
|
||||
};
|
||||
|
||||
const timers = [...Array(13).keys()].map((i) => (i === 0 ? 1 : i * 5));
|
||||
const TIMER_OPTIONS = [...Array(13).keys()].map((index) => (index === 0 ? 1 : index * 5));
|
||||
const ITEM_SIZE = width * 0.38;
|
||||
const ITEM_SPACING = (width - ITEM_SIZE) / 2;
|
||||
const TIMER_UNIT_IN_SECONDS = 60;
|
||||
const HOLD_TO_CANCEL_MS = 2000;
|
||||
const CANCEL_ANIMATION_DELAY_MS = 250;
|
||||
const BUTTON_PRESS_IN_MS = 80;
|
||||
const BUTTON_PRESS_OUT_MS = 140;
|
||||
|
||||
/*
|
||||
Har bare skrevet timeren som en egen tab til å begynne med.
|
||||
Planen er at når bruker starter en task så vil de få opp denne timeren
|
||||
som viser TaskName og Description der tallene står nå
|
||||
Kanskje en animert figur hvis vi får tid
|
||||
*/
|
||||
export default function App() {
|
||||
const scrollX = React.useRef(new Animated.Value(0)).current;
|
||||
const [duration, setDuration] = React.useState(timers[0])
|
||||
const timerAnimation = React.useRef(new Animated.Value(height)).current
|
||||
const buttonAnimation = React.useRef(new Animated.Value(0)).current
|
||||
const animation = React.useCallback(() => {
|
||||
Animated.sequence([
|
||||
Animated.timing(buttonAnimation, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(timerAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(timerAnimation, {
|
||||
toValue: height,
|
||||
duration: duration * 1000,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
]) .start(() => {
|
||||
Animated.timing(buttonAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
}).start()
|
||||
})
|
||||
}, [duration])
|
||||
const placeholderTask = {
|
||||
name: 'Read chapter 4',
|
||||
description: 'Focus on the summary questions and write down anything unclear.',
|
||||
};
|
||||
|
||||
const opacity = buttonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0]
|
||||
})
|
||||
const translateY = buttonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200]
|
||||
})
|
||||
function formatTime(totalSeconds: number) {
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function TimerScreen() {
|
||||
const [containerHeight, setContainerHeight] = React.useState(0);
|
||||
const [duration, setDuration] = React.useState(TIMER_OPTIONS[0]);
|
||||
const [timerIsRunning, setIsRunning] = React.useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = React.useState(0);
|
||||
|
||||
const scrollX = React.useRef(new Animated.Value(0)).current;
|
||||
const timerAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const buttonAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const countdownAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const cancelButtonAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const pressedButtonAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const focusModeAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
const cancelOverlayAnimation = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const cancelHoldTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const cancelHoldAnimationDelayRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const runningAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null);
|
||||
const progressAnimationRef = React.useRef<Animated.CompositeAnimation | null>(null);
|
||||
const sessionStartedAtRef = React.useRef<number | null>(null);
|
||||
const sessionDurationMsRef = React.useRef(0);
|
||||
const cancelAccelStartedRef = React.useRef(false);
|
||||
const cancelHoldActiveRef = React.useRef(false);
|
||||
const cancelHoldIdRef = React.useRef(0);
|
||||
const cancelHoldStartedAtRef = React.useRef(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (containerHeight > 0 && !timerIsRunning) {
|
||||
timerAnimation.setValue(containerHeight);
|
||||
}
|
||||
}, [containerHeight, timerIsRunning, timerAnimation]);
|
||||
|
||||
const pressedButtonScale = pressedButtonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0.9],
|
||||
});
|
||||
|
||||
const cancelButtonTranslateY = cancelButtonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [16, 0],
|
||||
});
|
||||
|
||||
// Real timer progress comes from timerAnimation. The cancel hold adds a
|
||||
// temporary visual offset on top so release/cancel logic does not fight the
|
||||
// underlying progress animation.
|
||||
const timerOverlayTranslateY = Animated.add(
|
||||
timerAnimation,
|
||||
cancelOverlayAnimation
|
||||
).interpolate({
|
||||
inputRange: [0, Math.max(containerHeight, 1)],
|
||||
outputRange: [0, Math.max(containerHeight, 1)],
|
||||
extrapolate: 'clamp',
|
||||
});
|
||||
|
||||
const countdownTranslateX = focusModeAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, -width * 0.3],
|
||||
});
|
||||
|
||||
const countdownTranslateY = focusModeAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, -containerHeight * 0.35],
|
||||
});
|
||||
|
||||
const countdownScale = focusModeAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0.55],
|
||||
});
|
||||
|
||||
const startButtonOpacity = buttonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0],
|
||||
});
|
||||
|
||||
const startButtonTranslateY = buttonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200],
|
||||
});
|
||||
|
||||
const pickerOpacity = buttonAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0],
|
||||
});
|
||||
|
||||
const taskDetailsOpacity = taskDetailsAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
const taskDetailsTranslateY = taskDetailsAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [20, 0],
|
||||
});
|
||||
|
||||
const clearCountdownInterval = React.useCallback(() => {
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
countdownRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearCancelHoldTimeouts = React.useCallback(() => {
|
||||
if (cancelHoldTimeoutRef.current) {
|
||||
clearTimeout(cancelHoldTimeoutRef.current);
|
||||
cancelHoldTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (cancelHoldAnimationDelayRef.current) {
|
||||
clearTimeout(cancelHoldAnimationDelayRef.current);
|
||||
cancelHoldAnimationDelayRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const stopRunningAnimations = React.useCallback(() => {
|
||||
runningAnimationRef.current?.stop();
|
||||
runningAnimationRef.current = null;
|
||||
|
||||
progressAnimationRef.current?.stop();
|
||||
progressAnimationRef.current = null;
|
||||
|
||||
cancelOverlayAnimation.stopAnimation();
|
||||
}, [cancelOverlayAnimation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearCountdownInterval();
|
||||
clearCancelHoldTimeouts();
|
||||
stopRunningAnimations();
|
||||
};
|
||||
}, [clearCancelHoldTimeouts, clearCountdownInterval, stopRunningAnimations]);
|
||||
|
||||
const animateButtonPress = React.useCallback(
|
||||
(pressed: boolean) => {
|
||||
Animated.timing(pressedButtonAnimation, {
|
||||
toValue: pressed ? 1 : 0,
|
||||
duration: pressed ? BUTTON_PRESS_IN_MS : BUTTON_PRESS_OUT_MS,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
},
|
||||
[pressedButtonAnimation]
|
||||
);
|
||||
|
||||
const resetSessionValues = React.useCallback(() => {
|
||||
sessionStartedAtRef.current = null;
|
||||
sessionDurationMsRef.current = 0;
|
||||
cancelHoldActiveRef.current = false;
|
||||
cancelAccelStartedRef.current = false;
|
||||
|
||||
timerAnimation.setValue(containerHeight);
|
||||
cancelOverlayAnimation.setValue(0);
|
||||
setTimeRemaining(0);
|
||||
setIsRunning(false);
|
||||
}, [cancelOverlayAnimation, containerHeight, timerAnimation]);
|
||||
|
||||
const finishTimer = React.useCallback(() => {
|
||||
clearCountdownInterval();
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(countdownAnimation, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(focusModeAnimation, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(taskDetailsAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(buttonAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(cancelButtonAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
setIsRunning(false);
|
||||
/* TODO
|
||||
Implement store and send of ellapsed time value in seconds to DB
|
||||
for total time spent statistic
|
||||
*/
|
||||
|
||||
resetSessionValues();
|
||||
});
|
||||
});
|
||||
}, [
|
||||
buttonAnimation,
|
||||
cancelButtonAnimation,
|
||||
clearCountdownInterval,
|
||||
countdownAnimation,
|
||||
focusModeAnimation,
|
||||
resetSessionValues,
|
||||
taskDetailsAnimation,
|
||||
]);
|
||||
|
||||
// This picks up the timer overlay animation from the current Y position and
|
||||
// runs it to the bottom over the remaining session time.
|
||||
const startProgressAnimation = React.useCallback(
|
||||
(fromY: number) => {
|
||||
const elapsedRatio = fromY / containerHeight;
|
||||
const remainingMs = sessionDurationMsRef.current * (1 - elapsedRatio);
|
||||
|
||||
sessionStartedAtRef.current = Date.now() - sessionDurationMsRef.current * elapsedRatio;
|
||||
timerAnimation.setValue(fromY);
|
||||
|
||||
const progressAnimation = Animated.timing(timerAnimation, {
|
||||
toValue: containerHeight,
|
||||
duration: remainingMs,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
|
||||
progressAnimationRef.current = progressAnimation;
|
||||
progressAnimation.start(({ finished }) => {
|
||||
progressAnimationRef.current = null;
|
||||
|
||||
if (!finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
finishTimer();
|
||||
});
|
||||
},
|
||||
[containerHeight, finishTimer, timerAnimation]
|
||||
);
|
||||
|
||||
const runStartSequence = React.useCallback(() => {
|
||||
const runningAnimation = Animated.sequence([
|
||||
Animated.parallel([
|
||||
Animated.timing(buttonAnimation, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(cancelButtonAnimation, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(countdownAnimation, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(timerAnimation, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
Animated.timing(focusModeAnimation, {
|
||||
toValue: 1,
|
||||
duration: 450,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(taskDetailsAnimation, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
runningAnimationRef.current = runningAnimation;
|
||||
runningAnimation.start(({ finished }) => {
|
||||
runningAnimationRef.current = null;
|
||||
|
||||
if (!finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
startProgressAnimation(0);
|
||||
});
|
||||
}, [
|
||||
buttonAnimation,
|
||||
cancelButtonAnimation,
|
||||
countdownAnimation,
|
||||
focusModeAnimation,
|
||||
startProgressAnimation,
|
||||
taskDetailsAnimation,
|
||||
timerAnimation,
|
||||
]);
|
||||
|
||||
const startCountdown = React.useCallback(
|
||||
(totalSeconds: number) => {
|
||||
setTimeRemaining(totalSeconds);
|
||||
clearCountdownInterval();
|
||||
|
||||
countdownRef.current = setInterval(() => {
|
||||
setTimeRemaining((currentTime) => {
|
||||
if (currentTime <= 1) {
|
||||
clearCountdownInterval();
|
||||
return 0;
|
||||
}
|
||||
|
||||
return currentTime - 1;
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
[clearCountdownInterval]
|
||||
);
|
||||
|
||||
const startTimerSession = React.useCallback(() => {
|
||||
if (timerIsRunning || containerHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
setIsRunning(true);
|
||||
|
||||
taskDetailsAnimation.setValue(0);
|
||||
countdownAnimation.setValue(0);
|
||||
cancelOverlayAnimation.setValue(0);
|
||||
|
||||
const totalSeconds = duration * TIMER_UNIT_IN_SECONDS;
|
||||
sessionStartedAtRef.current = Date.now();
|
||||
sessionDurationMsRef.current = totalSeconds * 1000;
|
||||
|
||||
startCountdown(totalSeconds);
|
||||
runStartSequence();
|
||||
}, [
|
||||
cancelOverlayAnimation,
|
||||
containerHeight,
|
||||
countdownAnimation,
|
||||
duration,
|
||||
runStartSequence,
|
||||
startCountdown,
|
||||
taskDetailsAnimation,
|
||||
timerIsRunning,
|
||||
]);
|
||||
|
||||
const cancelTimer = React.useCallback(() => {
|
||||
if (!timerIsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCountdownInterval();
|
||||
clearCancelHoldTimeouts();
|
||||
stopRunningAnimations();
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(cancelButtonAnimation, {
|
||||
toValue: 0,
|
||||
duration: 180,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(taskDetailsAnimation, {
|
||||
toValue: 0,
|
||||
duration: 220,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(focusModeAnimation, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(countdownAnimation, {
|
||||
toValue: 0,
|
||||
duration: 180,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(timerAnimation, {
|
||||
toValue: containerHeight,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(cancelOverlayAnimation, {
|
||||
toValue: 0,
|
||||
duration: 120,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
Animated.timing(buttonAnimation, {
|
||||
toValue: 0,
|
||||
duration: 220,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
resetSessionValues();
|
||||
});
|
||||
});
|
||||
}, [
|
||||
buttonAnimation,
|
||||
cancelButtonAnimation,
|
||||
cancelOverlayAnimation,
|
||||
clearCancelHoldTimeouts,
|
||||
clearCountdownInterval,
|
||||
containerHeight,
|
||||
countdownAnimation,
|
||||
focusModeAnimation,
|
||||
resetSessionValues,
|
||||
stopRunningAnimations,
|
||||
taskDetailsAnimation,
|
||||
timerAnimation,
|
||||
timerIsRunning,
|
||||
]);
|
||||
|
||||
const handleCancelHoldStart = React.useCallback(() => {
|
||||
animateButtonPress(true);
|
||||
cancelHoldIdRef.current += 1;
|
||||
|
||||
const cancelHoldId = cancelHoldIdRef.current;
|
||||
cancelHoldActiveRef.current = true;
|
||||
cancelHoldStartedAtRef.current = Date.now();
|
||||
cancelAccelStartedRef.current = false;
|
||||
|
||||
cancelHoldAnimationDelayRef.current = setTimeout(() => {
|
||||
cancelHoldAnimationDelayRef.current = null;
|
||||
|
||||
if (!cancelHoldActiveRef.current || cancelHoldIdRef.current !== cancelHoldId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The hold starts with normal button feedback. After a short delay, we
|
||||
// begin the accelerated red overlay preview so quick taps do not cause a
|
||||
// jolt, while long holds still clearly show that cancel is about to fire.
|
||||
cancelAccelStartedRef.current = true;
|
||||
cancelOverlayAnimation.setValue(0);
|
||||
|
||||
const elapsedHoldMs = Date.now() - cancelHoldStartedAtRef.current;
|
||||
const remainingHoldMs = Math.max(1, HOLD_TO_CANCEL_MS - elapsedHoldMs);
|
||||
const sessionStartedAt = sessionStartedAtRef.current ?? Date.now();
|
||||
const elapsedAtCancelMs = Date.now() + remainingHoldMs - sessionStartedAt;
|
||||
const expectedProgress = elapsedAtCancelMs / sessionDurationMsRef.current;
|
||||
const clampedProgress = Math.max(0, Math.min(expectedProgress, 1));
|
||||
const expectedYAtCancel = containerHeight * clampedProgress;
|
||||
const cancelOffset = Math.max(0, containerHeight - expectedYAtCancel);
|
||||
|
||||
Animated.timing(cancelOverlayAnimation, {
|
||||
toValue: cancelOffset,
|
||||
duration: remainingHoldMs,
|
||||
easing: Easing.in(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, CANCEL_ANIMATION_DELAY_MS);
|
||||
|
||||
cancelHoldTimeoutRef.current = setTimeout(() => {
|
||||
cancelHoldActiveRef.current = false;
|
||||
cancelHoldIdRef.current += 1;
|
||||
cancelAccelStartedRef.current = false;
|
||||
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
cancelTimer();
|
||||
cancelHoldTimeoutRef.current = null;
|
||||
}, HOLD_TO_CANCEL_MS);
|
||||
}, [animateButtonPress, cancelOverlayAnimation, cancelTimer, containerHeight]);
|
||||
|
||||
const handleCancelHoldEnd = React.useCallback(() => {
|
||||
animateButtonPress(false);
|
||||
cancelHoldActiveRef.current = false;
|
||||
cancelHoldIdRef.current += 1;
|
||||
|
||||
clearCancelHoldTimeouts();
|
||||
|
||||
if (!cancelAccelStartedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAccelStartedRef.current = false;
|
||||
cancelOverlayAnimation.stopAnimation((currentOffset) => {
|
||||
cancelOverlayAnimation.setValue(currentOffset);
|
||||
Animated.timing(cancelOverlayAnimation, {
|
||||
toValue: 0,
|
||||
duration: 750,
|
||||
easing: Easing.in(Easing.bounce),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
});
|
||||
}, [animateButtonPress, cancelOverlayAnimation, clearCancelHoldTimeouts]);
|
||||
|
||||
const handleTimerPickerMomentumEnd = React.useCallback(
|
||||
(event: { nativeEvent: { contentOffset: { x: number } } }) => {
|
||||
if (timerIsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Math.round(event.nativeEvent.contentOffset.x / ITEM_SIZE);
|
||||
const clampedIndex = Math.max(0, Math.min(index, TIMER_OPTIONS.length - 1));
|
||||
setDuration(TIMER_OPTIONS[clampedIndex]);
|
||||
},
|
||||
[timerIsRunning]
|
||||
);
|
||||
|
||||
const renderTimerItem = React.useCallback(
|
||||
({ item, index }: { item: number; index: number }) => {
|
||||
const inputRange = [
|
||||
(index - 1) * ITEM_SIZE,
|
||||
index * ITEM_SIZE,
|
||||
(index + 1) * ITEM_SIZE,
|
||||
];
|
||||
|
||||
const baseOpacity = scrollX.interpolate({
|
||||
inputRange,
|
||||
outputRange: [0.4, 1, 0.4],
|
||||
});
|
||||
|
||||
const opacity = Animated.multiply(baseOpacity, pickerOpacity);
|
||||
const scale = scrollX.interpolate({
|
||||
inputRange,
|
||||
outputRange: [0.7, 1, 0.7],
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.timerOptionItem}>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.text,
|
||||
{
|
||||
opacity,
|
||||
transform: [{ scale }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[pickerOpacity, scrollX]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={(event) => {
|
||||
setContainerHeight(event.nativeEvent.layout.height);
|
||||
}}
|
||||
>
|
||||
<StatusBar hidden />
|
||||
<Animated.View
|
||||
style={[StyleSheet.absoluteFillObject, {
|
||||
height,
|
||||
width,
|
||||
backgroundColor: colors.red,
|
||||
transform: [{
|
||||
translateY: timerAnimation
|
||||
}]
|
||||
}]}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
styles.timerOverlay,
|
||||
{
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 100,
|
||||
opacity,
|
||||
transform: [{
|
||||
translateY
|
||||
}]
|
||||
height: containerHeight,
|
||||
width,
|
||||
transform: [{ translateY: timerOverlayTranslateY }],
|
||||
},
|
||||
]}>
|
||||
]}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
styles.startButtonContainer,
|
||||
{
|
||||
opacity: startButtonOpacity,
|
||||
transform: [{ translateY: startButtonTranslateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={animation}>
|
||||
<View
|
||||
style={styles.roundButton}
|
||||
/>
|
||||
disabled={timerIsRunning}
|
||||
onPress={startTimerSession}
|
||||
onPressIn={() => animateButtonPress(true)}
|
||||
onPressOut={() => animateButtonPress(false)}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.roundButton,
|
||||
{
|
||||
transform: [{ scale: pressedButtonScale }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-text-main text-xl">Start</Text>
|
||||
<Text className="text-text-main text-xl">Sprint</Text>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
pointerEvents={timerIsRunning ? 'auto' : 'none'}
|
||||
style={[
|
||||
styles.cancelButtonContainer,
|
||||
{
|
||||
opacity: cancelButtonAnimation,
|
||||
transform: [{ translateY: cancelButtonTranslateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity onPressIn={handleCancelHoldStart} onPressOut={handleCancelHoldEnd}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.cancelButton,
|
||||
{
|
||||
transform: [{ scale: pressedButtonScale }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-text-main text-xl">Hold to end sprint</Text>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.countdownOverlay,
|
||||
{
|
||||
opacity: countdownAnimation,
|
||||
transform: [
|
||||
{ translateX: countdownTranslateX },
|
||||
{ translateY: countdownTranslateY },
|
||||
{ scale: countdownScale },
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.countdownText}>{formatTime(timeRemaining)}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: height / 3,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
}}>
|
||||
<Animated.FlatList
|
||||
data={timers}
|
||||
keyExtractor={item => item.toString()}
|
||||
style={[
|
||||
styles.timerPickerWrapper,
|
||||
{
|
||||
top: containerHeight / 3,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Animated.FlatList
|
||||
data={TIMER_OPTIONS}
|
||||
scrollEnabled={!timerIsRunning}
|
||||
keyExtractor={(item) => item.toString()}
|
||||
horizontal
|
||||
bounces={false}
|
||||
onScroll={Animated.event(
|
||||
[{nativeEvent: {contentOffset: {x: scrollX}}}],
|
||||
{ useNativeDriver: true}
|
||||
)}
|
||||
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } } }], {
|
||||
useNativeDriver: true,
|
||||
})}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={ev => {
|
||||
const index = Math.round(ev.nativeEvent.contentOffset.x / ITEM_SIZE)
|
||||
setDuration(timers[index]);
|
||||
}}
|
||||
onMomentumScrollEnd={handleTimerPickerMomentumEnd}
|
||||
snapToInterval={ITEM_SIZE}
|
||||
decelerationRate={"fast"}
|
||||
style={{flexGrow: 0}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: ITEM_SPACING
|
||||
}}
|
||||
renderItem={({item, index}) => {
|
||||
const inputRange = [
|
||||
(index - 1) * ITEM_SIZE,
|
||||
index * ITEM_SIZE,
|
||||
(index + 1) * ITEM_SIZE,
|
||||
]
|
||||
decelerationRate="fast"
|
||||
style={styles.timerPickerList}
|
||||
contentContainerStyle={styles.timerPickerContent}
|
||||
renderItem={renderTimerItem}
|
||||
/>
|
||||
</View>
|
||||
|
||||
const opacity = scrollX.interpolate({
|
||||
inputRange,
|
||||
outputRange: [.4, 1, .4]
|
||||
})
|
||||
const scale = scrollX.interpolate({
|
||||
inputRange,
|
||||
outputRange: [.7, 1, .7]
|
||||
})
|
||||
return <View style={{width: ITEM_SIZE, justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Animated.Text style={[styles.text, {
|
||||
opacity,
|
||||
transform: [{
|
||||
scale
|
||||
}]
|
||||
|
||||
}]}>
|
||||
{item}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.taskDetails,
|
||||
{
|
||||
opacity: taskDetailsOpacity,
|
||||
transform: [{ translateY: taskDetailsTranslateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.taskName}>{placeholderTask.name}</Text>
|
||||
<Text style={styles.taskDescription}>{placeholderTask.description}</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -165,16 +711,98 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
timerOverlay: {
|
||||
backgroundColor: colors.red,
|
||||
},
|
||||
startButtonContainer: {
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 100,
|
||||
},
|
||||
roundButton: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 80,
|
||||
backgroundColor: colors.red,
|
||||
backgroundColor: '#beb9a7',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timerPickerWrapper: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
},
|
||||
timerPickerList: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
timerPickerContent: {
|
||||
paddingHorizontal: ITEM_SPACING,
|
||||
},
|
||||
timerOptionItem: {
|
||||
width: ITEM_SIZE,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: ITEM_SIZE * 0.8,
|
||||
fontFamily: 'Menlo',
|
||||
color: colors.text,
|
||||
fontWeight: '900',
|
||||
}
|
||||
},
|
||||
taskDetails: {
|
||||
position: 'absolute',
|
||||
top: height * 0.34,
|
||||
left: 32,
|
||||
right: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
taskName: {
|
||||
color: colors.text,
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
textAlign: 'center',
|
||||
},
|
||||
taskDescription: {
|
||||
color: colors.text,
|
||||
fontSize: 24,
|
||||
lineHeight: 32,
|
||||
marginTop: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
countdownText: {
|
||||
fontSize: ITEM_SIZE * 0.32,
|
||||
fontFamily: 'Menlo',
|
||||
color: colors.text,
|
||||
fontWeight: '900',
|
||||
textAlign: 'center',
|
||||
},
|
||||
cancelButtonContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 44,
|
||||
alignItems: 'center',
|
||||
zIndex: 2,
|
||||
},
|
||||
cancelButton: {
|
||||
minWidth: 112,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(155, 155, 155, 0.35)',
|
||||
backgroundColor: '#beb9a7',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 22,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
countdownOverlay: {
|
||||
position: 'absolute',
|
||||
top: height / 3,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
BIN
assets/study-sprint-image-pack/android-icon-background.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/study-sprint-image-pack/android-icon-foreground.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
assets/study-sprint-image-pack/android-icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/study-sprint-image-pack/favicon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
assets/study-sprint-image-pack/icon.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
assets/study-sprint-image-pack/master.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
assets/study-sprint-image-pack/splash-icon.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
92
notes/work-report-timer-2026-04-21.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Timer Element Work Report
|
||||
|
||||
## #Overview
|
||||
This note documents the timer work completed by **Chris Sanden** in the Study-Sprint project.
|
||||
|
||||
The git history shows a dedicated timer commit:
|
||||
- Commit: `d50301cb04837b196110cea43ff15c0493c5fac2`
|
||||
- Short hash: `d50301c`
|
||||
- Author: `Chris Sanden <c.sanden@outlook.com>`
|
||||
- Date: `2026-04-21`
|
||||
- Message: `First draft of timer element`
|
||||
- File added: `app/(tabs)/timer.tsx`
|
||||
- Branch references at inspection time: `timer`, `origin/timer`
|
||||
|
||||
---
|
||||
|
||||
## #ImplementedFeatures
|
||||
|
||||
### #TimerTab
|
||||
Created the first draft of a standalone timer screen:
|
||||
- Added `app/(tabs)/timer.tsx`
|
||||
- Implemented the timer as its own tab while the final task-start flow is still planned
|
||||
- Used React Native and Expo tab routing conventions already present in the project
|
||||
|
||||
---
|
||||
|
||||
### #DurationSelector
|
||||
Implemented a horizontal animated selector for timer durations:
|
||||
- Uses `Animated.FlatList`
|
||||
- Supports snap scrolling with `snapToInterval`
|
||||
- Shows selectable durations from `1` to `60`
|
||||
- Uses scaled and faded text animation so the centered duration is emphasized
|
||||
- Updates the selected duration when scrolling ends
|
||||
|
||||
---
|
||||
|
||||
### #TimerAnimation
|
||||
Implemented the first timer start animation:
|
||||
- Added a circular start button
|
||||
- Button fades and moves down after the timer starts
|
||||
- Timer overlay animates into view
|
||||
- Timer overlay then animates out based on the selected duration
|
||||
- Uses `Animated.sequence` and `useNativeDriver`
|
||||
|
||||
---
|
||||
|
||||
## #UserInterface
|
||||
|
||||
The timer screen includes:
|
||||
- Full-screen dark background
|
||||
- Red timer overlay
|
||||
- Large centered duration numbers
|
||||
- Circular red start button near the bottom of the screen
|
||||
- Hidden status bar for a focused timer view
|
||||
|
||||
The visual direction is a simple first draft intended to make the timer interaction testable before deeper integration with tasks.
|
||||
|
||||
---
|
||||
|
||||
## #PlannedIntegration
|
||||
|
||||
The in-code note describes the intended next step:
|
||||
- Keep the timer as a separate tab initially
|
||||
- Later open the timer when a user starts a task
|
||||
- Replace the current duration-number area with task information such as:
|
||||
- Task name
|
||||
- Task description
|
||||
- Potentially add an animated character or visual element if time allows
|
||||
|
||||
---
|
||||
|
||||
## #GitEvidence
|
||||
|
||||
The work attributed to Chris is supported by this git log entry:
|
||||
|
||||
```text
|
||||
d50301c Chris Sanden 2026-04-21 First draft of timer element
|
||||
```
|
||||
|
||||
The commit added one new file:
|
||||
|
||||
```text
|
||||
A app/(tabs)/timer.tsx
|
||||
```
|
||||
|
||||
The file was later also touched in commit `cb6368a` by `Teodor` on `2026-04-22` as part of broader UI and routing fixes. The original timer implementation documented here is the `d50301c` commit authored by Chris.
|
||||
|
||||
---
|
||||
|
||||
## #Conclusion
|
||||
|
||||
Chris implemented the first functional timer draft for the application. The work established a standalone timer tab, duration selection, animated start behavior, and a clear path for later connecting the timer to task-start workflows.
|
||||
151
notes/work-report-timer-2026-04-22.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Timer UI and Countdown Work Report
|
||||
|
||||
## #Overview
|
||||
Today the standalone timer screen was developed further before wiring it into the task system.
|
||||
|
||||
The main focus was improving the timer interaction and learning how the React Native animation flow works. The timer is still being treated as its own tab for now, with placeholder task data used in place of real task integration.
|
||||
|
||||
---
|
||||
|
||||
## #ImplementedFeatures
|
||||
|
||||
### #TaskInformationPlaceholder
|
||||
Added placeholder task information to the timer screen:
|
||||
- Placeholder task name
|
||||
- Placeholder task description
|
||||
- Fade-in animation when the timer starts
|
||||
- Fade-out animation when the timer finishes
|
||||
|
||||
This prepares the timer UI for the later task integration, where the placeholder values can be replaced by real task data.
|
||||
|
||||
---
|
||||
|
||||
### #AdjacentTimerFade
|
||||
Updated the timer duration selector so adjacent numbers fade away when the timer starts:
|
||||
- The centered selected value remains visible
|
||||
- Neighboring values fade out during the active timer state
|
||||
- Neighboring values are intended to fade back in after the timer finishes
|
||||
|
||||
This was implemented by separating the normal picker opacity from the active timer opacity and combining them with `Animated.add` and `Animated.multiply`.
|
||||
|
||||
---
|
||||
|
||||
### #MeasuredTimerHeight
|
||||
Started adjusting the timer overlay to use the measured screen/container height:
|
||||
- Added `containerHeight`
|
||||
- Added `onLayout` to measure the actual timer screen area
|
||||
- Updated timer overlay movement to use the measured container height
|
||||
|
||||
This was done because the full window height does not always match the visible tab screen area when headers, tab bars, or safe areas are involved.
|
||||
|
||||
---
|
||||
|
||||
### #CountdownDisplay
|
||||
Added countdown display logic:
|
||||
- Added `timeRemaining`
|
||||
- Added `selectedIndex`
|
||||
- Added `formatTime(totalSeconds)`
|
||||
- Converted the selected timer value into a `MM:SS` display while running
|
||||
- Added `TIMER_UNIT_IN_SECONDS` so timer values can behave as seconds during development and minutes later
|
||||
|
||||
Current development behavior:
|
||||
- `TIMER_UNIT_IN_SECONDS = 1`
|
||||
- Selecting `5` means a 5-second timer
|
||||
|
||||
Planned production behavior:
|
||||
- `TIMER_UNIT_IN_SECONDS = 60`
|
||||
- Selecting `5` means a 5-minute timer
|
||||
|
||||
---
|
||||
|
||||
### #CountdownFadeControl
|
||||
Started separating countdown visibility from the rest of the timer UI:
|
||||
- Added `countdownAnimation`
|
||||
- Added `showCountdownText`
|
||||
- Began separating the `MM:SS` countdown fade from the button and picker fade
|
||||
- Fixed the nested animation callback syntax after adding the countdown fade-out flow
|
||||
|
||||
The goal is for the countdown text to fade out first, then for the button and adjacent timer values to fade back in after the countdown is gone.
|
||||
|
||||
---
|
||||
|
||||
## #LearningNotes
|
||||
|
||||
### #ReactState
|
||||
Worked with several pieces of state:
|
||||
- `duration` stores the selected timer value
|
||||
- `isRunning` tracks whether the timer is active
|
||||
- `timeRemaining` stores the countdown value
|
||||
- `selectedIndex` identifies which duration is selected
|
||||
- `showCountdownText` controls whether the selected item renders as `MM:SS`
|
||||
- `containerHeight` stores the measured height of the timer screen
|
||||
|
||||
Important distinction:
|
||||
- State values trigger re-renders when changed
|
||||
- Animated values drive smooth visual changes without normal React state updates on every animation frame
|
||||
|
||||
---
|
||||
|
||||
### #Hooks
|
||||
Clarified where hooks are allowed:
|
||||
- `useState`, `useRef`, `useEffect`, and `useCallback` must be called inside the component
|
||||
- Hooks must not be placed inside callbacks, conditionals, loops, or event handlers
|
||||
- `useEffect` dependency arrays must be inside the `useEffect(...)` call
|
||||
|
||||
One key bug came from an effect without a proper dependency array. Because the countdown updates state every second, the effect ran every second and reset the red overlay position.
|
||||
|
||||
---
|
||||
|
||||
### #AnimationFlow
|
||||
The timer now uses multiple animated values:
|
||||
- `timerAnimation` controls the red overlay movement
|
||||
- `buttonAnimation` controls the start button and inactive timer value visibility
|
||||
- `taskDetailsAnimation` controls the placeholder task information
|
||||
- `countdownAnimation` controls the `MM:SS` countdown visibility
|
||||
|
||||
The main lesson was that one animation value should not control too many unrelated visual states. Separate animation values make it easier to control the order of fade-out and fade-in transitions.
|
||||
|
||||
---
|
||||
|
||||
## #Verification
|
||||
|
||||
The timer file syntax issue around the end of the `animation` callback was fixed.
|
||||
|
||||
Current lint result:
|
||||
|
||||
```text
|
||||
npm run lint
|
||||
exited successfully
|
||||
```
|
||||
|
||||
The previous parse error was caused by mismatched closing braces/parentheses near the nested `.start(...)` callbacks at the end of the animation sequence.
|
||||
|
||||
The remaining behavior to confirm is the final transition order:
|
||||
- `MM:SS` countdown should fade out
|
||||
- selected text should switch back to the normal timer value while hidden
|
||||
- adjacent timer values should fade back in
|
||||
- start button should fade back in
|
||||
|
||||
---
|
||||
|
||||
## #FilesChanged
|
||||
|
||||
Main file worked on:
|
||||
|
||||
```text
|
||||
app/(tabs)/timer.tsx
|
||||
```
|
||||
|
||||
New note added:
|
||||
|
||||
```text
|
||||
notes/work-report-timer-2026-04-22.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## #Conclusion
|
||||
|
||||
The timer UI moved from a basic animated duration selector toward a more complete timer experience. It now has placeholder task information, a `MM:SS` countdown concept, measured layout support, and separate animation values for different UI elements.
|
||||
|
||||
The syntax error at the end of the animation callback has been fixed and lint now passes. The remaining immediate work is to finish confirming the final fade-out/fade-in ordering so the countdown disappears cleanly before the picker and start button return.
|
||||
141
notes/work-report-timer-2026-04-23.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Timer Interaction and Cancel Flow Work Report
|
||||
|
||||
## #Overview
|
||||
Today the standalone timer screen was developed further with a focus on the cancel interaction, countdown reset order, and a progress cue inside the cancel button.
|
||||
|
||||
The main work was not just adding UI pieces, but understanding how the existing React Native `Animated` flow behaves when a timer is started, cancelled, or allowed to finish naturally. The timer is still being treated as its own tab with placeholder task information, but the interaction model is now closer to the intended study-session behavior.
|
||||
|
||||
---
|
||||
|
||||
## #ImplementedFeatures
|
||||
|
||||
### #CancelButton
|
||||
Added a dedicated cancel control for the active timer state:
|
||||
- Added a separate cancel button animation value
|
||||
- Added a bottom-positioned cancel button that appears only during the running state
|
||||
- Added reverse handling so the button can be dismissed again when cancelling manually or when the timer finishes
|
||||
|
||||
The main goal was to keep the original large start control as the primary entry point, while giving the active timer state its own secondary exit action.
|
||||
|
||||
---
|
||||
|
||||
### #CancelProgressCue
|
||||
Started adding a progress cue directly inside the cancel button:
|
||||
- Added a separate `cancelProgressAnimation`
|
||||
- Added an inner animated fill layer inside the cancel button
|
||||
- Changed the progress direction to move left-to-right inside the button instead of using a full-button opacity fade
|
||||
|
||||
This was done to match the visual language of the main red timer overlay while keeping the progress indicator smaller and more local to the cancel action.
|
||||
|
||||
---
|
||||
|
||||
### #DurationLocking
|
||||
Updated the duration selector to stay fixed while the timer is running:
|
||||
- Added `scrollEnabled={!timerIsRunning}` to the horizontal timer picker
|
||||
- Added an early return inside `onMomentumScrollEnd`
|
||||
- Prevented the selected timer duration from changing once a session has started
|
||||
|
||||
This keeps the timer state consistent after the session begins and avoids the picker drifting into a visually different value while the countdown is active.
|
||||
|
||||
---
|
||||
|
||||
### #CountdownOwnership
|
||||
Clarified how the countdown interval should be owned and reset:
|
||||
- Added `countdownRef`
|
||||
- Added interval clearing before starting a new countdown
|
||||
- Used the ref-based interval handle so cancel and finish logic can target the active countdown
|
||||
|
||||
This work was needed because countdown behavior becomes unreliable if the code starts new intervals without keeping a consistent reference to the currently running one.
|
||||
|
||||
---
|
||||
|
||||
### #CancelFlowSequencing
|
||||
Worked on the ordering of reverse animations during manual cancel:
|
||||
- Tested separating countdown fade-out from the picker/start-button return
|
||||
- Investigated why adjacent numbers were reappearing before the countdown text had fully finished reversing
|
||||
- Traced the problem to both animation timing and the `showCountdownText` render condition
|
||||
|
||||
The important lesson here was that hiding the countdown visually and switching the rendered text back to the normal timer value are related, but not identical, events.
|
||||
|
||||
---
|
||||
|
||||
## #LearningNotes
|
||||
|
||||
### #AnimatedValueResponsibilities
|
||||
Today reinforced that each `Animated.Value` should have one clear responsibility:
|
||||
- `timerAnimation` controls the red overlay position
|
||||
- `buttonAnimation` controls start-button disappearance and inactive picker return
|
||||
- `countdownAnimation` controls countdown visibility
|
||||
- `cancelButtonAnimation` controls the cancel button itself
|
||||
- `cancelProgressAnimation` controls the left-to-right fill inside the cancel button
|
||||
|
||||
Several visual bugs came from trying to make one animated value carry two different meanings at the same time.
|
||||
|
||||
---
|
||||
|
||||
### #RenderStateVsAnimationState
|
||||
A key distinction became clearer during the cancel-flow debugging:
|
||||
- Animated values control motion and opacity
|
||||
- Regular React state controls what text/content is actually rendered
|
||||
|
||||
One important example is `showCountdownText`:
|
||||
- Even if the countdown has visually faded out, the selected timer item still renders `MM:SS` while `showCountdownText` remains `true`
|
||||
- This means the UI can still appear to be in “countdown mode” even after part of the reverse animation has already completed
|
||||
|
||||
This is why some cancel-order issues were not purely animation problems.
|
||||
|
||||
---
|
||||
|
||||
### #SequenceVsParallel
|
||||
The timer work also clarified when `Animated.sequence([...])` and `Animated.parallel([...])` should be used:
|
||||
- `sequence` is for strict order
|
||||
- `parallel` is for animations that should run at the same time
|
||||
|
||||
One mistake that surfaced during the progress-button work was placing the long progress-fill animation in a sequence after the main timer animation, which caused the fill to begin only after the timer had already ended.
|
||||
|
||||
---
|
||||
|
||||
## #CurrentIssue
|
||||
|
||||
The current timer screen still has remaining cancel-flow polish issues around visual timing and overlay cleanup.
|
||||
|
||||
The main issue currently under investigation is the reset order during manual cancel:
|
||||
- the red timer overlay can still produce a visible flash/jump when the running animation is interrupted
|
||||
- the adjacent picker numbers and selected countdown text are sensitive to both animation order and `showCountdownText`
|
||||
- the current implementation needs further refinement so cancel feels deliberate instead of visually noisy
|
||||
|
||||
Current lint result:
|
||||
|
||||
```text
|
||||
npm run lint
|
||||
completed with 1 warning
|
||||
```
|
||||
|
||||
Current warning:
|
||||
- unnecessary `showCountdownText` dependency in one `useCallback`
|
||||
|
||||
There are no current lint errors, but the cancel interaction is not yet considered visually finished.
|
||||
|
||||
---
|
||||
|
||||
## #FilesChanged
|
||||
|
||||
Main file worked on:
|
||||
|
||||
```text
|
||||
app/(tabs)/timer.tsx
|
||||
```
|
||||
|
||||
New note added:
|
||||
|
||||
```text
|
||||
notes/work-report-timer-2026-04-23.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## #Conclusion
|
||||
|
||||
The timer screen moved further toward a complete active-session interaction today. It now has a dedicated cancel control, a left-to-right progress cue inside that control, a locked duration picker while running, and a clearer separation between countdown ownership and animation ownership.
|
||||
|
||||
The main remaining work is not basic feature addition, but interaction polish. In particular, the cancel sequence still needs refinement so the red overlay, countdown text, and adjacent timer values return in a clean and intentional order.
|
||||
191
notes/work-report-timer-2026-04-24.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Timer Focus Mode and Hold-Cancel Work Report
|
||||
|
||||
## #Overview
|
||||
Today the standalone timer screen was reworked further with a focus on the active sprint layout, countdown ownership, and the hold-to-cancel interaction.
|
||||
|
||||
The main direction was to make the running timer feel more like a focused study state instead of a duration picker that happens to count down. The countdown was moved toward a separate overlay, the task details were given more visual emphasis, and the cancel interaction was changed from a simple button press into a deliberate hold action.
|
||||
|
||||
---
|
||||
|
||||
## #ImplementedFeatures
|
||||
|
||||
### #CountdownOverlay
|
||||
Moved the active countdown away from the duration picker:
|
||||
- Removed the old selected-picker countdown state
|
||||
- Added a separate countdown overlay using `countdownAnimation`
|
||||
- Added `focusModeAnimation` so the countdown can move from the central timer area toward the upper-left area
|
||||
- Kept the picker responsible for duration values only
|
||||
|
||||
This separates two responsibilities that had previously been mixed together: the picker selects a duration, while the overlay shows active countdown time.
|
||||
|
||||
---
|
||||
|
||||
### #FocusModeLayout
|
||||
Adjusted the active timer layout to put more attention on the task:
|
||||
- Moved task details higher and closer to the center of the running screen
|
||||
- Increased the task title and description size
|
||||
- Kept task details animated through `taskDetailsAnimation`
|
||||
- Continued using the red screen overlay as the main visual timer-progress element
|
||||
|
||||
The intent is for the active state to feel more like a study-session spotlight, where the selected task becomes the main focus and the countdown becomes supporting information.
|
||||
|
||||
---
|
||||
|
||||
### #HoldToCancel
|
||||
Changed the cancel action into a hold interaction:
|
||||
- Added `HOLD_TO_CANCEL_MS`
|
||||
- Added `cancelHoldTimeoutRef`
|
||||
- Added a hold-completion haptic warning
|
||||
- Kept the cancel button scale feedback during press
|
||||
- Changed the label to `Hold to end sprint`
|
||||
|
||||
This makes cancellation more deliberate and reduces the chance of accidentally ending a sprint with a single tap.
|
||||
|
||||
---
|
||||
|
||||
### #CancelAccelerationExperiment
|
||||
Implemented the red timer overlay as cancel feedback:
|
||||
- Added delayed cancel acceleration through `CANCEL_ANIMATION_DELAY_MS`
|
||||
- Added `cancelHoldAnimationDelayRef`
|
||||
- Added `cancelAccelStartedRef` to distinguish quick taps from actual hold acceleration
|
||||
- Split normal timer progress into `progressAnimationRef`
|
||||
- Added `startProgressAnimation(fromY)` so progress can start or resume from a specific overlay position
|
||||
- Added `cancelOverlayAnimation` as a temporary visual offset on top of the real timer progress
|
||||
- Added `getCancelOverlayTarget(...)` to calculate how far the cancel preview should move
|
||||
- Added a release handoff animation so the cancel offset eases back into the real timer position
|
||||
- Added clamping so the visual overlay does not move past the finished timer position
|
||||
- Added easing constants for the cancel delay, release handoff, and timer reset timings
|
||||
|
||||
The goal was for the red overlay to speed toward the finished position during a hold, then return smoothly to the real timer progress if the user releases before the cancel completes. The important change is that cancel preview motion is now layered on top of the real progress instead of directly taking over the main timer animation.
|
||||
|
||||
---
|
||||
|
||||
### #DurationPickerCleanup
|
||||
Cleaned up the duration picker after moving countdown ownership out of it:
|
||||
- Removed selected countdown rendering from the picker item
|
||||
- Kept picker items rendering plain timer values
|
||||
- Kept picker values fading out during active timer mode
|
||||
- Added index clamping when reading the selected duration from `onMomentumScrollEnd`
|
||||
- Restored `duration` as a dependency of the start callback so the selected picker value is used correctly
|
||||
|
||||
This fixed the earlier issue where the timer could behave as if the selected duration was still the initial value.
|
||||
|
||||
---
|
||||
|
||||
### #TimerCodeCleanup
|
||||
Cleaned up the timer screen structure after the interaction behavior was stabilized:
|
||||
- Renamed the old `animation` callback to `startTimer`
|
||||
- Renamed unclear animated values like `opacity` and `translateY` to `startButtonOpacity` and `startButtonTranslateY`
|
||||
- Grouped refs by purpose: animated values, timer/session refs, and cancel-hold refs
|
||||
- Extracted `clearCountdown`, `clearCancelHoldTimers`, and `stopTimerAnimations`
|
||||
- Extracted the cancel overlay target calculation into `getCancelOverlayTarget(...)`
|
||||
- Split the render section into local render helpers for the overlay, start button, cancel button, countdown, duration picker, and task details
|
||||
- Moved the timer item layout into `styles.timerItem`
|
||||
|
||||
This did not change the screen into a separate hook or split the timer into multiple files. The cleanup stayed local to `timer.tsx` so the current animation work remains easy to inspect.
|
||||
|
||||
---
|
||||
|
||||
## #LearningNotes
|
||||
|
||||
### #AnimationOwnership
|
||||
The main lesson today was that an `Animated.Value` should have one clear owner at a time.
|
||||
|
||||
The red overlay now combines two animated values:
|
||||
- normal timer progress
|
||||
- hold-to-cancel visual offset
|
||||
|
||||
The normal timer progress is controlled by `timerAnimation`, while cancel preview motion is controlled by `cancelOverlayAnimation`. This avoids stopping the real timer progress just to show the cancel speed-up effect.
|
||||
|
||||
---
|
||||
|
||||
### #RefsAsMutableState
|
||||
Several refs were added to track animation and timer ownership:
|
||||
- `progressAnimationRef` tracks the long-running red overlay progress animation
|
||||
- `sessionStartedAtRef` tracks the progress timeline used for recovery calculations
|
||||
- `sessionDurationMsRef` stores the current timer duration in milliseconds
|
||||
- `cancelHoldTimeoutRef` tracks when hold cancellation should complete
|
||||
- `cancelHoldAnimationDelayRef` tracks when cancel acceleration should begin
|
||||
- `cancelAccelStartedRef` tracks whether the red overlay acceleration actually started
|
||||
- `cancelHoldActiveRef` and `cancelHoldIdRef` prevent stale delayed hold callbacks from taking over after release
|
||||
|
||||
The important distinction is that assigning to `.current` is allowed even when the ref variable itself is declared with `const`.
|
||||
|
||||
---
|
||||
|
||||
### #CancelOffsetHandoff
|
||||
The release recovery logic was changed to avoid rewriting the real timer progress:
|
||||
- keep `timerAnimation` running as the source of real timer progress
|
||||
- add `cancelOverlayAnimation` on top of it while the cancel button is held
|
||||
- animate only the cancel offset back to `0` when the hold is released
|
||||
- keep the visible overlay clamped to the screen height
|
||||
- tune the release handoff timing with `CANCEL_RELEASE_MS`
|
||||
|
||||
This makes the visual red overlay return to the countdown's real timer position without forcing the main timer animation to stop and restart.
|
||||
|
||||
---
|
||||
|
||||
## #CurrentState
|
||||
|
||||
The hold-cancel red overlay interaction has been reworked so the cancel preview no longer directly mutates the real timer progress.
|
||||
|
||||
The current implementation:
|
||||
- keeps the countdown and real timer progress owned by `timerAnimation`
|
||||
- uses `cancelOverlayAnimation` as a temporary visual offset during hold-to-cancel
|
||||
- invalidates stale hold callbacks with `cancelHoldIdRef`
|
||||
- eases the cancel offset back to `0` on release
|
||||
- keeps the cancel-completion path separate from normal timer completion
|
||||
|
||||
This should make the red overlay speed-up feel connected to the cancel hold while still keeping the timer progress visually aligned with the countdown after release.
|
||||
|
||||
---
|
||||
|
||||
## #Verification
|
||||
|
||||
Current static checks pass:
|
||||
|
||||
```text
|
||||
npm run lint
|
||||
exited successfully
|
||||
```
|
||||
|
||||
```text
|
||||
npx tsc --noEmit
|
||||
exited successfully
|
||||
```
|
||||
|
||||
The hold-cancel handoff was also adjusted based on runtime feedback so the cancel offset eases back more smoothly into the real timer progress.
|
||||
|
||||
---
|
||||
|
||||
## #FilesChanged
|
||||
|
||||
Main file worked on:
|
||||
|
||||
```text
|
||||
app/(tabs)/timer.tsx
|
||||
```
|
||||
|
||||
New note added:
|
||||
|
||||
```text
|
||||
notes/work-report-timer-2026-04-24.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## #Conclusion
|
||||
|
||||
The timer screen moved further toward a focused active-sprint experience. The countdown is now separated from the duration picker, task details have more visual weight, and cancel is treated as a deliberate hold action rather than a normal tap.
|
||||
|
||||
The main animation change is that hold-to-cancel now keeps the real timer progress separate from the temporary cancel speed-up effect. The code was also cleaned up so the timer flow is easier to read and continue working on.
|
||||
|
||||
## Problems occuring after writing conclusion
|
||||
Tried to implement sound by installing expo-audio. This caused the dependency list to update. The diff was massive, and something in the diff caused the entire timer page to break. Logic, animations - the lot. Have reverted back to last known working dependency list, as well as un-refactored a lot of code in an attempt to revert to a functioning state before figuring out that the culprit was dependencies. Need to figure our what is causing the critical failure in the new list.
|
||||
|
||||
## Todo
|
||||
- Re-refactor to make code cleaner, more readable and easier to maintain.
|
||||
- Figure out the dependency issues of later dependency lists
|
||||
|
||||
## Conclusion of dependecy saga
|
||||
There was a mismatch in the nativewind dependency, with my one being ^4.2.3 and the other list being ^4.1.23. This cause my entire timer screen to fail. Animations got borked, buttons not working properly, duration picker only showing 2 indexes... the works. Solution - keepp nativewind dependency to ^4.2.3
|
||||
163
notes/work-report-timer-2026-04-25.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Timer Refactor and Verification Work Report
|
||||
|
||||
## #Overview
|
||||
Today the timer screen was worked on with a narrower goal than yesterday: not new interaction features, but cleanup, readability, and making the existing timer flow easier to understand and maintain.
|
||||
|
||||
This follows directly from yesterday's state. The April 24 note ended with two follow-up items:
|
||||
- re-refactor the timer code so it becomes easier to read and work on
|
||||
- keep the dependency situation stable after the NativeWind version mismatch had broken the screen
|
||||
|
||||
Today's work focused on the first of those. The interaction model was kept the same, but the internal structure of `timer.tsx` was cleaned up so the current hold-to-cancel and focus-mode behavior is easier to inspect without splitting the code into hooks or separate files.
|
||||
|
||||
---
|
||||
|
||||
## #ImplementedFeatures
|
||||
|
||||
### #TimerCodeRefactor
|
||||
Refactored the timer screen structure inside `app/(tabs)/timer.tsx`:
|
||||
- renamed the component from `App` to `TimerScreen`
|
||||
- renamed unclear callbacks such as the old generic start-animation function into `startTimerSession`
|
||||
- grouped the file more clearly into constants, animated values, refs, derived values, actions, render helpers, and JSX
|
||||
- renamed vague animated/interpolated values to clearer names such as `startButtonOpacity`, `startButtonTranslateY`, and `pickerOpacity`
|
||||
|
||||
This did not change the screen architecture into multiple files. The cleanup stayed local to the timer file so the animation flow is still easy to inspect in one place.
|
||||
|
||||
---
|
||||
|
||||
### #CleanupHelpers
|
||||
Extracted repeated timer cleanup work into small local helpers:
|
||||
- added `clearCountdownInterval()`
|
||||
- added `clearCancelHoldTimeouts()`
|
||||
- added `stopRunningAnimations()`
|
||||
- added `resetSessionValues()`
|
||||
|
||||
Before this, the same interval, timeout, and animation-reset work was spread across multiple callbacks. Pulling it into helpers makes it easier to follow what happens when a session starts, finishes, or is cancelled.
|
||||
|
||||
---
|
||||
|
||||
### #RenderStructureCleanup
|
||||
Cleaned up the render section so it is easier to read:
|
||||
- moved repeated inline layout styles into named `StyleSheet` entries
|
||||
- extracted the timer picker item rendering into a local `renderTimerItem(...)` helper
|
||||
- kept the JSX order aligned with the visible screen layers: overlay, start button, cancel button, countdown, duration picker, and task details
|
||||
|
||||
This mainly improves scanning. The old file worked, but the render section made you jump between inline style objects and animation expressions to understand each layer.
|
||||
|
||||
---
|
||||
|
||||
### #CommentAndNamingPass
|
||||
Added a small number of comments only where the code was genuinely hard to follow:
|
||||
- clarified that `timerAnimation` owns real timer progress
|
||||
- clarified that `cancelOverlayAnimation` is only a temporary visual offset during hold-to-cancel
|
||||
- clarified that `startProgressAnimation(fromY)` resumes overlay progress from the current Y position
|
||||
- clarified why cancel acceleration starts after a short delay
|
||||
|
||||
The aim was not to comment every line, but to explain the parts that are hard to infer just by reading the code.
|
||||
|
||||
---
|
||||
|
||||
### #StateResetTightening
|
||||
Made the session cleanup more explicit:
|
||||
- reset `sessionStartedAtRef` and `sessionDurationMsRef` when a session ends
|
||||
- reset cancel-hold flags during session cleanup
|
||||
- made `finishTimer()` explicitly clear the countdown interval before running exit animations
|
||||
- kept the existing unmount cleanup so intervals, timeouts, and running animations are not left behind if the screen disappears mid-session
|
||||
|
||||
These are small changes, but they make the timer lifecycle more predictable and reduce the amount of stale mutable state left around after finish or cancel paths.
|
||||
|
||||
---
|
||||
|
||||
## #LearningNotes
|
||||
|
||||
### #ReadableCodeVsNewFeatures
|
||||
Today's timer work was a good reminder that "more maintainable" does not always mean "more abstract."
|
||||
|
||||
For this screen, the right cleanup level was:
|
||||
- better names
|
||||
- smaller local helpers
|
||||
- clearer grouping
|
||||
- a few targeted comments
|
||||
|
||||
The wrong cleanup level for the current stage would have been moving the logic into extra hooks or files too early, because that would make it harder to inspect the animation flow while the interaction is still being tuned.
|
||||
|
||||
---
|
||||
|
||||
### #MutableRefOwnership
|
||||
The timer file still relies heavily on refs because several parts of the interaction are long-lived and imperative:
|
||||
- active countdown interval
|
||||
- running start animation
|
||||
- running progress animation
|
||||
- delayed cancel-preview start
|
||||
- hold-to-cancel completion timeout
|
||||
|
||||
The cleanup made this easier to see by separating refs that hold animated values from refs that track mutable timer/session ownership.
|
||||
|
||||
---
|
||||
|
||||
## #CurrentState
|
||||
|
||||
Compared with yesterday, the timer interaction model is mostly the same, but the code behind it is more structured.
|
||||
|
||||
The current implementation:
|
||||
- keeps the red overlay model used yesterday
|
||||
- keeps `timerAnimation` as the real progress owner
|
||||
- keeps `cancelOverlayAnimation` as the temporary hold-preview layer
|
||||
- keeps the delayed hold acceleration and release recovery flow
|
||||
- keeps all timer logic local to `timer.tsx`
|
||||
- is now easier to read because repeated cleanup and render logic have been extracted into named local pieces
|
||||
|
||||
This means today's work was mainly a recovery and consolidation pass after yesterday's interaction-heavy changes and the earlier dependency-related breakage.
|
||||
|
||||
---
|
||||
|
||||
## #Verification
|
||||
|
||||
Today's static checks passed after the refactor:
|
||||
|
||||
```text
|
||||
npm run lint
|
||||
exited successfully
|
||||
```
|
||||
|
||||
```text
|
||||
npx tsc --noEmit
|
||||
exited successfully
|
||||
```
|
||||
|
||||
```text
|
||||
git diff --check -- 'app/(tabs)/timer.tsx'
|
||||
exited successfully
|
||||
```
|
||||
|
||||
There was no new timer commit for today at the time of writing this note. The summary above is based on:
|
||||
- the current working-tree diff for `app/(tabs)/timer.tsx`
|
||||
- the verification commands run after the refactor
|
||||
- yesterday's note and timer history for context
|
||||
|
||||
I did not do a live Expo interaction test inside this note workflow, so runtime behavior is verified statically plus by code review rather than by manually pressing through the UI.
|
||||
|
||||
---
|
||||
|
||||
## #FilesChanged
|
||||
|
||||
Main file worked on:
|
||||
|
||||
```text
|
||||
app/(tabs)/timer.tsx
|
||||
```
|
||||
|
||||
New note added:
|
||||
|
||||
```text
|
||||
notes/work-report-timer-2026-04-25.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## #Conclusion
|
||||
|
||||
The main timer work today was not adding new features, but making yesterday's feature-rich timer implementation is easier to continue working on.
|
||||
|
||||
The result is a timer file that keeps the same focus-mode and hold-to-cancel behavior, while being more readable, more structured, and easier to maintain. The biggest improvement is that the important ideas in the file now have clearer names, clearer ownership, and clearer cleanup paths.
|
||||
|
||||
The timer is now considered finished and ready to implement into the rest of the project.
|
||||
840
package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "study-sprint",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
@@ -29,7 +28,7 @@
|
||||
"expo-symbols": "~1.0.8",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"nativewind": "^4.1.23",
|
||||
"nativewind": "^4.2.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
@@ -39,14 +38,13 @@
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwindcss": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
},
|
||||
@@ -1764,6 +1762,198 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli": {
|
||||
"version": "54.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz",
|
||||
"integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@0no-co/graphql.web": "^1.0.8",
|
||||
"@expo/code-signing-certificates": "^0.0.6",
|
||||
"@expo/config": "~12.0.13",
|
||||
"@expo/config-plugins": "~54.0.4",
|
||||
"@expo/devcert": "^1.2.1",
|
||||
"@expo/env": "~2.0.8",
|
||||
"@expo/image-utils": "^0.8.8",
|
||||
"@expo/json-file": "^10.0.8",
|
||||
"@expo/metro": "~54.2.0",
|
||||
"@expo/metro-config": "~54.0.14",
|
||||
"@expo/osascript": "^2.3.8",
|
||||
"@expo/package-manager": "^1.9.10",
|
||||
"@expo/plist": "^0.4.8",
|
||||
"@expo/prebuild-config": "^54.0.8",
|
||||
"@expo/schema-utils": "^0.1.8",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/ws-tunnel": "^1.0.1",
|
||||
"@expo/xcpretty": "^4.3.0",
|
||||
"@react-native/dev-middleware": "0.81.5",
|
||||
"@urql/core": "^5.0.6",
|
||||
"@urql/exchange-retry": "^1.3.0",
|
||||
"accepts": "^1.3.8",
|
||||
"arg": "^5.0.2",
|
||||
"better-opn": "~3.0.2",
|
||||
"bplist-creator": "0.1.0",
|
||||
"bplist-parser": "^0.3.1",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.3.0",
|
||||
"compression": "^1.7.4",
|
||||
"connect": "^3.7.0",
|
||||
"debug": "^4.3.4",
|
||||
"env-editor": "^0.4.1",
|
||||
"expo-server": "^1.0.5",
|
||||
"freeport-async": "^2.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"lan-network": "^0.1.6",
|
||||
"minimatch": "^9.0.0",
|
||||
"node-forge": "^1.3.3",
|
||||
"npm-package-arg": "^11.0.0",
|
||||
"ora": "^3.4.0",
|
||||
"picomatch": "^3.0.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"pretty-format": "^29.7.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "0.11.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"requireg": "^0.2.2",
|
||||
"resolve": "^1.22.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"resolve.exports": "^2.0.3",
|
||||
"semver": "^7.6.0",
|
||||
"send": "^0.19.0",
|
||||
"slugify": "^1.3.4",
|
||||
"source-map-support": "~0.5.21",
|
||||
"stacktrace-parser": "^0.1.10",
|
||||
"structured-headers": "^0.4.1",
|
||||
"tar": "^7.5.2",
|
||||
"terminal-link": "^2.1.1",
|
||||
"undici": "^6.18.2",
|
||||
"wrap-ansi": "^7.0.0",
|
||||
"ws": "^8.12.1"
|
||||
},
|
||||
"bin": {
|
||||
"expo-internal": "build/bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"expo-router": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo-router": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/picomatch": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.2.tgz",
|
||||
"integrity": "sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/code-signing-certificates": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
|
||||
@@ -4083,21 +4273,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
||||
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
@@ -6856,198 +7039,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/@expo/cli": {
|
||||
"version": "54.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz",
|
||||
"integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@0no-co/graphql.web": "^1.0.8",
|
||||
"@expo/code-signing-certificates": "^0.0.6",
|
||||
"@expo/config": "~12.0.13",
|
||||
"@expo/config-plugins": "~54.0.4",
|
||||
"@expo/devcert": "^1.2.1",
|
||||
"@expo/env": "~2.0.8",
|
||||
"@expo/image-utils": "^0.8.8",
|
||||
"@expo/json-file": "^10.0.8",
|
||||
"@expo/metro": "~54.2.0",
|
||||
"@expo/metro-config": "~54.0.14",
|
||||
"@expo/osascript": "^2.3.8",
|
||||
"@expo/package-manager": "^1.9.10",
|
||||
"@expo/plist": "^0.4.8",
|
||||
"@expo/prebuild-config": "^54.0.8",
|
||||
"@expo/schema-utils": "^0.1.8",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/ws-tunnel": "^1.0.1",
|
||||
"@expo/xcpretty": "^4.3.0",
|
||||
"@react-native/dev-middleware": "0.81.5",
|
||||
"@urql/core": "^5.0.6",
|
||||
"@urql/exchange-retry": "^1.3.0",
|
||||
"accepts": "^1.3.8",
|
||||
"arg": "^5.0.2",
|
||||
"better-opn": "~3.0.2",
|
||||
"bplist-creator": "0.1.0",
|
||||
"bplist-parser": "^0.3.1",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.3.0",
|
||||
"compression": "^1.7.4",
|
||||
"connect": "^3.7.0",
|
||||
"debug": "^4.3.4",
|
||||
"env-editor": "^0.4.1",
|
||||
"expo-server": "^1.0.5",
|
||||
"freeport-async": "^2.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"lan-network": "^0.1.6",
|
||||
"minimatch": "^9.0.0",
|
||||
"node-forge": "^1.3.3",
|
||||
"npm-package-arg": "^11.0.0",
|
||||
"ora": "^3.4.0",
|
||||
"picomatch": "^3.0.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"pretty-format": "^29.7.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "0.11.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"requireg": "^0.2.2",
|
||||
"resolve": "^1.22.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"resolve.exports": "^2.0.3",
|
||||
"semver": "^7.6.0",
|
||||
"send": "^0.19.0",
|
||||
"slugify": "^1.3.4",
|
||||
"source-map-support": "~0.5.21",
|
||||
"stacktrace-parser": "^0.1.10",
|
||||
"structured-headers": "^0.4.1",
|
||||
"tar": "^7.5.2",
|
||||
"terminal-link": "^2.1.1",
|
||||
"undici": "^6.18.2",
|
||||
"wrap-ansi": "^7.0.0",
|
||||
"ws": "^8.12.1"
|
||||
},
|
||||
"bin": {
|
||||
"expo-internal": "build/bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"expo-router": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo-router": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/picomatch": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.2.tgz",
|
||||
"integrity": "sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/exponential-backoff": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||
@@ -7233,16 +7224,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-yarn-workspace-root": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
|
||||
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"micromatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
@@ -7309,21 +7290,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -8745,26 +8711,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
|
||||
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.4",
|
||||
"isarray": "^2.0.5",
|
||||
"jsonify": "^0.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
@@ -8784,29 +8730,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonify": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
|
||||
"dev": true,
|
||||
"license": "Public Domain",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -8833,16 +8756,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/klaw-sync": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
@@ -9832,14 +9745,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nativewind": {
|
||||
"version": "4.1.23",
|
||||
"resolved": "https://registry.npmjs.org/nativewind/-/nativewind-4.1.23.tgz",
|
||||
"integrity": "sha512-oLX3suGI6ojQqWxdQezOSM5GmJ4KvMnMtmaSMN9Ggb5j7ysFt4nHxb1xs8RDjZR7BWc+bsetNJU8IQdQMHqRpg==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/nativewind/-/nativewind-4.2.3.tgz",
|
||||
"integrity": "sha512-HglF1v6A8CqBFpXWs0d3yf4qQGurrreLuyE8FTRI/VDH8b0npZa2SDG5tviTkLiBg0s5j09mQALZOjxuocgMLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"comment-json": "^4.2.5",
|
||||
"debug": "^4.3.7",
|
||||
"react-native-css-interop": "0.1.22"
|
||||
"react-native-css-interop": "0.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@@ -10406,75 +10319,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
|
||||
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^3.7.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"find-yarn-workspace-root": "^2.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"json-stable-stringify": "^1.0.2",
|
||||
"klaw-sync": "^6.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"open": "^7.4.2",
|
||||
"semver": "^7.5.3",
|
||||
"slash": "^2.0.0",
|
||||
"tmp": "^0.2.4",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"patch-package": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">5"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -11101,16 +10945,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop": {
|
||||
"version": "0.1.22",
|
||||
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz",
|
||||
"integrity": "sha512-Mu01e+H9G+fxSWvwtgWlF5MJBJC4VszTCBXopIpeR171lbeBInHb8aHqoqRPxmJpi3xIHryzqKFOJYAdk7PBxg==",
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz",
|
||||
"integrity": "sha512-wc+JI7iUfdFBqnE18HhMTtD0q9vkhuMczToA87UdHGWwMyxdT5sCcNy+i4KInPCE855IY0Ic8kLQqecAIBWz7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.3.7",
|
||||
"lightningcss": "^1.27.0",
|
||||
"lightningcss": "~1.27.0",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -11131,6 +10975,258 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz",
|
||||
"integrity": "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.27.0",
|
||||
"lightningcss-darwin-x64": "1.27.0",
|
||||
"lightningcss-freebsd-x64": "1.27.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.27.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.27.0",
|
||||
"lightningcss-linux-arm64-musl": "1.27.0",
|
||||
"lightningcss-linux-x64-gnu": "1.27.0",
|
||||
"lightningcss-linux-x64-musl": "1.27.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.27.0",
|
||||
"lightningcss-win32-x64-msvc": "1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz",
|
||||
"integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz",
|
||||
"integrity": "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz",
|
||||
"integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz",
|
||||
"integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz",
|
||||
"integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz",
|
||||
"integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz",
|
||||
"integrity": "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz",
|
||||
"integrity": "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz",
|
||||
"integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz",
|
||||
"integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
@@ -12793,16 +12889,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -13110,16 +13196,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"postinstall": "patch-package",
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
@@ -31,7 +33,7 @@
|
||||
"expo-symbols": "~1.0.8",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"nativewind": "^4.1.23",
|
||||
"nativewind": "^4.2.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
@@ -41,14 +43,13 @@
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwindcss": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||