diff --git a/.jules/palette.md b/.jules/palette.md
new file mode 100644
index 0000000..8f68cda
--- /dev/null
+++ b/.jules/palette.md
@@ -0,0 +1,9 @@
+# Palette's Journal - Critical Learnings Only
+
+## 2025-05-15 - Haptic Feedback in Continuous Gestures
+**Learning:** In 'PanResponder' implementations, haptic feedback can easily misfire or trigger repeatedly during a single swipe if not guarded by a state ref.
+**Action:** Use a 'useRef' toggle (e.g., 'hasTriggeredHaptic') to ensure the feedback triggers only once when a threshold is crossed during a continuous gesture.
+
+## 2025-05-15 - Accessibility for Swipe Actions
+**Learning:** Swipe-based actions (like pinning) are inaccessible to screen reader users.
+**Action:** Use 'accessibilityActions' and 'onAccessibilityAction' on the card component to provide an alternative way for assistive technologies to trigger these specific interactions without requiring complex gestures.
diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts
index 8ccd1e2..6119713 100644
--- a/src/i18n/translations.ts
+++ b/src/i18n/translations.ts
@@ -40,6 +40,8 @@ const it = {
notepadClearTitle: 'Cancella note',
notepadClearMsg: 'Sei sicuro di voler cancellare tutte le note?',
notepadClearConfirm: 'Cancella', notepadPlaceholder: 'Inizia a scrivere...',
+ notepadAccessibilityClear: 'Cancella tutte le note',
+ notepadAccessibilitySave: 'Salva note',
// TravelDoc
traveldocSub: 'Verifica documenti di viaggio',
traveldocLoading: 'Caricamento TravelDoc…',
@@ -106,6 +108,10 @@ const it = {
flightNotifMsg1: 'Programmate {count} notifiche: arrivi voli (15 min prima) + fine turno.',
flightNotifMsg0: 'Nessun volo futuro trovato, ma riceverai la notifica di fine turno.',
flightNotifAccessEnable: 'Attiva notifiche voli', flightNotifAccessDisable: 'Disattiva notifiche voli',
+ flightAccessibilityPin: 'Pinna volo',
+ flightAccessibilityUnpin: 'Rimuovi pin volo',
+ flightAccessibilityPinHint: 'Sposta il volo in cima alla lista e abilita il monitoraggio',
+ flightAccessibilityUnpinHint: 'Rimuovi il volo dalla posizione in cima',
// Phonebook
phonebookTitle: 'Rubrica', contactAdd: 'Aggiungi',
contactSearch: 'Cerca nome o numero...', contactAll: 'Tutti',
@@ -198,6 +204,8 @@ const en: typeof it = {
notepadClearTitle: 'Clear notes',
notepadClearMsg: 'Are you sure you want to clear all notes?',
notepadClearConfirm: 'Clear', notepadPlaceholder: 'Start writing...',
+ notepadAccessibilityClear: 'Clear all notes',
+ notepadAccessibilitySave: 'Save notes',
// TravelDoc
traveldocSub: 'Travel document check',
traveldocLoading: 'Loading TravelDoc…',
@@ -264,6 +272,10 @@ const en: typeof it = {
flightNotifMsg1: '{count} notifications scheduled: flight arrivals (15 min before) + end of shift.',
flightNotifMsg0: 'No future flights found, but you will receive the end-of-shift notification.',
flightNotifAccessEnable: 'Enable flight notifications', flightNotifAccessDisable: 'Disable flight notifications',
+ flightAccessibilityPin: 'Pin flight',
+ flightAccessibilityUnpin: 'Unpin flight',
+ flightAccessibilityPinHint: 'Move flight to the top of the list and enable tracking',
+ flightAccessibilityUnpinHint: 'Remove flight from the top position',
// Phonebook
phonebookTitle: 'Phonebook', contactAdd: 'Add',
contactSearch: 'Search name or number...', contactAll: 'All',
diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx
index 7b37ca6..01fda35 100644
--- a/src/screens/FlightScreen.tsx
+++ b/src/screens/FlightScreen.tsx
@@ -3,9 +3,11 @@ import {
View, Text, StyleSheet, ActivityIndicator, Modal,
FlatList, TouchableOpacity, RefreshControl, Image, Alert,
Animated, PanResponder, NativeModules, Platform,
+ AccessibilityActionEvent,
} from 'react-native';
import * as Calendar from 'expo-calendar';
import * as Notifications from 'expo-notifications';
+import * as Haptics from 'expo-haptics';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons';
import { useAppTheme, type ThemeColors } from '../context/ThemeContext';
@@ -61,24 +63,36 @@ function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineN
const SWIPE_THRESHOLD = 80;
function SwipeableFlightCardComponent({
- children, isPinned, onToggle,
+ children, isPinned, onToggle, ...rest
}: {
children: React.ReactNode;
isPinned: boolean;
onToggle: () => void;
+ [key: string]: any;
}) {
const translateX = useRef(new Animated.Value(0)).current;
const onToggleRef = useRef(onToggle);
onToggleRef.current = onToggle;
+ const hasTriggeredHaptic = useRef(false);
const panResponder = useMemo(() => PanResponder.create({
onMoveShouldSetPanResponder: (_, g) =>
Math.abs(g.dx) > 15 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
onPanResponderMove: (_, g) => {
- if (g.dx < 0) translateX.setValue(g.dx);
+ if (g.dx < 0) {
+ translateX.setValue(g.dx);
+ if (Math.abs(g.dx) > SWIPE_THRESHOLD && !hasTriggeredHaptic.current) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ hasTriggeredHaptic.current = true;
+ } else if (Math.abs(g.dx) < SWIPE_THRESHOLD && hasTriggeredHaptic.current) {
+ hasTriggeredHaptic.current = false;
+ }
+ }
},
onPanResponderRelease: (_, g) => {
+ hasTriggeredHaptic.current = false;
if (g.dx < -SWIPE_THRESHOLD) {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => {
onToggleRef.current();
Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
@@ -94,7 +108,11 @@ function SwipeableFlightCardComponent({
return (
-
+
{children}
@@ -605,10 +623,23 @@ export default function FlightScreen() {
console.log(`[FlightScreen] No staffMonitor match for "${normFn}" (stripped: "${normFnStripped}") in ${activeTab}`);
}
+ const accessibilityLabel = `${isPinned ? t('flightPinnedLabel') + ', ' : ''}${flightNumber}, ${airline}, ${activeTab === 'arrivals' ? t('homeArrival') : t('homeDeparture')}, ${originDest}, ${time}, ${statusText}`;
+
return (
isPinned ? unpinFlight() : pinFlight(item)}
+ accessible
+ accessibilityLabel={accessibilityLabel}
+ accessibilityHint={isPinned ? t('flightAccessibilityUnpinHint') : t('flightAccessibilityPinHint')}
+ accessibilityActions={[
+ { name: 'togglePin', label: isPinned ? t('flightAccessibilityUnpin') : t('flightAccessibilityPin') },
+ ]}
+ onAccessibilityAction={(event: AccessibilityActionEvent) => {
+ if (event.nativeEvent.actionName === 'togglePin') {
+ isPinned ? unpinFlight() : pinFlight(item);
+ }
+ }}
>
{isPinned && {t('flightPinned')}}
@@ -769,7 +800,7 @@ export default function FlightScreen() {
onPress={toggleNotifications}
activeOpacity={0.8}
accessible
- accessibilityLabel={notifsEnabled ? 'Disattiva notifiche voli' : 'Attiva notifiche voli'}
+ accessibilityLabel={notifsEnabled ? t('flightNotifAccessDisable') : t('flightNotifAccessEnable')}
accessibilityRole="button"
>
{
await AsyncStorage.setItem(STORAGE_KEY, text);
setSaved(true);
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}, [text]);
const clear = useCallback(() => {
@@ -109,12 +111,20 @@ export default function NotepadScreen() {
{t('notepadTitle')}
-
+
{saved ? t('notepadSaved') : t('notepadSave')}