From f629603ffc714a43e590cfb545768a24af7b5651 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 28 May 2026 18:07:34 -0700 Subject: [PATCH 01/23] Hotspot filtering prototype --- components/FilterSection.tsx | 20 +- components/HotspotList.tsx | 53 +++- components/Mapbox.tsx | 172 +++++++++-- .../PersonalizedHotspotFilterControls.tsx | 111 +++++++ hooks/usePersonalizedHotspotFilter.ts | 289 ++++++++++++++++++ lib/database.ts | 65 ++-- lib/hotspotTargets.ts | 54 ++++ lib/personalizedHotspotFilter.ts | 287 +++++++++++++++++ stores/filtersStore.ts | 25 +- 9 files changed, 1006 insertions(+), 70 deletions(-) create mode 100644 components/PersonalizedHotspotFilterControls.tsx create mode 100644 hooks/usePersonalizedHotspotFilter.ts create mode 100644 lib/hotspotTargets.ts create mode 100644 lib/personalizedHotspotFilter.ts diff --git a/components/FilterSection.tsx b/components/FilterSection.tsx index e0af35b..23fbbc5 100644 --- a/components/FilterSection.tsx +++ b/components/FilterSection.tsx @@ -1,11 +1,15 @@ +import PersonalizedHotspotFilterControls from "@/components/PersonalizedHotspotFilterControls"; import tw from "@/lib/tw"; import { useFiltersStore } from "@/stores/filtersStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import React from "react"; import { Platform, Switch, Text, View } from "react-native"; import { BorderlessButton } from "react-native-gesture-handler"; export default function FilterSection() { const { showSavedOnly, setShowSavedOnly } = useFiltersStore(); + const lifelist = useSettingsStore((state) => state.lifelist); + const hasLifeList = (lifelist?.length ?? 0) > 0; const content = ( @@ -16,11 +20,19 @@ export default function FilterSection() { if (Platform.OS === "android") { return ( - setShowSavedOnly(!showSavedOnly)} style={tw`pl-6 pr-5 py-4`} activeOpacity={1}> - {content} - + + setShowSavedOnly(!showSavedOnly)} activeOpacity={1}> + {content} + + + ); } - return {content}; + return ( + + {content} + + + ); } diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index bc8ad2f..feb440f 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -1,3 +1,4 @@ +import { usePersonalizedHotspotFilter } from "@/hooks/usePersonalizedHotspotFilter"; import { useLocation } from "@/hooks/useLocation"; import { useScrollRestore } from "@/hooks/useScrollRestore"; import { getAllHotspots, getNearbyHotspots, searchHotspots } from "@/lib/database"; @@ -6,6 +7,7 @@ import { Hotspot } from "@/lib/types"; import { calculateDistance, getBoundingBoxFromLocation } from "@/lib/utils"; import { useFiltersStore } from "@/stores/filtersStore"; import { useLocationPermissionStore } from "@/stores/locationPermissionStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; import debounce from "lodash/debounce"; @@ -16,6 +18,7 @@ import BaseBottomSheet from "./BaseBottomSheet"; import HotspotItem from "./HotspotItem"; import IconButton from "./IconButton"; import IconButtonGroup from "./IconButtonGroup"; +import PersonalizedHotspotFilterControls from "./PersonalizedHotspotFilterControls"; import SearchInput from "./SearchInput"; type HotspotListProps = { @@ -35,7 +38,10 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo const { location, isLoading: isLoadingUserLocation } = useLocation(isOpen); const isLoadingLocation = isLoadingPermission || isLoadingUserLocation; const { showSavedOnly, setShowSavedOnly } = useFiltersStore(); - const activeFilterCount = [showSavedOnly].filter(Boolean).length; + const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const lifelist = useSettingsStore((state) => state.lifelist); + const hasLifeList = (lifelist?.length ?? 0) > 0; + const activeFilterCount = [showSavedOnly, personalizedFilterEnabled && hasLifeList].filter(Boolean).length; const dismissRef = useRef<(() => Promise) | null>(null); const [searchQuery, setSearchQuery] = useState(""); @@ -57,7 +63,11 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo const hasLocationAccess = permissionStatus === "granted" && location !== null; - const { data: searchResults = [], dataUpdatedAt: searchUpdatedAt } = useQuery({ + const { + data: searchResults = [], + dataUpdatedAt: searchUpdatedAt, + isFetching: isFetchingSearchResults, + } = useQuery({ queryKey: ["hotspotSearch", debouncedQuery, showSavedOnly], queryFn: () => searchHotspots(debouncedQuery, SEARCH_LIMIT, showSavedOnly), enabled: isOpen && debouncedQuery.length >= 2 && !isLoadingLocation, @@ -65,7 +75,7 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo placeholderData: (prev) => prev, }); - const { data: allHotspots = [] } = useQuery({ + const { data: allHotspots = [], isFetching: isFetchingAllHotspots } = useQuery({ queryKey: hasLocationAccess && location ? ["nearbyHotspots", location.lat, location.lng, showSavedOnly] @@ -106,6 +116,21 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo } }, [debouncedQuery, searchResults, allHotspots, hasLocationAccess, location]); + const isBaseResultsFetching = debouncedQuery.length >= 2 ? isFetchingSearchResults : isFetchingAllHotspots; + const personalizedFilter = usePersonalizedHotspotFilter(displayedHotspots.map((hotspot) => hotspot.id), { + enabled: !isBaseResultsFetching, + blockWhileDisabled: true, + }); + const isPersonalizedLoading = personalizedFilter.isActive && (isBaseResultsFetching || personalizedFilter.isLoading); + const displayedHotspotsSet = useMemo(() => new Set(personalizedFilter.filteredIds), [personalizedFilter.filteredIds]); + const filteredDisplayedHotspots = useMemo(() => { + if (!personalizedFilter.isActive) { + return displayedHotspots; + } + + return displayedHotspots.filter((hotspot) => displayedHotspotsSet.has(hotspot.id)); + }, [displayedHotspots, displayedHotspotsSet, personalizedFilter.isActive]); + const { listRef, onScroll } = useScrollRestore(isOpen, searchUpdatedAt); const handleSelectHotspot = useCallback( @@ -123,7 +148,12 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo const listEmptyComponent = ( - {isLoadingLocation && permissionStatus === "granted" ? ( + {isPersonalizedLoading ? ( + <> + + Filtering hotspots... + + ) : isLoadingLocation && permissionStatus === "granted" ? ( <> Getting current location... @@ -164,9 +194,12 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo {isFilterPanelOpen && ( - - Show saved only - + + + Show saved only + + + )} @@ -188,12 +221,14 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo > void; }; -const THROTTLE_DELAY = 750; -const THROTTLE_DELAY_WITH_OPEN_HOTSPOT = 250; // Load hotspots faster when jumping to a hotspot from list modal +const THROTTLE_DELAY = 300; +const THROTTLE_DELAY_WITH_OPEN_HOTSPOT = 150; // Load hotspots faster when jumping to a hotspot from list modal const MIN_ZOOM = 8; const DEFAULT_USER_ZOOM = 14; const DEFAULT_HOTSPOT_ZOOM = 13; +const BOUNDS_EPSILON = 0.0001; + +function areBoundsEquivalent(left: Bounds | null, right: Bounds | null): boolean { + if (left === right) { + return true; + } + + if (!left || !right) { + return false; + } + + return ( + Math.abs(left.west - right.west) < BOUNDS_EPSILON && + Math.abs(left.south - right.south) < BOUNDS_EPSILON && + Math.abs(left.east - right.east) < BOUNDS_EPSILON && + Math.abs(left.north - right.north) < BOUNDS_EPSILON + ); +} + const isValidUserCoord = (coord: [number, number] | null) => { if (!coord) return false; const [lng, lat] = coord; @@ -147,6 +168,8 @@ const MapboxMap = forwardRef( const centeredToUserRef = useRef(false); const userCoordRef = useRef<[number, number] | null>(null); const isTouchActiveRef = useRef(false); + const lastPersonalizedMapDebugRef = useRef(null); + const lastResolvedHotspotsRef = useRef<{ id: string; lat: number; lng: number; species: number }[]>([]); const [isMapReady, setIsMapReady] = useState(false); const [bounds, setBounds] = useState(null); @@ -157,7 +180,7 @@ const MapboxMap = forwardRef( : "mapbox://styles/mapbox/outdoors-v12"; }, [currentLayer]); - const { data: hotspots = [] } = useQuery({ + const { data: hotspots = [], isFetching: isFetchingHotspots } = useQuery({ queryKey: ["hotspots", bounds], queryFn: async () => { if (!bounds) return []; @@ -192,7 +215,10 @@ const MapboxMap = forwardRef( } const throttledSetBounds = useMemo( - () => throttle((b: Bounds | null) => setBounds(b), throttleDelay), + () => + debounce((nextBounds: Bounds | null) => { + setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); + }, throttleDelay), [throttleDelay] ); const debouncedSaveLocation = useMemo( @@ -228,14 +254,25 @@ const MapboxMap = forwardRef( return null; }, [isZoomedTooFarOut, setIsZoomedTooFarOut]); - const syncViewport = useCallback(async () => { + const syncViewport = useCallback(async (immediate = false) => { if (!mapRef.current) return; - const b = await readBoundsIfZoomed(); - throttledSetBounds(b); + const nextBounds = await readBoundsIfZoomed(); + if (immediate) { + throttledSetBounds.cancel(); + setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); + } else { + throttledSetBounds(nextBounds); + } debouncedSaveLocation(); throttledSetMapCenter(); }, [readBoundsIfZoomed, throttledSetBounds, debouncedSaveLocation, throttledSetMapCenter]); + useEffect(() => { + return () => { + throttledSetBounds.cancel(); + }; + }, [throttledSetBounds]); + const setTouchActive = useCallback( (isActive: boolean) => { if (isTouchActiveRef.current === isActive) return; @@ -285,6 +322,81 @@ const MapboxMap = forwardRef( ); const savedHotspotsSet = useMemo(() => new Set(savedHotspots.map((s) => s.hotspot_id)), [savedHotspots]); + const mapCandidateHotspots = useMemo( + () => hotspots.filter((hotspot) => !showSavedOnly || savedHotspotsSet.has(hotspot.id)), + [hotspots, savedHotspotsSet, showSavedOnly] + ); + const personalizedFilter = usePersonalizedHotspotFilter(mapCandidateHotspots.map((hotspot) => hotspot.id), { + enabled: bounds !== null && !isFetchingHotspots, + blockWhileDisabled: true, + }); + const personalizedHotspotIds = useMemo(() => new Set(personalizedFilter.filteredIds), [personalizedFilter.filteredIds]); + const isPersonalizedLoading = personalizedFilter.isActive && ((bounds !== null && isFetchingHotspots) || personalizedFilter.isLoading); + const displayedHotspots = useMemo(() => { + if (showSavedOnly || personalizedFilter.isActive) { + const resolvedHotspots = mapCandidateHotspots.filter((hotspot) => + personalizedFilter.isActive ? personalizedHotspotIds.has(hotspot.id) : true + ); + + if (isPersonalizedLoading) { + return lastResolvedHotspotsRef.current; + } + + return resolvedHotspots; + } + + return hotspots; + }, [ + hotspots, + isPersonalizedLoading, + mapCandidateHotspots, + personalizedFilter.isActive, + personalizedHotspotIds, + showSavedOnly, + ]); + + useEffect(() => { + if (!isPersonalizedLoading) { + lastResolvedHotspotsRef.current = displayedHotspots; + } + }, [displayedHotspots, isPersonalizedLoading]); + + useEffect(() => { + if (!personalizedFilter.isActive) { + return; + } + + const debugState = JSON.stringify({ + hasBounds: bounds !== null, + isFetchingHotspots, + hotspotCount: hotspots.length, + candidateCount: mapCandidateHotspots.length, + displayedCount: displayedHotspots.length, + isPersonalizedLoading, + }); + + if (lastPersonalizedMapDebugRef.current === debugState) { + return; + } + + lastPersonalizedMapDebugRef.current = debugState; + logPersonalizedHotspotFilterDebug("map personalized filter state", { + hasBounds: bounds !== null, + isFetchingHotspots, + hotspotCount: hotspots.length, + candidateCount: mapCandidateHotspots.length, + displayedCount: displayedHotspots.length, + isPersonalizedLoading, + }); + }, [ + displayedHotspots.length, + bounds, + hotspots.length, + isFetchingHotspots, + isPersonalizedLoading, + mapCandidateHotspots.length, + personalizedFilter.isActive, + ]); const handleFeaturePress = useCallback( (event: any) => { @@ -346,10 +458,14 @@ const MapboxMap = forwardRef( style={tw`flex-1`} styleURL={mapStyle} onDidFinishLoadingMap={() => setIsMapReady(true)} - onDidFinishLoadingStyle={syncViewport} - onCameraChanged={syncViewport} - onMapIdle={() => { + onDidFinishLoadingStyle={() => { + syncViewport(true); + }} + onCameraChanged={() => { syncViewport(); + }} + onMapIdle={() => { + syncViewport(true); centerMapOnUserInitial(); }} onPress={handleFeaturePress} @@ -393,14 +509,14 @@ const MapboxMap = forwardRef( )} - {isMapReady && (hotspots.length > 0 || savedPlaces.length > 0) && ( + {isMapReady && (displayedHotspots.length > 0 || savedPlaces.length > 0) && ( { + ...displayedHotspots.map((h: any) => { const isSaved = savedHotspotsSet.has(h.id); return { type: "Feature" as const, @@ -427,15 +543,10 @@ const MapboxMap = forwardRef( ], }} > - {/* Hotspot - hidden when showSavedOnly filter is active */} + {/* Hotspot */} @@ -446,7 +557,7 @@ const MapboxMap = forwardRef( style={savedHotspotSymbolStyle() as any} /> - {/* Selected hotspot - hidden when showSavedOnly filter is active */} + {/* Selected hotspot */} ( ["==", ["get", "featureType"], "hotspot"], ["==", ["get", "isSaved"], false], ["==", ["get", "isSelected"], true], - ["literal", !showSavedOnly], ]} style={haloInnerStyle() as any} /> @@ -465,7 +575,6 @@ const MapboxMap = forwardRef( ["==", ["get", "featureType"], "hotspot"], ["==", ["get", "isSaved"], false], ["==", ["get", "isSelected"], true], - ["literal", !showSavedOnly], ]} style={haloOuterStyle() as any} /> @@ -476,7 +585,6 @@ const MapboxMap = forwardRef( ["==", ["get", "featureType"], "hotspot"], ["==", ["get", "isSaved"], false], ["==", ["get", "isSelected"], true], - ["literal", !showSavedOnly], ]} style={hotspotSymbolStyle() as any} /> @@ -591,6 +699,22 @@ const MapboxMap = forwardRef( )} + {isPersonalizedLoading && !isZoomedTooFarOut && ( + + {Platform.OS === "ios" && isLiquidGlassAvailable() ? ( + + + Filtering hotspots... + + + ) : ( + + Filtering hotspots... + + )} + + )} + void; + keyboardType: "decimal-pad" | "number-pad"; + onChangeText: (text: string) => void; +}) { + return ( + + {label} + + + ); +} + +export default function PersonalizedHotspotFilterControls({ + hasLifeList, +}: PersonalizedHotspotFilterControlsProps) { + const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const setPersonalizedFilterEnabled = useFiltersStore((state) => state.setPersonalizedFilterEnabled); + const neededSpeciesMinCount = useFiltersStore((state) => state.neededSpeciesMinCount); + const setNeededSpeciesMinCount = useFiltersStore((state) => state.setNeededSpeciesMinCount); + const neededSpeciesMinPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); + const setNeededSpeciesMinPercent = useFiltersStore((state) => state.setNeededSpeciesMinPercent); + + const [countText, setCountText] = useState(String(neededSpeciesMinCount)); + const [percentText, setPercentText] = useState(String(neededSpeciesMinPercent)); + + useEffect(() => { + setCountText(String(neededSpeciesMinCount)); + }, [neededSpeciesMinCount]); + + useEffect(() => { + setPercentText(String(neededSpeciesMinPercent)); + }, [neededSpeciesMinPercent]); + + const commitCount = () => { + const parsedValue = Number.parseInt(countText.replace(/[^\d]/g, ""), 10); + const nextValue = Number.isFinite(parsedValue) ? parsedValue : neededSpeciesMinCount; + setNeededSpeciesMinCount(nextValue); + setCountText(String(nextValue)); + }; + + const commitPercent = () => { + const parsedValue = Number.parseFloat(percentText.replace(/[^0-9.]/g, "")); + const nextValue = Number.isFinite(parsedValue) ? parsedValue : neededSpeciesMinPercent; + setNeededSpeciesMinPercent(nextValue); + setPercentText(String(nextValue)); + }; + + return ( + + + + Personalized hotspot filter + + Show only hotspots with at least X needed species above Y%. + + + + + + {!hasLifeList ? ( + Import a life list to enable this filter. + ) : personalizedFilterEnabled ? ( + + setCountText(text.replace(/[^\d]/g, ""))} + onChangeValue={commitCount} + /> + setPercentText(text.replace(/[^0-9.]/g, ""))} + onChangeValue={commitPercent} + /> + + ) : null} + + ); +} diff --git a/hooks/usePersonalizedHotspotFilter.ts b/hooks/usePersonalizedHotspotFilter.ts new file mode 100644 index 0000000..eb841ed --- /dev/null +++ b/hooks/usePersonalizedHotspotFilter.ts @@ -0,0 +1,289 @@ +import { + createPersonalizedHotspotFilterBasis, + logPersonalizedHotspotFilterDebug, + personalizedHotspotCache, + syncPersonalizedHotspotCacheBasis, +} from "@/lib/personalizedHotspotFilter"; +import { useFiltersStore } from "@/stores/filtersStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { useEffect, useMemo, useRef, useState } from "react"; + +type UsePersonalizedHotspotFilterOptions = { + enabled?: boolean; + blockWhileDisabled?: boolean; +}; + +type PersonalizedHotspotFilterState = { + filteredIds: string[]; + isActive: boolean; + isLoading: boolean; + hasLifeList: boolean; +}; + +type AsyncPersonalizedHotspotFilterState = { + filteredIds: string[]; + isLoading: boolean; +}; + +function filterResolvedHotspotIds(hotspotIds: string[]): string[] { + return hotspotIds.filter((hotspotId) => personalizedHotspotCache.get(hotspotId) === true); +} + +function areStringArraysEqual(left: string[], right: string[]): boolean { + if (left.length !== right.length) { + return false; + } + + return left.every((value, index) => value === right[index]); +} + +export function usePersonalizedHotspotFilter( + hotspotIds: string[], + options: UsePersonalizedHotspotFilterOptions = {} +): PersonalizedHotspotFilterState { + const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const neededSpeciesMinCount = useFiltersStore((state) => state.neededSpeciesMinCount); + const neededSpeciesMinPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); + const lifelist = useSettingsStore((state) => state.lifelist); + const lifelistExclusions = useSettingsStore((state) => state.lifelistExclusions); + const targetMonths = useSettingsStore((state) => state.targetMonths); + + const basis = useMemo( + () => + createPersonalizedHotspotFilterBasis({ + lifelist, + lifelistExclusions, + targetMonths, + neededSpeciesMinCount, + neededSpeciesMinPercent, + }), + [lifelist, lifelistExclusions, targetMonths, neededSpeciesMinCount, neededSpeciesMinPercent] + ); + + const hasLifeList = basis !== null; + const isActive = personalizedFilterEnabled && hasLifeList; + const isEnabled = options.enabled ?? true; + const candidateKey = useMemo(() => hotspotIds.join("|"), [hotspotIds]); + const stableHotspotIdsRef = useRef(hotspotIds); + const basisRef = useRef(basis); + const asyncStateRef = useRef({ filteredIds: [], isLoading: false }); + const lastDebugStatusRef = useRef(null); + const logDebugStatusRef = useRef<(status: string, details?: Record) => void>(() => {}); + + if (stableHotspotIdsRef.current !== hotspotIds && stableHotspotIdsRef.current.join("|") !== candidateKey) { + stableHotspotIdsRef.current = hotspotIds; + } + + const stableHotspotIds = stableHotspotIdsRef.current; + + const [asyncState, setAsyncState] = useState({ + filteredIds: [], + isLoading: false, + }); + + useEffect(() => { + basisRef.current = basis; + }, [basis]); + + useEffect(() => { + asyncStateRef.current = asyncState; + }, [asyncState]); + + logDebugStatusRef.current = (status: string, details?: Record) => { + const statusKey = JSON.stringify({ + status, + candidateCount: stableHotspotIds.length, + filteredCount: asyncStateRef.current.filteredIds.length, + isLoading: asyncStateRef.current.isLoading, + isEnabled, + isActive, + hasLifeList, + ...details, + }); + + if (lastDebugStatusRef.current === statusKey) { + return; + } + + lastDebugStatusRef.current = statusKey; + logPersonalizedHotspotFilterDebug(status, { + candidateCount: stableHotspotIds.length, + filteredCount: asyncStateRef.current.filteredIds.length, + isLoading: asyncStateRef.current.isLoading, + isEnabled, + isActive, + hasLifeList, + ...details, + }); + }; + + useEffect(() => { + syncPersonalizedHotspotCacheBasis(basis?.cacheKey ?? null); + }, [basis?.cacheKey]); + + useEffect(() => { + if (!isActive) { + logDebugStatusRef.current("hook inactive"); + return; + } + + if (!isEnabled) { + logDebugStatusRef.current("waiting for prerequisites", { + blockWhileDisabled: options.blockWhileDisabled ?? false, + }); + return; + } + + if (stableHotspotIds.length === 0) { + logDebugStatusRef.current("no candidate hotspots"); + return; + } + + if (!basisRef.current) { + logDebugStatusRef.current("missing filter basis"); + return; + } + + const basisForRun = basisRef.current; + const unresolvedHotspotIds = stableHotspotIds.filter((hotspotId) => !personalizedHotspotCache.has(hotspotId)); + if (unresolvedHotspotIds.length === 0) { + const filteredIds = filterResolvedHotspotIds(stableHotspotIds); + logDebugStatusRef.current("all candidates resolved from cache", { + matchedCount: filteredIds.length, + }); + setAsyncState((currentState) => { + if (!currentState.isLoading && areStringArraysEqual(currentState.filteredIds, filteredIds)) { + return currentState; + } + + return { + filteredIds, + isLoading: false, + }; + }); + return; + } + + const abortController = new AbortController(); + logDebugStatusRef.current("evaluating unresolved hotspots", { + unresolvedCount: unresolvedHotspotIds.length, + cachedCount: stableHotspotIds.length - unresolvedHotspotIds.length, + }); + + setAsyncState((currentState) => ({ + filteredIds: currentState.isLoading ? currentState.filteredIds : [], + isLoading: true, + })); + + void personalizedHotspotCache + .evaluateMany(unresolvedHotspotIds, basisForRun, abortController.signal) + .then(() => { + if (abortController.signal.aborted) { + return; + } + + const stillUnresolved = stableHotspotIds.some((hotspotId) => !personalizedHotspotCache.has(hotspotId)); + if (stillUnresolved) { + logDebugStatusRef.current("evaluation completed but candidates still unresolved"); + setAsyncState((currentState) => ({ + filteredIds: currentState.filteredIds, + isLoading: true, + })); + return; + } + + const filteredIds = filterResolvedHotspotIds(stableHotspotIds); + logDebugStatusRef.current("evaluation resolved", { + matchedCount: filteredIds.length, + unresolvedCount: 0, + }); + setAsyncState((currentState) => { + if (!currentState.isLoading && areStringArraysEqual(currentState.filteredIds, filteredIds)) { + return currentState; + } + + return { + filteredIds, + isLoading: false, + }; + }); + }) + .catch((error) => { + if (abortController.signal.aborted || error?.name === "AbortError") { + logDebugStatusRef.current("evaluation aborted"); + return; + } + + logDebugStatusRef.current("evaluation failed"); + console.error("Failed to evaluate personalized hotspot filter", error); + setAsyncState((currentState) => ({ + filteredIds: currentState.filteredIds, + isLoading: false, + })); + }); + + return () => { + abortController.abort(); + }; + }, [ + candidateKey, + hasLifeList, + isActive, + isEnabled, + options.blockWhileDisabled, + stableHotspotIds, + ]); + + return useMemo(() => { + if (!isActive) { + return { + filteredIds: stableHotspotIds, + isActive: false, + isLoading: false, + hasLifeList, + }; + } + + if (!isEnabled && !options.blockWhileDisabled) { + return { + filteredIds: stableHotspotIds, + isActive: true, + isLoading: false, + hasLifeList, + }; + } + + if (!isEnabled && options.blockWhileDisabled) { + return { + filteredIds: [], + isActive: true, + isLoading: stableHotspotIds.length > 0, + hasLifeList, + }; + } + + if (stableHotspotIds.length === 0) { + return { + filteredIds: [], + isActive: true, + isLoading: false, + hasLifeList, + }; + } + + return { + filteredIds: asyncState.filteredIds, + isActive: true, + isLoading: asyncState.isLoading, + hasLifeList, + }; + }, [ + asyncState.filteredIds, + asyncState.isLoading, + hasLifeList, + isActive, + isEnabled, + options.blockWhileDisabled, + stableHotspotIds, + ]); +} diff --git a/lib/database.ts b/lib/database.ts index f7b8ecc..2ba4d96 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -1,5 +1,6 @@ import * as SQLite from "expo-sqlite"; import { BirdPlanTripData, SavedPlace, StaticPackHotspot, StaticPackTarget, Trip } from "./types"; +import { aggregateHotspotTargets, getMonthIndices, getTotalSamplesForMonths, parseHotspotTargetData } from "./hotspotTargets"; let db: SQLite.SQLiteDatabase | null = null; let isInstallingPack = false; @@ -637,6 +638,34 @@ export type HotspotTargetsResult = { version: string | null; }; +const TARGET_QUERY_BATCH_SIZE = 400; + +export async function getTargetDataForHotspots(hotspotIds: string[]): Promise> { + if (!db) throw new Error("Database not initialized"); + + const uniqueHotspotIds = [...new Set(hotspotIds)]; + const targetData = new Map(); + + for (let index = 0; index < uniqueHotspotIds.length; index += TARGET_QUERY_BATCH_SIZE) { + const batch = uniqueHotspotIds.slice(index, index + TARGET_QUERY_BATCH_SIZE); + if (batch.length === 0) { + continue; + } + + const placeholders = batch.map(() => "?").join(", "); + const rows = await db.getAllAsync<{ id: string; data: string }>( + `SELECT id, data FROM targets WHERE id IN (${placeholders})`, + batch + ); + + for (const row of rows) { + targetData.set(row.id, row.data); + } + } + + return targetData; +} + export async function getTargetsForHotspot(hotspotId: string, months?: number[]): Promise { if (!db) throw new Error("Database not initialized"); @@ -648,40 +677,12 @@ export async function getTargetsForHotspot(hotspotId: string, months?: number[]) if (!result) return null; const row = result as { data: string; version: string | null }; - const data = JSON.parse(row.data) as { - samples: (number | null)[]; - species: (string | number)[][]; - }; - - // Determine which month indices to aggregate (0-11) - const monthIndices = months && months.length > 0 ? months : data.samples.map((_, i) => i); - - const totalSamples = monthIndices.reduce((sum, i) => sum + (data.samples[i] ?? 0), 0); + const data = parseHotspotTargetData(row.data); + const monthIndices = getMonthIndices(data, months); + const totalSamples = getTotalSamplesForMonths(data, monthIndices); if (totalSamples === 0) return { samples: 0, targets: [], version: row.version }; - - // Aggregate observations per species for selected months - const speciesMap = new Map(); - for (const speciesEntry of data.species) { - const speciesCode = String(speciesEntry[0]); - // Species entry layout: [code, janObs, febObs, ..., decObs] — index i+1 for month i - const totalObs = monthIndices.reduce((sum, i) => { - const val = speciesEntry[i + 1]; - return sum + (typeof val === "number" ? val : 0); - }, 0); - if (totalObs > 0) { - speciesMap.set(speciesCode, (speciesMap.get(speciesCode) ?? 0) + totalObs); - } - } - - // Convert to array, calculate percentages, and sort by percentage descending - const targets: HotspotTarget[] = Array.from(speciesMap.entries()) - .map(([speciesCode, observations]) => ({ - speciesCode, - observations, - percentage: (observations / totalSamples) * 100, - })) - .sort((a, b) => b.percentage - a.percentage); + const targets = aggregateHotspotTargets(data, monthIndices, totalSamples); return { samples: totalSamples, targets, version: row.version }; } diff --git a/lib/hotspotTargets.ts b/lib/hotspotTargets.ts new file mode 100644 index 0000000..4e3f9ad --- /dev/null +++ b/lib/hotspotTargets.ts @@ -0,0 +1,54 @@ +export type RawHotspotTargetData = { + samples: (number | null)[]; + species: (string | number)[][]; +}; + +export type AggregatedHotspotTarget = { + speciesCode: string; + observations: number; + percentage: number; +}; + +export function parseHotspotTargetData(rawData: string): RawHotspotTargetData { + return JSON.parse(rawData) as RawHotspotTargetData; +} + +export function getMonthIndices(data: RawHotspotTargetData, months?: number[]): number[] { + return months && months.length > 0 ? months : data.samples.map((_, index) => index); +} + +export function getTotalSamplesForMonths(data: RawHotspotTargetData, monthIndices: number[]): number { + return monthIndices.reduce((sum, monthIndex) => sum + (data.samples[monthIndex] ?? 0), 0); +} + +export function aggregateHotspotTargets( + data: RawHotspotTargetData, + monthIndices: number[], + totalSamples: number +): AggregatedHotspotTarget[] { + if (totalSamples === 0) { + return []; + } + + const speciesMap = new Map(); + + for (const speciesEntry of data.species) { + const speciesCode = String(speciesEntry[0]); + const totalObservations = monthIndices.reduce((sum, monthIndex) => { + const value = speciesEntry[monthIndex + 1]; + return sum + (typeof value === "number" ? value : 0); + }, 0); + + if (totalObservations > 0) { + speciesMap.set(speciesCode, (speciesMap.get(speciesCode) ?? 0) + totalObservations); + } + } + + return Array.from(speciesMap.entries()) + .map(([speciesCode, observations]) => ({ + speciesCode, + observations, + percentage: (observations / totalSamples) * 100, + })) + .sort((a, b) => b.percentage - a.percentage); +} diff --git a/lib/personalizedHotspotFilter.ts b/lib/personalizedHotspotFilter.ts new file mode 100644 index 0000000..d397b7f --- /dev/null +++ b/lib/personalizedHotspotFilter.ts @@ -0,0 +1,287 @@ +import { LifeListEntry } from "@/stores/settingsStore"; +import { getTargetDataForHotspots } from "./database"; +import { getMonthIndices, getTotalSamplesForMonths, parseHotspotTargetData } from "./hotspotTargets"; + +const CACHE_CAPACITY = 5_000; +const COMPUTE_BATCH_SIZE = 50; +const DEBUG_PERSONALIZED_FILTER = __DEV__; + +let evaluationRunCounter = 0; + +export type PersonalizedHotspotFilterBasis = { + cacheKey: string; + lifeListCodes: ReadonlySet; + excludedCodes: ReadonlySet; + selectedMonths: number[]; + neededSpeciesMinCount: number; + neededSpeciesMinPercent: number; +}; + +export function logPersonalizedHotspotFilterDebug(message: string, details?: Record) { + if (!DEBUG_PERSONALIZED_FILTER) { + return; + } + + if (details) { + console.log(`[personalized-hotspot-filter] ${message}`, details); + return; + } + + console.log(`[personalized-hotspot-filter] ${message}`); +} + +export function normalizeNeededSpeciesMinCount(value: number): number { + if (!Number.isFinite(value)) return 1; + return Math.max(1, Math.floor(value)); +} + +export function normalizeNeededSpeciesMinPercent(value: number): number { + if (!Number.isFinite(value)) return 1; + return Math.min(100, Math.max(0, value)); +} + +export function createPersonalizedHotspotFilterBasis(params: { + lifelist: LifeListEntry[] | null; + lifelistExclusions: string[] | null; + targetMonths: number[]; + neededSpeciesMinCount: number; + neededSpeciesMinPercent: number; +}): PersonalizedHotspotFilterBasis | null { + const lifeListCodes = [...new Set((params.lifelist ?? []).map((entry) => entry.code))].sort(); + + if (lifeListCodes.length === 0) { + return null; + } + + const excludedCodes = [...new Set(params.lifelistExclusions ?? [])].sort(); + const selectedMonths = [...new Set(params.targetMonths)].sort((a, b) => a - b); + const neededSpeciesMinCount = normalizeNeededSpeciesMinCount(params.neededSpeciesMinCount); + const neededSpeciesMinPercent = normalizeNeededSpeciesMinPercent(params.neededSpeciesMinPercent); + + return { + cacheKey: JSON.stringify({ + lifeListCodes, + excludedCodes, + selectedMonths, + neededSpeciesMinCount, + neededSpeciesMinPercent, + }), + lifeListCodes: new Set(lifeListCodes), + excludedCodes: new Set(excludedCodes), + selectedMonths, + neededSpeciesMinCount, + neededSpeciesMinPercent, + }; +} + +function createAbortError(): Error { + const error = new Error("Personalized hotspot evaluation aborted"); + error.name = "AbortError"; + return error; +} + +function throwIfAborted(signal?: AbortSignal) { + if (signal?.aborted) { + throw createAbortError(); + } +} + +function matchesPersonalizedHotspotFilter(rawData: string, basis: PersonalizedHotspotFilterBasis): boolean { + const parsed = parseHotspotTargetData(rawData); + const monthIndices = getMonthIndices(parsed, basis.selectedMonths); + const totalSamples = getTotalSamplesForMonths(parsed, monthIndices); + + if (totalSamples === 0) { + return false; + } + + let qualifyingSpeciesCount = 0; + + for (const speciesEntry of parsed.species) { + const speciesCode = String(speciesEntry[0]); + + if (basis.lifeListCodes.has(speciesCode) || basis.excludedCodes.has(speciesCode)) { + continue; + } + + const observations = monthIndices.reduce((sum, monthIndex) => { + const value = speciesEntry[monthIndex + 1]; + return sum + (typeof value === "number" ? value : 0); + }, 0); + + if (observations === 0) { + continue; + } + + const percentage = (observations / totalSamples) * 100; + if (percentage >= basis.neededSpeciesMinPercent) { + qualifyingSpeciesCount += 1; + if (qualifyingSpeciesCount >= basis.neededSpeciesMinCount) { + return true; + } + } + } + + return false; +} + +class PersonalizedHotspotCache { + private cache = new Map(); + private activeSignals = new Set(); + + clear() { + logPersonalizedHotspotFilterDebug("cache clear", { previousSize: this.cache.size }); + this.cache.clear(); + } + + has(hotspotId: string): boolean { + return this.cache.has(hotspotId); + } + + get(hotspotId: string): boolean | undefined { + const value = this.cache.get(hotspotId); + if (value === undefined) { + return undefined; + } + + this.cache.delete(hotspotId); + this.cache.set(hotspotId, value); + return value; + } + + cancelActiveRun() { + if (this.activeSignals.size > 0) { + logPersonalizedHotspotFilterDebug("cancel active runs", { activeRunCount: this.activeSignals.size }); + } + + for (const controller of this.activeSignals) { + controller.abort(); + } + this.activeSignals.clear(); + } + + async evaluateMany( + hotspotIds: string[], + basis: PersonalizedHotspotFilterBasis, + signal?: AbortSignal + ): Promise { + const missingHotspotIds = [...new Set(hotspotIds)].filter((hotspotId) => !this.cache.has(hotspotId)); + if (missingHotspotIds.length === 0) { + return; + } + + const runId = ++evaluationRunCounter; + const controller = new AbortController(); + this.activeSignals.add(controller); + + const combinedSignal = controller.signal; + const isAborted = () => combinedSignal.aborted || signal?.aborted; + + try { + logPersonalizedHotspotFilterDebug("evaluate start", { + runId, + requestedCount: hotspotIds.length, + missingCount: missingHotspotIds.length, + cachedCount: hotspotIds.length - missingHotspotIds.length, + cacheSize: this.cache.size, + monthCount: basis.selectedMonths.length === 0 ? 12 : basis.selectedMonths.length, + neededSpeciesMinCount: basis.neededSpeciesMinCount, + neededSpeciesMinPercent: basis.neededSpeciesMinPercent, + }); + + throwIfAborted(signal); + const targetData = await getTargetDataForHotspots(missingHotspotIds); + throwIfAborted(signal); + + logPersonalizedHotspotFilterDebug("target data fetched", { + runId, + requestedCount: missingHotspotIds.length, + foundCount: targetData.size, + missingDataCount: missingHotspotIds.length - targetData.size, + }); + + for (let index = 0; index < missingHotspotIds.length; index += COMPUTE_BATCH_SIZE) { + if (isAborted()) { + throw createAbortError(); + } + + const batch = missingHotspotIds.slice(index, index + COMPUTE_BATCH_SIZE); + for (const hotspotId of batch) { + if (isAborted()) { + throw createAbortError(); + } + + const rawData = targetData.get(hotspotId); + this.set(hotspotId, rawData ? matchesPersonalizedHotspotFilter(rawData, basis) : false); + } + + const processedCount = Math.min(index + batch.length, missingHotspotIds.length); + if ( + processedCount === missingHotspotIds.length || + (missingHotspotIds.length > 250 && processedCount % 250 === 0) + ) { + logPersonalizedHotspotFilterDebug("evaluate progress", { + runId, + processedCount, + totalCount: missingHotspotIds.length, + cacheSize: this.cache.size, + }); + } + + if (index + COMPUTE_BATCH_SIZE < missingHotspotIds.length) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + logPersonalizedHotspotFilterDebug("evaluate complete", { + runId, + computedCount: missingHotspotIds.length, + cacheSize: this.cache.size, + }); + } catch (error) { + if ((error as Error | undefined)?.name === "AbortError") { + logPersonalizedHotspotFilterDebug("evaluate aborted", { + runId, + processedCandidateCount: missingHotspotIds.length, + cacheSize: this.cache.size, + }); + } + throw error; + } finally { + this.activeSignals.delete(controller); + } + } + + private set(hotspotId: string, value: boolean) { + if (this.cache.has(hotspotId)) { + this.cache.delete(hotspotId); + } + + this.cache.set(hotspotId, value); + + while (this.cache.size > CACHE_CAPACITY) { + const oldestHotspotId = this.cache.keys().next().value; + if (!oldestHotspotId) { + break; + } + this.cache.delete(oldestHotspotId); + } + } +} + +export const personalizedHotspotCache = new PersonalizedHotspotCache(); + +let activeBasisKey: string | null = null; + +export function syncPersonalizedHotspotCacheBasis(nextBasisKey: string | null) { + if (activeBasisKey === nextBasisKey) { + return; + } + + logPersonalizedHotspotFilterDebug("basis changed", { + hadBasis: activeBasisKey !== null, + hasBasis: nextBasisKey !== null, + }); + activeBasisKey = nextBasisKey; + personalizedHotspotCache.cancelActiveRun(); + personalizedHotspotCache.clear(); +} diff --git a/stores/filtersStore.ts b/stores/filtersStore.ts index 3458e52..ac1913c 100644 --- a/stores/filtersStore.ts +++ b/stores/filtersStore.ts @@ -1,13 +1,23 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; +import { + normalizeNeededSpeciesMinCount, + normalizeNeededSpeciesMinPercent, +} from "@/lib/personalizedHotspotFilter"; type FiltersState = { showSavedOnly: boolean; + personalizedFilterEnabled: boolean; + neededSpeciesMinCount: number; + neededSpeciesMinPercent: number; }; type FiltersActions = { setShowSavedOnly: (value: boolean) => void; + setPersonalizedFilterEnabled: (value: boolean) => void; + setNeededSpeciesMinCount: (value: number) => void; + setNeededSpeciesMinPercent: (value: number) => void; resetFilters: () => void; }; @@ -15,8 +25,21 @@ export const useFiltersStore = create()( persist( (set) => ({ showSavedOnly: false, + personalizedFilterEnabled: false, + neededSpeciesMinCount: 1, + neededSpeciesMinPercent: 1, setShowSavedOnly: (value) => set({ showSavedOnly: value }), - resetFilters: () => set({ showSavedOnly: false }), + setPersonalizedFilterEnabled: (value) => set({ personalizedFilterEnabled: value }), + setNeededSpeciesMinCount: (value) => set({ neededSpeciesMinCount: normalizeNeededSpeciesMinCount(value) }), + setNeededSpeciesMinPercent: (value) => + set({ neededSpeciesMinPercent: normalizeNeededSpeciesMinPercent(value) }), + resetFilters: () => + set({ + showSavedOnly: false, + personalizedFilterEnabled: false, + neededSpeciesMinCount: 1, + neededSpeciesMinPercent: 1, + }), }), { name: "filters-storage", From a6c25a7090e24df29ef664ad9f9da5e6dc6359ab Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 28 May 2026 18:13:28 -0700 Subject: [PATCH 02/23] Keep existing throttled loading when not filtering --- components/Mapbox.tsx | 58 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/components/Mapbox.tsx b/components/Mapbox.tsx index ab8cec6..9f1e92a 100644 --- a/components/Mapbox.tsx +++ b/components/Mapbox.tsx @@ -7,13 +7,14 @@ import { savedHotspotSymbolStyle, savedPlaceSymbolStyle, } from "@/lib/layers"; -import { logPersonalizedHotspotFilterDebug } from "@/lib/personalizedHotspotFilter"; +import { logPersonalizedHotspotFilterDebug, personalizedHotspotCache } from "@/lib/personalizedHotspotFilter"; import tw from "@/lib/tw"; import { OnPressEvent } from "@/lib/types"; import { findClosestFeature, getMarkerColorIndex, padBoundsBySize } from "@/lib/utils"; import { useFiltersStore } from "@/stores/filtersStore"; import { useLocationPermissionStore } from "@/stores/locationPermissionStore"; import { useMapStore } from "@/stores/mapStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import Mapbox from "@rnmapbox/maps"; import { useQuery } from "@tanstack/react-query"; import Constants from "expo-constants"; @@ -102,8 +103,9 @@ export type MapboxMapRef = { centerOnCoordinates: (lng: number, lat: number, offsetY?: number) => void; }; -const THROTTLE_DELAY = 300; -const THROTTLE_DELAY_WITH_OPEN_HOTSPOT = 150; // Load hotspots faster when jumping to a hotspot from list modal +const THROTTLE_DELAY = 750; +const THROTTLE_DELAY_WITH_OPEN_HOTSPOT = 250; // Load hotspots faster when jumping to a hotspot from list modal +const SETTLED_BOUNDS_DELAY = 300; const MIN_ZOOM = 8; const DEFAULT_USER_ZOOM = 14; const DEFAULT_HOTSPOT_ZOOM = 13; @@ -160,7 +162,8 @@ const MapboxMap = forwardRef( const showAttribution = useMapStore((state) => state.isMapAttributionOpen); const setShowAttribution = useMapStore((state) => state.setIsMapAttributionOpen); const { status: permissionStatus } = useLocationPermissionStore(); - const { showSavedOnly } = useFiltersStore(); + const { showSavedOnly, personalizedFilterEnabled } = useFiltersStore(); + const lifelist = useSettingsStore((state) => state.lifelist); const mapRef = useRef(null); const cameraRef = useRef(null); @@ -173,6 +176,8 @@ const MapboxMap = forwardRef( const [isMapReady, setIsMapReady] = useState(false); const [bounds, setBounds] = useState(null); + const hasLifeList = (lifelist?.length ?? 0) > 0; + const isPersonalizedMapFiltering = personalizedFilterEnabled && hasLifeList; const mapStyle = useMemo(() => { return currentLayer === "satellite" @@ -216,11 +221,18 @@ const MapboxMap = forwardRef( const throttledSetBounds = useMemo( () => - debounce((nextBounds: Bounds | null) => { + throttle((nextBounds: Bounds | null) => { setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); }, throttleDelay), [throttleDelay] ); + const debouncedSetSettledBounds = useMemo( + () => + debounce((nextBounds: Bounds | null) => { + setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); + }, SETTLED_BOUNDS_DELAY), + [] + ); const debouncedSaveLocation = useMemo( () => debounce(async () => { @@ -259,19 +271,36 @@ const MapboxMap = forwardRef( const nextBounds = await readBoundsIfZoomed(); if (immediate) { throttledSetBounds.cancel(); + debouncedSetSettledBounds.cancel(); setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); + } else if (isPersonalizedMapFiltering) { + debouncedSetSettledBounds(nextBounds); } else { throttledSetBounds(nextBounds); } debouncedSaveLocation(); throttledSetMapCenter(); - }, [readBoundsIfZoomed, throttledSetBounds, debouncedSaveLocation, throttledSetMapCenter]); + }, [ + debouncedSaveLocation, + debouncedSetSettledBounds, + isPersonalizedMapFiltering, + readBoundsIfZoomed, + throttledSetBounds, + throttledSetMapCenter, + ]); useEffect(() => { return () => { throttledSetBounds.cancel(); + debouncedSetSettledBounds.cancel(); }; - }, [throttledSetBounds]); + }, [debouncedSetSettledBounds, throttledSetBounds]); + + useEffect(() => { + throttledSetBounds.cancel(); + debouncedSetSettledBounds.cancel(); + void syncViewport(true); + }, [debouncedSetSettledBounds, isPersonalizedMapFiltering, syncViewport, throttledSetBounds]); const setTouchActive = useCallback( (isActive: boolean) => { @@ -331,7 +360,17 @@ const MapboxMap = forwardRef( blockWhileDisabled: true, }); const personalizedHotspotIds = useMemo(() => new Set(personalizedFilter.filteredIds), [personalizedFilter.filteredIds]); - const isPersonalizedLoading = personalizedFilter.isActive && ((bounds !== null && isFetchingHotspots) || personalizedFilter.isLoading); + const unresolvedCandidateCount = personalizedFilter.isActive + ? mapCandidateHotspots.reduce((count, hotspot) => count + (personalizedHotspotCache.has(hotspot.id) ? 0 : 1), 0) + : 0; + const isInitialPersonalizedFetch = + personalizedFilter.isActive && + bounds !== null && + isFetchingHotspots && + lastResolvedHotspotsRef.current.length === 0; + const isPersonalizedLoading = + personalizedFilter.isActive && + (isInitialPersonalizedFetch || unresolvedCandidateCount > 0 || personalizedFilter.isLoading); const displayedHotspots = useMemo(() => { if (showSavedOnly || personalizedFilter.isActive) { const resolvedHotspots = mapCandidateHotspots.filter((hotspot) => @@ -371,6 +410,7 @@ const MapboxMap = forwardRef( isFetchingHotspots, hotspotCount: hotspots.length, candidateCount: mapCandidateHotspots.length, + unresolvedCandidateCount, displayedCount: displayedHotspots.length, isPersonalizedLoading, }); @@ -385,6 +425,7 @@ const MapboxMap = forwardRef( isFetchingHotspots, hotspotCount: hotspots.length, candidateCount: mapCandidateHotspots.length, + unresolvedCandidateCount, displayedCount: displayedHotspots.length, isPersonalizedLoading, }); @@ -396,6 +437,7 @@ const MapboxMap = forwardRef( isPersonalizedLoading, mapCandidateHotspots.length, personalizedFilter.isActive, + unresolvedCandidateCount, ]); const handleFeaturePress = useCallback( From eef0869059498422931a60c8fed8a2d5919638ed Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 11 Jun 2026 17:08:16 -0700 Subject: [PATCH 03/23] Move filters UI --- app/index.tsx | 16 +++++++++----- components/CountBadge.tsx | 20 ++++++++++++++++++ components/FilterSection.tsx | 38 ---------------------------------- components/HotspotList.tsx | 4 ++-- components/MenuBottomSheet.tsx | 2 -- components/MenuList.tsx | 2 +- hooks/useActiveFilterCount.ts | 16 ++++++++++++++ 7 files changed, 50 insertions(+), 48 deletions(-) create mode 100644 components/CountBadge.tsx delete mode 100644 components/FilterSection.tsx create mode 100644 hooks/useActiveFilterCount.ts diff --git a/app/index.tsx b/app/index.tsx index 36d02e1..b584519 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,3 +1,4 @@ +import CountBadge from "@/components/CountBadge"; import FloatingButton from "@/components/FloatingButton"; import HotspotDialog from "@/components/HotspotDialog"; import HotspotList from "@/components/HotspotList"; @@ -9,6 +10,7 @@ import PlaceDialog from "@/components/PlaceDialog"; import SunIndicator from "@/components/SunIndicator"; import { useInstalledPacks } from "@/hooks/useInstalledPacks"; import { usePackUpdates } from "@/hooks/usePackUpdates"; +import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { useSavedLocation } from "@/hooks/useSavedLocation"; import tw from "@/lib/tw"; import { useMapStore } from "@/stores/mapStore"; @@ -39,7 +41,8 @@ export default function HomeScreen() { setIsMapAttributionOpen, } = useMapStore(); const { data: installedPacks, isLoading: isLoadingInstalledPacks } = useInstalledPacks(); - const { hasUpdates } = usePackUpdates(); + const { updateCount } = usePackUpdates(); + const activeFilterCount = useActiveFilterCount(); const handleMapPress = (_event: any) => { if (isMenuOpen) handleCloseBottomSheet(); @@ -171,14 +174,17 @@ export default function HomeScreen() { - - - + + + + + + - {hasUpdates && } + diff --git a/components/CountBadge.tsx b/components/CountBadge.tsx new file mode 100644 index 0000000..158fd02 --- /dev/null +++ b/components/CountBadge.tsx @@ -0,0 +1,20 @@ +import tw from "@/lib/tw"; +import React from "react"; +import { Text, View } from "react-native"; + +/** + * Small count badge overlaid on the top-right corner of a floating map button. + * Renders nothing when count is zero. Used for active filters (Hotspots button) + * and pending pack updates (Map Options button). + */ +export default function CountBadge({ count }: { count: number }) { + if (count <= 0) return null; + + return ( + + {count} + + ); +} diff --git a/components/FilterSection.tsx b/components/FilterSection.tsx deleted file mode 100644 index 23fbbc5..0000000 --- a/components/FilterSection.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import PersonalizedHotspotFilterControls from "@/components/PersonalizedHotspotFilterControls"; -import tw from "@/lib/tw"; -import { useFiltersStore } from "@/stores/filtersStore"; -import { useSettingsStore } from "@/stores/settingsStore"; -import React from "react"; -import { Platform, Switch, Text, View } from "react-native"; -import { BorderlessButton } from "react-native-gesture-handler"; - -export default function FilterSection() { - const { showSavedOnly, setShowSavedOnly } = useFiltersStore(); - const lifelist = useSettingsStore((state) => state.lifelist); - const hasLifeList = (lifelist?.length ?? 0) > 0; - - const content = ( - - Show saved only - - - ); - - if (Platform.OS === "android") { - return ( - - setShowSavedOnly(!showSavedOnly)} activeOpacity={1}> - {content} - - - - ); - } - - return ( - - {content} - - - ); -} diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index feb440f..106cbe1 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -1,3 +1,4 @@ +import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { usePersonalizedHotspotFilter } from "@/hooks/usePersonalizedHotspotFilter"; import { useLocation } from "@/hooks/useLocation"; import { useScrollRestore } from "@/hooks/useScrollRestore"; @@ -38,10 +39,9 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo const { location, isLoading: isLoadingUserLocation } = useLocation(isOpen); const isLoadingLocation = isLoadingPermission || isLoadingUserLocation; const { showSavedOnly, setShowSavedOnly } = useFiltersStore(); - const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); const lifelist = useSettingsStore((state) => state.lifelist); const hasLifeList = (lifelist?.length ?? 0) > 0; - const activeFilterCount = [showSavedOnly, personalizedFilterEnabled && hasLifeList].filter(Boolean).length; + const activeFilterCount = useActiveFilterCount(); const dismissRef = useRef<(() => Promise) | null>(null); const [searchQuery, setSearchQuery] = useState(""); diff --git a/components/MenuBottomSheet.tsx b/components/MenuBottomSheet.tsx index 0169b8c..c29b2e2 100644 --- a/components/MenuBottomSheet.tsx +++ b/components/MenuBottomSheet.tsx @@ -4,7 +4,6 @@ import { useRouter } from "expo-router"; import React from "react"; import { View } from "react-native"; import BaseBottomSheet from "./BaseBottomSheet"; -import FilterSection from "./FilterSection"; import MapLayerSwitcher from "./MapLayerSwitcher"; import MenuList from "./MenuList"; @@ -27,7 +26,6 @@ export default function MenuBottomSheet({ isOpen, onClose }: MenuBottomSheetProp {(dismiss) => ( - { await dismiss(); diff --git a/components/MenuList.tsx b/components/MenuList.tsx index 1b31d36..7b8f2b0 100644 --- a/components/MenuList.tsx +++ b/components/MenuList.tsx @@ -54,7 +54,7 @@ export default function MenuList({ onNavigateToPacks, onNavigateToSettings, pack ); return ( - + {menuOptions.map((item) => ( {renderMenuItem({ item })} ))} diff --git a/hooks/useActiveFilterCount.ts b/hooks/useActiveFilterCount.ts new file mode 100644 index 0000000..b44986b --- /dev/null +++ b/hooks/useActiveFilterCount.ts @@ -0,0 +1,16 @@ +import { useFiltersStore } from "@/stores/filtersStore"; +import { useSettingsStore } from "@/stores/settingsStore"; + +/** + * Number of hotspot filters currently active. Drives the badge on both the + * Nearby Hotspots map button and the filter toggle inside the hotspot list. + * The personalized filter only counts when a life list is actually imported. + */ +export function useActiveFilterCount() { + const showSavedOnly = useFiltersStore((state) => state.showSavedOnly); + const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const lifelist = useSettingsStore((state) => state.lifelist); + const hasLifeList = (lifelist?.length ?? 0) > 0; + + return [showSavedOnly, personalizedFilterEnabled && hasLifeList].filter(Boolean).length; +} From f653a713bc54a05538ac5b9566329977449f9683 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 11 Jun 2026 17:30:37 -0700 Subject: [PATCH 04/23] Adjust target hotspots feature UI --- .../PersonalizedHotspotFilterControls.tsx | 135 ++++++++---------- stores/filtersStore.ts | 8 +- 2 files changed, 61 insertions(+), 82 deletions(-) diff --git a/components/PersonalizedHotspotFilterControls.tsx b/components/PersonalizedHotspotFilterControls.tsx index 0584151..b39dd42 100644 --- a/components/PersonalizedHotspotFilterControls.tsx +++ b/components/PersonalizedHotspotFilterControls.tsx @@ -1,109 +1,88 @@ import tw from "@/lib/tw"; import { useFiltersStore } from "@/stores/filtersStore"; -import React, { useEffect, useState } from "react"; -import { Switch, Text, TextInput, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import React from "react"; +import { Pressable, Switch, Text, View } from "react-native"; type PersonalizedHotspotFilterControlsProps = { hasLifeList: boolean; }; -function FilterNumberField({ - label, - value, - onChangeValue, - keyboardType, - onChangeText, +const PERCENT_STEP = 10; +const MIN_PERCENT = 10; + +function StepperButton({ + icon, + onPress, + disabled, }: { - label: string; - value: string; - onChangeValue: () => void; - keyboardType: "decimal-pad" | "number-pad"; - onChangeText: (text: string) => void; + icon: keyof typeof Ionicons.glyphMap; + onPress: () => void; + disabled?: boolean; }) { return ( - - {label} - - + [ + tw`w-9 h-9 rounded-full items-center justify-center border border-gray-300`, + disabled && tw`opacity-40`, + pressed && !disabled && tw`bg-gray-100`, + ]} + > + + ); } export default function PersonalizedHotspotFilterControls({ hasLifeList, }: PersonalizedHotspotFilterControlsProps) { - const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); - const setPersonalizedFilterEnabled = useFiltersStore((state) => state.setPersonalizedFilterEnabled); - const neededSpeciesMinCount = useFiltersStore((state) => state.neededSpeciesMinCount); - const setNeededSpeciesMinCount = useFiltersStore((state) => state.setNeededSpeciesMinCount); - const neededSpeciesMinPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); - const setNeededSpeciesMinPercent = useFiltersStore((state) => state.setNeededSpeciesMinPercent); - - const [countText, setCountText] = useState(String(neededSpeciesMinCount)); - const [percentText, setPercentText] = useState(String(neededSpeciesMinPercent)); - - useEffect(() => { - setCountText(String(neededSpeciesMinCount)); - }, [neededSpeciesMinCount]); + const enabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const setEnabled = useFiltersStore((state) => state.setPersonalizedFilterEnabled); + const minCount = useFiltersStore((state) => state.neededSpeciesMinCount); + const setMinCount = useFiltersStore((state) => state.setNeededSpeciesMinCount); + const minPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); + const setMinPercent = useFiltersStore((state) => state.setNeededSpeciesMinPercent); - useEffect(() => { - setPercentText(String(neededSpeciesMinPercent)); - }, [neededSpeciesMinPercent]); - - const commitCount = () => { - const parsedValue = Number.parseInt(countText.replace(/[^\d]/g, ""), 10); - const nextValue = Number.isFinite(parsedValue) ? parsedValue : neededSpeciesMinCount; - setNeededSpeciesMinCount(nextValue); - setCountText(String(nextValue)); - }; - - const commitPercent = () => { - const parsedValue = Number.parseFloat(percentText.replace(/[^0-9.]/g, "")); - const nextValue = Number.isFinite(parsedValue) ? parsedValue : neededSpeciesMinPercent; - setNeededSpeciesMinPercent(nextValue); - setPercentText(String(nextValue)); + // Snap to clean tens as the user steps, so values stay 0/10/20/… even if a + // legacy stored value (e.g. 12) was off the grid. + const stepPercent = (delta: number) => { + const base = Math.round(minPercent / PERCENT_STEP) * PERCENT_STEP; + setMinPercent(Math.min(100, Math.max(MIN_PERCENT, base + delta))); }; return ( - Personalized hotspot filter - - Show only hotspots with at least X needed species above Y%. - + Target-Rich Hotspots - + {!hasLifeList ? ( Import a life list to enable this filter. - ) : personalizedFilterEnabled ? ( - - setCountText(text.replace(/[^\d]/g, ""))} - onChangeValue={commitCount} - /> - setPercentText(text.replace(/[^0-9.]/g, ""))} - onChangeValue={commitPercent} - /> + ) : enabled ? ( + + + Minimum targets + + setMinCount(minCount - 1)} disabled={minCount <= 1} /> + {minCount} + setMinCount(minCount + 1)} /> + + + + + Minimum frequency + + stepPercent(-PERCENT_STEP)} disabled={minPercent <= MIN_PERCENT} /> + {minPercent}% + stepPercent(PERCENT_STEP)} disabled={minPercent >= 100} /> + + ) : null} diff --git a/stores/filtersStore.ts b/stores/filtersStore.ts index ac1913c..bb15232 100644 --- a/stores/filtersStore.ts +++ b/stores/filtersStore.ts @@ -26,8 +26,8 @@ export const useFiltersStore = create()( (set) => ({ showSavedOnly: false, personalizedFilterEnabled: false, - neededSpeciesMinCount: 1, - neededSpeciesMinPercent: 1, + neededSpeciesMinCount: 5, + neededSpeciesMinPercent: 50, setShowSavedOnly: (value) => set({ showSavedOnly: value }), setPersonalizedFilterEnabled: (value) => set({ personalizedFilterEnabled: value }), setNeededSpeciesMinCount: (value) => set({ neededSpeciesMinCount: normalizeNeededSpeciesMinCount(value) }), @@ -37,8 +37,8 @@ export const useFiltersStore = create()( set({ showSavedOnly: false, personalizedFilterEnabled: false, - neededSpeciesMinCount: 1, - neededSpeciesMinPercent: 1, + neededSpeciesMinCount: 5, + neededSpeciesMinPercent: 50, }), }), { From 62b18b456b23a1e719e2f14ff55024cc450b461e Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 11 Jun 2026 17:44:17 -0700 Subject: [PATCH 05/23] Rename code references to new feature --- components/HotspotList.tsx | 22 ++--- components/Mapbox.tsx | 68 +++++++-------- ...rols.tsx => TargetRichHotspotControls.tsx} | 18 ++-- hooks/useActiveFilterCount.ts | 6 +- ...spotFilter.ts => useTargetRichHotspots.ts} | 58 ++++++------- ...HotspotFilter.ts => targetRichHotspots.ts} | 82 +++++++++---------- stores/filtersStore.ts | 38 ++++----- 7 files changed, 146 insertions(+), 146 deletions(-) rename components/{PersonalizedHotspotFilterControls.tsx => TargetRichHotspotControls.tsx} (82%) rename hooks/{usePersonalizedHotspotFilter.ts => useTargetRichHotspots.ts} (80%) rename lib/{personalizedHotspotFilter.ts => targetRichHotspots.ts} (71%) diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index 106cbe1..60924b7 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -1,5 +1,5 @@ import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; -import { usePersonalizedHotspotFilter } from "@/hooks/usePersonalizedHotspotFilter"; +import { useTargetRichHotspots } from "@/hooks/useTargetRichHotspots"; import { useLocation } from "@/hooks/useLocation"; import { useScrollRestore } from "@/hooks/useScrollRestore"; import { getAllHotspots, getNearbyHotspots, searchHotspots } from "@/lib/database"; @@ -19,7 +19,7 @@ import BaseBottomSheet from "./BaseBottomSheet"; import HotspotItem from "./HotspotItem"; import IconButton from "./IconButton"; import IconButtonGroup from "./IconButtonGroup"; -import PersonalizedHotspotFilterControls from "./PersonalizedHotspotFilterControls"; +import TargetRichHotspotControls from "./TargetRichHotspotControls"; import SearchInput from "./SearchInput"; type HotspotListProps = { @@ -117,19 +117,19 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo }, [debouncedQuery, searchResults, allHotspots, hasLocationAccess, location]); const isBaseResultsFetching = debouncedQuery.length >= 2 ? isFetchingSearchResults : isFetchingAllHotspots; - const personalizedFilter = usePersonalizedHotspotFilter(displayedHotspots.map((hotspot) => hotspot.id), { + const targetRichFilter = useTargetRichHotspots(displayedHotspots.map((hotspot) => hotspot.id), { enabled: !isBaseResultsFetching, blockWhileDisabled: true, }); - const isPersonalizedLoading = personalizedFilter.isActive && (isBaseResultsFetching || personalizedFilter.isLoading); - const displayedHotspotsSet = useMemo(() => new Set(personalizedFilter.filteredIds), [personalizedFilter.filteredIds]); + const isTargetRichLoading = targetRichFilter.isActive && (isBaseResultsFetching || targetRichFilter.isLoading); + const displayedHotspotsSet = useMemo(() => new Set(targetRichFilter.filteredIds), [targetRichFilter.filteredIds]); const filteredDisplayedHotspots = useMemo(() => { - if (!personalizedFilter.isActive) { + if (!targetRichFilter.isActive) { return displayedHotspots; } return displayedHotspots.filter((hotspot) => displayedHotspotsSet.has(hotspot.id)); - }, [displayedHotspots, displayedHotspotsSet, personalizedFilter.isActive]); + }, [displayedHotspots, displayedHotspotsSet, targetRichFilter.isActive]); const { listRef, onScroll } = useScrollRestore(isOpen, searchUpdatedAt); @@ -148,7 +148,7 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo const listEmptyComponent = ( - {isPersonalizedLoading ? ( + {isTargetRichLoading ? ( <> Filtering hotspots... @@ -199,7 +199,7 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo Show saved only - + )} @@ -221,12 +221,12 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo > ( const showAttribution = useMapStore((state) => state.isMapAttributionOpen); const setShowAttribution = useMapStore((state) => state.setIsMapAttributionOpen); const { status: permissionStatus } = useLocationPermissionStore(); - const { showSavedOnly, personalizedFilterEnabled } = useFiltersStore(); + const { showSavedOnly, targetRichEnabled } = useFiltersStore(); const lifelist = useSettingsStore((state) => state.lifelist); const mapRef = useRef(null); @@ -171,13 +171,13 @@ const MapboxMap = forwardRef( const centeredToUserRef = useRef(false); const userCoordRef = useRef<[number, number] | null>(null); const isTouchActiveRef = useRef(false); - const lastPersonalizedMapDebugRef = useRef(null); + const lastTargetRichMapDebugRef = useRef(null); const lastResolvedHotspotsRef = useRef<{ id: string; lat: number; lng: number; species: number }[]>([]); const [isMapReady, setIsMapReady] = useState(false); const [bounds, setBounds] = useState(null); const hasLifeList = (lifelist?.length ?? 0) > 0; - const isPersonalizedMapFiltering = personalizedFilterEnabled && hasLifeList; + const isTargetRichMapFiltering = targetRichEnabled && hasLifeList; const mapStyle = useMemo(() => { return currentLayer === "satellite" @@ -273,7 +273,7 @@ const MapboxMap = forwardRef( throttledSetBounds.cancel(); debouncedSetSettledBounds.cancel(); setBounds((currentBounds) => (areBoundsEquivalent(currentBounds, nextBounds) ? currentBounds : nextBounds)); - } else if (isPersonalizedMapFiltering) { + } else if (isTargetRichMapFiltering) { debouncedSetSettledBounds(nextBounds); } else { throttledSetBounds(nextBounds); @@ -283,7 +283,7 @@ const MapboxMap = forwardRef( }, [ debouncedSaveLocation, debouncedSetSettledBounds, - isPersonalizedMapFiltering, + isTargetRichMapFiltering, readBoundsIfZoomed, throttledSetBounds, throttledSetMapCenter, @@ -300,7 +300,7 @@ const MapboxMap = forwardRef( throttledSetBounds.cancel(); debouncedSetSettledBounds.cancel(); void syncViewport(true); - }, [debouncedSetSettledBounds, isPersonalizedMapFiltering, syncViewport, throttledSetBounds]); + }, [debouncedSetSettledBounds, isTargetRichMapFiltering, syncViewport, throttledSetBounds]); const setTouchActive = useCallback( (isActive: boolean) => { @@ -355,29 +355,29 @@ const MapboxMap = forwardRef( () => hotspots.filter((hotspot) => !showSavedOnly || savedHotspotsSet.has(hotspot.id)), [hotspots, savedHotspotsSet, showSavedOnly] ); - const personalizedFilter = usePersonalizedHotspotFilter(mapCandidateHotspots.map((hotspot) => hotspot.id), { + const targetRichFilter = useTargetRichHotspots(mapCandidateHotspots.map((hotspot) => hotspot.id), { enabled: bounds !== null && !isFetchingHotspots, blockWhileDisabled: true, }); - const personalizedHotspotIds = useMemo(() => new Set(personalizedFilter.filteredIds), [personalizedFilter.filteredIds]); - const unresolvedCandidateCount = personalizedFilter.isActive - ? mapCandidateHotspots.reduce((count, hotspot) => count + (personalizedHotspotCache.has(hotspot.id) ? 0 : 1), 0) + const targetRichHotspotIds = useMemo(() => new Set(targetRichFilter.filteredIds), [targetRichFilter.filteredIds]); + const unresolvedCandidateCount = targetRichFilter.isActive + ? mapCandidateHotspots.reduce((count, hotspot) => count + (targetRichHotspotCache.has(hotspot.id) ? 0 : 1), 0) : 0; - const isInitialPersonalizedFetch = - personalizedFilter.isActive && + const isInitialTargetRichFetch = + targetRichFilter.isActive && bounds !== null && isFetchingHotspots && lastResolvedHotspotsRef.current.length === 0; - const isPersonalizedLoading = - personalizedFilter.isActive && - (isInitialPersonalizedFetch || unresolvedCandidateCount > 0 || personalizedFilter.isLoading); + const isTargetRichLoading = + targetRichFilter.isActive && + (isInitialTargetRichFetch || unresolvedCandidateCount > 0 || targetRichFilter.isLoading); const displayedHotspots = useMemo(() => { - if (showSavedOnly || personalizedFilter.isActive) { + if (showSavedOnly || targetRichFilter.isActive) { const resolvedHotspots = mapCandidateHotspots.filter((hotspot) => - personalizedFilter.isActive ? personalizedHotspotIds.has(hotspot.id) : true + targetRichFilter.isActive ? targetRichHotspotIds.has(hotspot.id) : true ); - if (isPersonalizedLoading) { + if (isTargetRichLoading) { return lastResolvedHotspotsRef.current; } @@ -387,21 +387,21 @@ const MapboxMap = forwardRef( return hotspots; }, [ hotspots, - isPersonalizedLoading, + isTargetRichLoading, mapCandidateHotspots, - personalizedFilter.isActive, - personalizedHotspotIds, + targetRichFilter.isActive, + targetRichHotspotIds, showSavedOnly, ]); useEffect(() => { - if (!isPersonalizedLoading) { + if (!isTargetRichLoading) { lastResolvedHotspotsRef.current = displayedHotspots; } - }, [displayedHotspots, isPersonalizedLoading]); + }, [displayedHotspots, isTargetRichLoading]); useEffect(() => { - if (!personalizedFilter.isActive) { + if (!targetRichFilter.isActive) { return; } @@ -412,31 +412,31 @@ const MapboxMap = forwardRef( candidateCount: mapCandidateHotspots.length, unresolvedCandidateCount, displayedCount: displayedHotspots.length, - isPersonalizedLoading, + isTargetRichLoading, }); - if (lastPersonalizedMapDebugRef.current === debugState) { + if (lastTargetRichMapDebugRef.current === debugState) { return; } - lastPersonalizedMapDebugRef.current = debugState; - logPersonalizedHotspotFilterDebug("map personalized filter state", { + lastTargetRichMapDebugRef.current = debugState; + logTargetRichHotspotDebug("map target-rich filter state", { hasBounds: bounds !== null, isFetchingHotspots, hotspotCount: hotspots.length, candidateCount: mapCandidateHotspots.length, unresolvedCandidateCount, displayedCount: displayedHotspots.length, - isPersonalizedLoading, + isTargetRichLoading, }); }, [ displayedHotspots.length, bounds, hotspots.length, isFetchingHotspots, - isPersonalizedLoading, + isTargetRichLoading, mapCandidateHotspots.length, - personalizedFilter.isActive, + targetRichFilter.isActive, unresolvedCandidateCount, ]); @@ -741,7 +741,7 @@ const MapboxMap = forwardRef( )} - {isPersonalizedLoading && !isZoomedTooFarOut && ( + {isTargetRichLoading && !isZoomedTooFarOut && ( {Platform.OS === "ios" && isLiquidGlassAvailable() ? ( diff --git a/components/PersonalizedHotspotFilterControls.tsx b/components/TargetRichHotspotControls.tsx similarity index 82% rename from components/PersonalizedHotspotFilterControls.tsx rename to components/TargetRichHotspotControls.tsx index b39dd42..f1550f7 100644 --- a/components/PersonalizedHotspotFilterControls.tsx +++ b/components/TargetRichHotspotControls.tsx @@ -4,7 +4,7 @@ import { Ionicons } from "@expo/vector-icons"; import React from "react"; import { Pressable, Switch, Text, View } from "react-native"; -type PersonalizedHotspotFilterControlsProps = { +type TargetRichHotspotControlsProps = { hasLifeList: boolean; }; @@ -36,15 +36,15 @@ function StepperButton({ ); } -export default function PersonalizedHotspotFilterControls({ +export default function TargetRichHotspotControls({ hasLifeList, -}: PersonalizedHotspotFilterControlsProps) { - const enabled = useFiltersStore((state) => state.personalizedFilterEnabled); - const setEnabled = useFiltersStore((state) => state.setPersonalizedFilterEnabled); - const minCount = useFiltersStore((state) => state.neededSpeciesMinCount); - const setMinCount = useFiltersStore((state) => state.setNeededSpeciesMinCount); - const minPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); - const setMinPercent = useFiltersStore((state) => state.setNeededSpeciesMinPercent); +}: TargetRichHotspotControlsProps) { + const enabled = useFiltersStore((state) => state.targetRichEnabled); + const setEnabled = useFiltersStore((state) => state.setTargetRichEnabled); + const minCount = useFiltersStore((state) => state.minTargets); + const setMinCount = useFiltersStore((state) => state.setMinTargets); + const minPercent = useFiltersStore((state) => state.minTargetFrequency); + const setMinPercent = useFiltersStore((state) => state.setMinTargetFrequency); // Snap to clean tens as the user steps, so values stay 0/10/20/… even if a // legacy stored value (e.g. 12) was off the grid. diff --git a/hooks/useActiveFilterCount.ts b/hooks/useActiveFilterCount.ts index b44986b..6920656 100644 --- a/hooks/useActiveFilterCount.ts +++ b/hooks/useActiveFilterCount.ts @@ -4,13 +4,13 @@ import { useSettingsStore } from "@/stores/settingsStore"; /** * Number of hotspot filters currently active. Drives the badge on both the * Nearby Hotspots map button and the filter toggle inside the hotspot list. - * The personalized filter only counts when a life list is actually imported. + * The target-rich filter only counts when a life list is actually imported. */ export function useActiveFilterCount() { const showSavedOnly = useFiltersStore((state) => state.showSavedOnly); - const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); + const targetRichEnabled = useFiltersStore((state) => state.targetRichEnabled); const lifelist = useSettingsStore((state) => state.lifelist); const hasLifeList = (lifelist?.length ?? 0) > 0; - return [showSavedOnly, personalizedFilterEnabled && hasLifeList].filter(Boolean).length; + return [showSavedOnly, targetRichEnabled && hasLifeList].filter(Boolean).length; } diff --git a/hooks/usePersonalizedHotspotFilter.ts b/hooks/useTargetRichHotspots.ts similarity index 80% rename from hooks/usePersonalizedHotspotFilter.ts rename to hooks/useTargetRichHotspots.ts index eb841ed..86cb990 100644 --- a/hooks/usePersonalizedHotspotFilter.ts +++ b/hooks/useTargetRichHotspots.ts @@ -1,32 +1,32 @@ import { - createPersonalizedHotspotFilterBasis, - logPersonalizedHotspotFilterDebug, - personalizedHotspotCache, - syncPersonalizedHotspotCacheBasis, -} from "@/lib/personalizedHotspotFilter"; + createTargetRichHotspotBasis, + logTargetRichHotspotDebug, + targetRichHotspotCache, + syncTargetRichHotspotCacheBasis, +} from "@/lib/targetRichHotspots"; import { useFiltersStore } from "@/stores/filtersStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useEffect, useMemo, useRef, useState } from "react"; -type UsePersonalizedHotspotFilterOptions = { +type UseTargetRichHotspotsOptions = { enabled?: boolean; blockWhileDisabled?: boolean; }; -type PersonalizedHotspotFilterState = { +type TargetRichHotspotState = { filteredIds: string[]; isActive: boolean; isLoading: boolean; hasLifeList: boolean; }; -type AsyncPersonalizedHotspotFilterState = { +type AsyncTargetRichHotspotState = { filteredIds: string[]; isLoading: boolean; }; function filterResolvedHotspotIds(hotspotIds: string[]): string[] { - return hotspotIds.filter((hotspotId) => personalizedHotspotCache.get(hotspotId) === true); + return hotspotIds.filter((hotspotId) => targetRichHotspotCache.get(hotspotId) === true); } function areStringArraysEqual(left: string[], right: string[]): boolean { @@ -37,36 +37,36 @@ function areStringArraysEqual(left: string[], right: string[]): boolean { return left.every((value, index) => value === right[index]); } -export function usePersonalizedHotspotFilter( +export function useTargetRichHotspots( hotspotIds: string[], - options: UsePersonalizedHotspotFilterOptions = {} -): PersonalizedHotspotFilterState { - const personalizedFilterEnabled = useFiltersStore((state) => state.personalizedFilterEnabled); - const neededSpeciesMinCount = useFiltersStore((state) => state.neededSpeciesMinCount); - const neededSpeciesMinPercent = useFiltersStore((state) => state.neededSpeciesMinPercent); + options: UseTargetRichHotspotsOptions = {} +): TargetRichHotspotState { + const targetRichEnabled = useFiltersStore((state) => state.targetRichEnabled); + const minTargets = useFiltersStore((state) => state.minTargets); + const minTargetFrequency = useFiltersStore((state) => state.minTargetFrequency); const lifelist = useSettingsStore((state) => state.lifelist); const lifelistExclusions = useSettingsStore((state) => state.lifelistExclusions); const targetMonths = useSettingsStore((state) => state.targetMonths); const basis = useMemo( () => - createPersonalizedHotspotFilterBasis({ + createTargetRichHotspotBasis({ lifelist, lifelistExclusions, targetMonths, - neededSpeciesMinCount, - neededSpeciesMinPercent, + minTargets, + minTargetFrequency, }), - [lifelist, lifelistExclusions, targetMonths, neededSpeciesMinCount, neededSpeciesMinPercent] + [lifelist, lifelistExclusions, targetMonths, minTargets, minTargetFrequency] ); const hasLifeList = basis !== null; - const isActive = personalizedFilterEnabled && hasLifeList; + const isActive = targetRichEnabled && hasLifeList; const isEnabled = options.enabled ?? true; const candidateKey = useMemo(() => hotspotIds.join("|"), [hotspotIds]); const stableHotspotIdsRef = useRef(hotspotIds); const basisRef = useRef(basis); - const asyncStateRef = useRef({ filteredIds: [], isLoading: false }); + const asyncStateRef = useRef({ filteredIds: [], isLoading: false }); const lastDebugStatusRef = useRef(null); const logDebugStatusRef = useRef<(status: string, details?: Record) => void>(() => {}); @@ -76,7 +76,7 @@ export function usePersonalizedHotspotFilter( const stableHotspotIds = stableHotspotIdsRef.current; - const [asyncState, setAsyncState] = useState({ + const [asyncState, setAsyncState] = useState({ filteredIds: [], isLoading: false, }); @@ -106,7 +106,7 @@ export function usePersonalizedHotspotFilter( } lastDebugStatusRef.current = statusKey; - logPersonalizedHotspotFilterDebug(status, { + logTargetRichHotspotDebug(status, { candidateCount: stableHotspotIds.length, filteredCount: asyncStateRef.current.filteredIds.length, isLoading: asyncStateRef.current.isLoading, @@ -118,7 +118,7 @@ export function usePersonalizedHotspotFilter( }; useEffect(() => { - syncPersonalizedHotspotCacheBasis(basis?.cacheKey ?? null); + syncTargetRichHotspotCacheBasis(basis?.cacheKey ?? null); }, [basis?.cacheKey]); useEffect(() => { @@ -145,7 +145,7 @@ export function usePersonalizedHotspotFilter( } const basisForRun = basisRef.current; - const unresolvedHotspotIds = stableHotspotIds.filter((hotspotId) => !personalizedHotspotCache.has(hotspotId)); + const unresolvedHotspotIds = stableHotspotIds.filter((hotspotId) => !targetRichHotspotCache.has(hotspotId)); if (unresolvedHotspotIds.length === 0) { const filteredIds = filterResolvedHotspotIds(stableHotspotIds); logDebugStatusRef.current("all candidates resolved from cache", { @@ -175,14 +175,14 @@ export function usePersonalizedHotspotFilter( isLoading: true, })); - void personalizedHotspotCache + void targetRichHotspotCache .evaluateMany(unresolvedHotspotIds, basisForRun, abortController.signal) .then(() => { if (abortController.signal.aborted) { return; } - const stillUnresolved = stableHotspotIds.some((hotspotId) => !personalizedHotspotCache.has(hotspotId)); + const stillUnresolved = stableHotspotIds.some((hotspotId) => !targetRichHotspotCache.has(hotspotId)); if (stillUnresolved) { logDebugStatusRef.current("evaluation completed but candidates still unresolved"); setAsyncState((currentState) => ({ @@ -215,7 +215,7 @@ export function usePersonalizedHotspotFilter( } logDebugStatusRef.current("evaluation failed"); - console.error("Failed to evaluate personalized hotspot filter", error); + console.error("Failed to evaluate target-rich hotspot filter", error); setAsyncState((currentState) => ({ filteredIds: currentState.filteredIds, isLoading: false, @@ -234,7 +234,7 @@ export function usePersonalizedHotspotFilter( stableHotspotIds, ]); - return useMemo(() => { + return useMemo(() => { if (!isActive) { return { filteredIds: stableHotspotIds, diff --git a/lib/personalizedHotspotFilter.ts b/lib/targetRichHotspots.ts similarity index 71% rename from lib/personalizedHotspotFilter.ts rename to lib/targetRichHotspots.ts index d397b7f..88799ca 100644 --- a/lib/personalizedHotspotFilter.ts +++ b/lib/targetRichHotspots.ts @@ -4,49 +4,49 @@ import { getMonthIndices, getTotalSamplesForMonths, parseHotspotTargetData } fro const CACHE_CAPACITY = 5_000; const COMPUTE_BATCH_SIZE = 50; -const DEBUG_PERSONALIZED_FILTER = __DEV__; +const DEBUG_TARGET_RICH_FILTER = __DEV__; let evaluationRunCounter = 0; -export type PersonalizedHotspotFilterBasis = { +export type TargetRichHotspotBasis = { cacheKey: string; lifeListCodes: ReadonlySet; excludedCodes: ReadonlySet; selectedMonths: number[]; - neededSpeciesMinCount: number; - neededSpeciesMinPercent: number; + minTargets: number; + minTargetFrequency: number; }; -export function logPersonalizedHotspotFilterDebug(message: string, details?: Record) { - if (!DEBUG_PERSONALIZED_FILTER) { +export function logTargetRichHotspotDebug(message: string, details?: Record) { + if (!DEBUG_TARGET_RICH_FILTER) { return; } if (details) { - console.log(`[personalized-hotspot-filter] ${message}`, details); + console.log(`[target-rich-hotspot] ${message}`, details); return; } - console.log(`[personalized-hotspot-filter] ${message}`); + console.log(`[target-rich-hotspot] ${message}`); } -export function normalizeNeededSpeciesMinCount(value: number): number { +export function normalizeMinTargets(value: number): number { if (!Number.isFinite(value)) return 1; return Math.max(1, Math.floor(value)); } -export function normalizeNeededSpeciesMinPercent(value: number): number { +export function normalizeMinTargetFrequency(value: number): number { if (!Number.isFinite(value)) return 1; return Math.min(100, Math.max(0, value)); } -export function createPersonalizedHotspotFilterBasis(params: { +export function createTargetRichHotspotBasis(params: { lifelist: LifeListEntry[] | null; lifelistExclusions: string[] | null; targetMonths: number[]; - neededSpeciesMinCount: number; - neededSpeciesMinPercent: number; -}): PersonalizedHotspotFilterBasis | null { + minTargets: number; + minTargetFrequency: number; +}): TargetRichHotspotBasis | null { const lifeListCodes = [...new Set((params.lifelist ?? []).map((entry) => entry.code))].sort(); if (lifeListCodes.length === 0) { @@ -55,27 +55,27 @@ export function createPersonalizedHotspotFilterBasis(params: { const excludedCodes = [...new Set(params.lifelistExclusions ?? [])].sort(); const selectedMonths = [...new Set(params.targetMonths)].sort((a, b) => a - b); - const neededSpeciesMinCount = normalizeNeededSpeciesMinCount(params.neededSpeciesMinCount); - const neededSpeciesMinPercent = normalizeNeededSpeciesMinPercent(params.neededSpeciesMinPercent); + const minTargets = normalizeMinTargets(params.minTargets); + const minTargetFrequency = normalizeMinTargetFrequency(params.minTargetFrequency); return { cacheKey: JSON.stringify({ lifeListCodes, excludedCodes, selectedMonths, - neededSpeciesMinCount, - neededSpeciesMinPercent, + minTargets, + minTargetFrequency, }), lifeListCodes: new Set(lifeListCodes), excludedCodes: new Set(excludedCodes), selectedMonths, - neededSpeciesMinCount, - neededSpeciesMinPercent, + minTargets, + minTargetFrequency, }; } function createAbortError(): Error { - const error = new Error("Personalized hotspot evaluation aborted"); + const error = new Error("Target-rich hotspot evaluation aborted"); error.name = "AbortError"; return error; } @@ -86,7 +86,7 @@ function throwIfAborted(signal?: AbortSignal) { } } -function matchesPersonalizedHotspotFilter(rawData: string, basis: PersonalizedHotspotFilterBasis): boolean { +function matchesTargetRichHotspot(rawData: string, basis: TargetRichHotspotBasis): boolean { const parsed = parseHotspotTargetData(rawData); const monthIndices = getMonthIndices(parsed, basis.selectedMonths); const totalSamples = getTotalSamplesForMonths(parsed, monthIndices); @@ -114,9 +114,9 @@ function matchesPersonalizedHotspotFilter(rawData: string, basis: PersonalizedHo } const percentage = (observations / totalSamples) * 100; - if (percentage >= basis.neededSpeciesMinPercent) { + if (percentage >= basis.minTargetFrequency) { qualifyingSpeciesCount += 1; - if (qualifyingSpeciesCount >= basis.neededSpeciesMinCount) { + if (qualifyingSpeciesCount >= basis.minTargets) { return true; } } @@ -125,12 +125,12 @@ function matchesPersonalizedHotspotFilter(rawData: string, basis: PersonalizedHo return false; } -class PersonalizedHotspotCache { +class TargetRichHotspotCache { private cache = new Map(); private activeSignals = new Set(); clear() { - logPersonalizedHotspotFilterDebug("cache clear", { previousSize: this.cache.size }); + logTargetRichHotspotDebug("cache clear", { previousSize: this.cache.size }); this.cache.clear(); } @@ -151,7 +151,7 @@ class PersonalizedHotspotCache { cancelActiveRun() { if (this.activeSignals.size > 0) { - logPersonalizedHotspotFilterDebug("cancel active runs", { activeRunCount: this.activeSignals.size }); + logTargetRichHotspotDebug("cancel active runs", { activeRunCount: this.activeSignals.size }); } for (const controller of this.activeSignals) { @@ -162,7 +162,7 @@ class PersonalizedHotspotCache { async evaluateMany( hotspotIds: string[], - basis: PersonalizedHotspotFilterBasis, + basis: TargetRichHotspotBasis, signal?: AbortSignal ): Promise { const missingHotspotIds = [...new Set(hotspotIds)].filter((hotspotId) => !this.cache.has(hotspotId)); @@ -178,22 +178,22 @@ class PersonalizedHotspotCache { const isAborted = () => combinedSignal.aborted || signal?.aborted; try { - logPersonalizedHotspotFilterDebug("evaluate start", { + logTargetRichHotspotDebug("evaluate start", { runId, requestedCount: hotspotIds.length, missingCount: missingHotspotIds.length, cachedCount: hotspotIds.length - missingHotspotIds.length, cacheSize: this.cache.size, monthCount: basis.selectedMonths.length === 0 ? 12 : basis.selectedMonths.length, - neededSpeciesMinCount: basis.neededSpeciesMinCount, - neededSpeciesMinPercent: basis.neededSpeciesMinPercent, + minTargets: basis.minTargets, + minTargetFrequency: basis.minTargetFrequency, }); throwIfAborted(signal); const targetData = await getTargetDataForHotspots(missingHotspotIds); throwIfAborted(signal); - logPersonalizedHotspotFilterDebug("target data fetched", { + logTargetRichHotspotDebug("target data fetched", { runId, requestedCount: missingHotspotIds.length, foundCount: targetData.size, @@ -212,7 +212,7 @@ class PersonalizedHotspotCache { } const rawData = targetData.get(hotspotId); - this.set(hotspotId, rawData ? matchesPersonalizedHotspotFilter(rawData, basis) : false); + this.set(hotspotId, rawData ? matchesTargetRichHotspot(rawData, basis) : false); } const processedCount = Math.min(index + batch.length, missingHotspotIds.length); @@ -220,7 +220,7 @@ class PersonalizedHotspotCache { processedCount === missingHotspotIds.length || (missingHotspotIds.length > 250 && processedCount % 250 === 0) ) { - logPersonalizedHotspotFilterDebug("evaluate progress", { + logTargetRichHotspotDebug("evaluate progress", { runId, processedCount, totalCount: missingHotspotIds.length, @@ -232,14 +232,14 @@ class PersonalizedHotspotCache { await new Promise((resolve) => setTimeout(resolve, 0)); } } - logPersonalizedHotspotFilterDebug("evaluate complete", { + logTargetRichHotspotDebug("evaluate complete", { runId, computedCount: missingHotspotIds.length, cacheSize: this.cache.size, }); } catch (error) { if ((error as Error | undefined)?.name === "AbortError") { - logPersonalizedHotspotFilterDebug("evaluate aborted", { + logTargetRichHotspotDebug("evaluate aborted", { runId, processedCandidateCount: missingHotspotIds.length, cacheSize: this.cache.size, @@ -268,20 +268,20 @@ class PersonalizedHotspotCache { } } -export const personalizedHotspotCache = new PersonalizedHotspotCache(); +export const targetRichHotspotCache = new TargetRichHotspotCache(); let activeBasisKey: string | null = null; -export function syncPersonalizedHotspotCacheBasis(nextBasisKey: string | null) { +export function syncTargetRichHotspotCacheBasis(nextBasisKey: string | null) { if (activeBasisKey === nextBasisKey) { return; } - logPersonalizedHotspotFilterDebug("basis changed", { + logTargetRichHotspotDebug("basis changed", { hadBasis: activeBasisKey !== null, hasBasis: nextBasisKey !== null, }); activeBasisKey = nextBasisKey; - personalizedHotspotCache.cancelActiveRun(); - personalizedHotspotCache.clear(); + targetRichHotspotCache.cancelActiveRun(); + targetRichHotspotCache.clear(); } diff --git a/stores/filtersStore.ts b/stores/filtersStore.ts index bb15232..48a5892 100644 --- a/stores/filtersStore.ts +++ b/stores/filtersStore.ts @@ -2,22 +2,22 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { - normalizeNeededSpeciesMinCount, - normalizeNeededSpeciesMinPercent, -} from "@/lib/personalizedHotspotFilter"; + normalizeMinTargets, + normalizeMinTargetFrequency, +} from "@/lib/targetRichHotspots"; type FiltersState = { showSavedOnly: boolean; - personalizedFilterEnabled: boolean; - neededSpeciesMinCount: number; - neededSpeciesMinPercent: number; + targetRichEnabled: boolean; + minTargets: number; + minTargetFrequency: number; }; type FiltersActions = { setShowSavedOnly: (value: boolean) => void; - setPersonalizedFilterEnabled: (value: boolean) => void; - setNeededSpeciesMinCount: (value: number) => void; - setNeededSpeciesMinPercent: (value: number) => void; + setTargetRichEnabled: (value: boolean) => void; + setMinTargets: (value: number) => void; + setMinTargetFrequency: (value: number) => void; resetFilters: () => void; }; @@ -25,20 +25,20 @@ export const useFiltersStore = create()( persist( (set) => ({ showSavedOnly: false, - personalizedFilterEnabled: false, - neededSpeciesMinCount: 5, - neededSpeciesMinPercent: 50, + targetRichEnabled: false, + minTargets: 5, + minTargetFrequency: 50, setShowSavedOnly: (value) => set({ showSavedOnly: value }), - setPersonalizedFilterEnabled: (value) => set({ personalizedFilterEnabled: value }), - setNeededSpeciesMinCount: (value) => set({ neededSpeciesMinCount: normalizeNeededSpeciesMinCount(value) }), - setNeededSpeciesMinPercent: (value) => - set({ neededSpeciesMinPercent: normalizeNeededSpeciesMinPercent(value) }), + setTargetRichEnabled: (value) => set({ targetRichEnabled: value }), + setMinTargets: (value) => set({ minTargets: normalizeMinTargets(value) }), + setMinTargetFrequency: (value) => + set({ minTargetFrequency: normalizeMinTargetFrequency(value) }), resetFilters: () => set({ showSavedOnly: false, - personalizedFilterEnabled: false, - neededSpeciesMinCount: 5, - neededSpeciesMinPercent: 50, + targetRichEnabled: false, + minTargets: 5, + minTargetFrequency: 50, }), }), { From 49e4ed671d3edbf34cae4c27c73d515411a91089 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Thu, 11 Jun 2026 17:56:15 -0700 Subject: [PATCH 06/23] Remove logging and fix state change bug --- components/Mapbox.tsx | 43 +----------------- hooks/useTargetRichHotspots.ts | 79 +--------------------------------- lib/targetRichHotspots.ts | 71 ------------------------------ 3 files changed, 3 insertions(+), 190 deletions(-) diff --git a/components/Mapbox.tsx b/components/Mapbox.tsx index 4215922..4292054 100644 --- a/components/Mapbox.tsx +++ b/components/Mapbox.tsx @@ -7,7 +7,7 @@ import { savedHotspotSymbolStyle, savedPlaceSymbolStyle, } from "@/lib/layers"; -import { logTargetRichHotspotDebug, targetRichHotspotCache } from "@/lib/targetRichHotspots"; +import { targetRichHotspotCache } from "@/lib/targetRichHotspots"; import tw from "@/lib/tw"; import { OnPressEvent } from "@/lib/types"; import { findClosestFeature, getMarkerColorIndex, padBoundsBySize } from "@/lib/utils"; @@ -171,7 +171,6 @@ const MapboxMap = forwardRef( const centeredToUserRef = useRef(false); const userCoordRef = useRef<[number, number] | null>(null); const isTouchActiveRef = useRef(false); - const lastTargetRichMapDebugRef = useRef(null); const lastResolvedHotspotsRef = useRef<{ id: string; lat: number; lng: number; species: number }[]>([]); const [isMapReady, setIsMapReady] = useState(false); @@ -400,46 +399,6 @@ const MapboxMap = forwardRef( } }, [displayedHotspots, isTargetRichLoading]); - useEffect(() => { - if (!targetRichFilter.isActive) { - return; - } - - const debugState = JSON.stringify({ - hasBounds: bounds !== null, - isFetchingHotspots, - hotspotCount: hotspots.length, - candidateCount: mapCandidateHotspots.length, - unresolvedCandidateCount, - displayedCount: displayedHotspots.length, - isTargetRichLoading, - }); - - if (lastTargetRichMapDebugRef.current === debugState) { - return; - } - - lastTargetRichMapDebugRef.current = debugState; - logTargetRichHotspotDebug("map target-rich filter state", { - hasBounds: bounds !== null, - isFetchingHotspots, - hotspotCount: hotspots.length, - candidateCount: mapCandidateHotspots.length, - unresolvedCandidateCount, - displayedCount: displayedHotspots.length, - isTargetRichLoading, - }); - }, [ - displayedHotspots.length, - bounds, - hotspots.length, - isFetchingHotspots, - isTargetRichLoading, - mapCandidateHotspots.length, - targetRichFilter.isActive, - unresolvedCandidateCount, - ]); - const handleFeaturePress = useCallback( (event: any) => { const pressEvent = event as OnPressEvent; diff --git a/hooks/useTargetRichHotspots.ts b/hooks/useTargetRichHotspots.ts index 86cb990..32b3ceb 100644 --- a/hooks/useTargetRichHotspots.ts +++ b/hooks/useTargetRichHotspots.ts @@ -1,6 +1,5 @@ import { createTargetRichHotspotBasis, - logTargetRichHotspotDebug, targetRichHotspotCache, syncTargetRichHotspotCacheBasis, } from "@/lib/targetRichHotspots"; @@ -66,9 +65,6 @@ export function useTargetRichHotspots( const candidateKey = useMemo(() => hotspotIds.join("|"), [hotspotIds]); const stableHotspotIdsRef = useRef(hotspotIds); const basisRef = useRef(basis); - const asyncStateRef = useRef({ filteredIds: [], isLoading: false }); - const lastDebugStatusRef = useRef(null); - const logDebugStatusRef = useRef<(status: string, details?: Record) => void>(() => {}); if (stableHotspotIdsRef.current !== hotspotIds && stableHotspotIdsRef.current.join("|") !== candidateKey) { stableHotspotIdsRef.current = hotspotIds; @@ -85,62 +81,12 @@ export function useTargetRichHotspots( basisRef.current = basis; }, [basis]); - useEffect(() => { - asyncStateRef.current = asyncState; - }, [asyncState]); - - logDebugStatusRef.current = (status: string, details?: Record) => { - const statusKey = JSON.stringify({ - status, - candidateCount: stableHotspotIds.length, - filteredCount: asyncStateRef.current.filteredIds.length, - isLoading: asyncStateRef.current.isLoading, - isEnabled, - isActive, - hasLifeList, - ...details, - }); - - if (lastDebugStatusRef.current === statusKey) { - return; - } - - lastDebugStatusRef.current = statusKey; - logTargetRichHotspotDebug(status, { - candidateCount: stableHotspotIds.length, - filteredCount: asyncStateRef.current.filteredIds.length, - isLoading: asyncStateRef.current.isLoading, - isEnabled, - isActive, - hasLifeList, - ...details, - }); - }; - useEffect(() => { syncTargetRichHotspotCacheBasis(basis?.cacheKey ?? null); }, [basis?.cacheKey]); useEffect(() => { - if (!isActive) { - logDebugStatusRef.current("hook inactive"); - return; - } - - if (!isEnabled) { - logDebugStatusRef.current("waiting for prerequisites", { - blockWhileDisabled: options.blockWhileDisabled ?? false, - }); - return; - } - - if (stableHotspotIds.length === 0) { - logDebugStatusRef.current("no candidate hotspots"); - return; - } - - if (!basisRef.current) { - logDebugStatusRef.current("missing filter basis"); + if (!isActive || !isEnabled || stableHotspotIds.length === 0 || !basisRef.current) { return; } @@ -148,9 +94,6 @@ export function useTargetRichHotspots( const unresolvedHotspotIds = stableHotspotIds.filter((hotspotId) => !targetRichHotspotCache.has(hotspotId)); if (unresolvedHotspotIds.length === 0) { const filteredIds = filterResolvedHotspotIds(stableHotspotIds); - logDebugStatusRef.current("all candidates resolved from cache", { - matchedCount: filteredIds.length, - }); setAsyncState((currentState) => { if (!currentState.isLoading && areStringArraysEqual(currentState.filteredIds, filteredIds)) { return currentState; @@ -165,10 +108,6 @@ export function useTargetRichHotspots( } const abortController = new AbortController(); - logDebugStatusRef.current("evaluating unresolved hotspots", { - unresolvedCount: unresolvedHotspotIds.length, - cachedCount: stableHotspotIds.length - unresolvedHotspotIds.length, - }); setAsyncState((currentState) => ({ filteredIds: currentState.isLoading ? currentState.filteredIds : [], @@ -184,7 +123,6 @@ export function useTargetRichHotspots( const stillUnresolved = stableHotspotIds.some((hotspotId) => !targetRichHotspotCache.has(hotspotId)); if (stillUnresolved) { - logDebugStatusRef.current("evaluation completed but candidates still unresolved"); setAsyncState((currentState) => ({ filteredIds: currentState.filteredIds, isLoading: true, @@ -193,10 +131,6 @@ export function useTargetRichHotspots( } const filteredIds = filterResolvedHotspotIds(stableHotspotIds); - logDebugStatusRef.current("evaluation resolved", { - matchedCount: filteredIds.length, - unresolvedCount: 0, - }); setAsyncState((currentState) => { if (!currentState.isLoading && areStringArraysEqual(currentState.filteredIds, filteredIds)) { return currentState; @@ -210,11 +144,9 @@ export function useTargetRichHotspots( }) .catch((error) => { if (abortController.signal.aborted || error?.name === "AbortError") { - logDebugStatusRef.current("evaluation aborted"); return; } - logDebugStatusRef.current("evaluation failed"); console.error("Failed to evaluate target-rich hotspot filter", error); setAsyncState((currentState) => ({ filteredIds: currentState.filteredIds, @@ -225,14 +157,7 @@ export function useTargetRichHotspots( return () => { abortController.abort(); }; - }, [ - candidateKey, - hasLifeList, - isActive, - isEnabled, - options.blockWhileDisabled, - stableHotspotIds, - ]); + }, [basis?.cacheKey, candidateKey, hasLifeList, isActive, isEnabled, stableHotspotIds]); return useMemo(() => { if (!isActive) { diff --git a/lib/targetRichHotspots.ts b/lib/targetRichHotspots.ts index 88799ca..49c11cf 100644 --- a/lib/targetRichHotspots.ts +++ b/lib/targetRichHotspots.ts @@ -4,9 +4,6 @@ import { getMonthIndices, getTotalSamplesForMonths, parseHotspotTargetData } fro const CACHE_CAPACITY = 5_000; const COMPUTE_BATCH_SIZE = 50; -const DEBUG_TARGET_RICH_FILTER = __DEV__; - -let evaluationRunCounter = 0; export type TargetRichHotspotBasis = { cacheKey: string; @@ -17,19 +14,6 @@ export type TargetRichHotspotBasis = { minTargetFrequency: number; }; -export function logTargetRichHotspotDebug(message: string, details?: Record) { - if (!DEBUG_TARGET_RICH_FILTER) { - return; - } - - if (details) { - console.log(`[target-rich-hotspot] ${message}`, details); - return; - } - - console.log(`[target-rich-hotspot] ${message}`); -} - export function normalizeMinTargets(value: number): number { if (!Number.isFinite(value)) return 1; return Math.max(1, Math.floor(value)); @@ -130,7 +114,6 @@ class TargetRichHotspotCache { private activeSignals = new Set(); clear() { - logTargetRichHotspotDebug("cache clear", { previousSize: this.cache.size }); this.cache.clear(); } @@ -150,10 +133,6 @@ class TargetRichHotspotCache { } cancelActiveRun() { - if (this.activeSignals.size > 0) { - logTargetRichHotspotDebug("cancel active runs", { activeRunCount: this.activeSignals.size }); - } - for (const controller of this.activeSignals) { controller.abort(); } @@ -170,7 +149,6 @@ class TargetRichHotspotCache { return; } - const runId = ++evaluationRunCounter; const controller = new AbortController(); this.activeSignals.add(controller); @@ -178,28 +156,10 @@ class TargetRichHotspotCache { const isAborted = () => combinedSignal.aborted || signal?.aborted; try { - logTargetRichHotspotDebug("evaluate start", { - runId, - requestedCount: hotspotIds.length, - missingCount: missingHotspotIds.length, - cachedCount: hotspotIds.length - missingHotspotIds.length, - cacheSize: this.cache.size, - monthCount: basis.selectedMonths.length === 0 ? 12 : basis.selectedMonths.length, - minTargets: basis.minTargets, - minTargetFrequency: basis.minTargetFrequency, - }); - throwIfAborted(signal); const targetData = await getTargetDataForHotspots(missingHotspotIds); throwIfAborted(signal); - logTargetRichHotspotDebug("target data fetched", { - runId, - requestedCount: missingHotspotIds.length, - foundCount: targetData.size, - missingDataCount: missingHotspotIds.length - targetData.size, - }); - for (let index = 0; index < missingHotspotIds.length; index += COMPUTE_BATCH_SIZE) { if (isAborted()) { throw createAbortError(); @@ -215,37 +175,10 @@ class TargetRichHotspotCache { this.set(hotspotId, rawData ? matchesTargetRichHotspot(rawData, basis) : false); } - const processedCount = Math.min(index + batch.length, missingHotspotIds.length); - if ( - processedCount === missingHotspotIds.length || - (missingHotspotIds.length > 250 && processedCount % 250 === 0) - ) { - logTargetRichHotspotDebug("evaluate progress", { - runId, - processedCount, - totalCount: missingHotspotIds.length, - cacheSize: this.cache.size, - }); - } - if (index + COMPUTE_BATCH_SIZE < missingHotspotIds.length) { await new Promise((resolve) => setTimeout(resolve, 0)); } } - logTargetRichHotspotDebug("evaluate complete", { - runId, - computedCount: missingHotspotIds.length, - cacheSize: this.cache.size, - }); - } catch (error) { - if ((error as Error | undefined)?.name === "AbortError") { - logTargetRichHotspotDebug("evaluate aborted", { - runId, - processedCandidateCount: missingHotspotIds.length, - cacheSize: this.cache.size, - }); - } - throw error; } finally { this.activeSignals.delete(controller); } @@ -277,10 +210,6 @@ export function syncTargetRichHotspotCacheBasis(nextBasisKey: string | null) { return; } - logTargetRichHotspotDebug("basis changed", { - hadBasis: activeBasisKey !== null, - hasBasis: nextBasisKey !== null, - }); activeBasisKey = nextBasisKey; targetRichHotspotCache.cancelActiveRun(); targetRichHotspotCache.clear(); From 3fbe824827436fcddb39169e39794cfab366bf50 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 08:12:57 -0700 Subject: [PATCH 07/23] Map aware list view --- app/index.tsx | 31 +++- components/FilterSheet.tsx | 31 ++++ components/HotspotItem.tsx | 21 +-- components/HotspotList.tsx | 277 ++++++++++++++++++------------- components/MapListTogglePill.tsx | 39 +++++ components/Mapbox.tsx | 19 +++ components/PlaceEditSheet.tsx | 25 +-- components/PlaceItem.tsx | 59 +++++++ lib/database.ts | 6 +- lib/placeIconImages.ts | 33 ++++ lib/types.ts | 7 + lib/utils.ts | 27 +++ stores/mapStore.ts | 13 ++ 13 files changed, 416 insertions(+), 172 deletions(-) create mode 100644 components/FilterSheet.tsx create mode 100644 components/MapListTogglePill.tsx create mode 100644 components/PlaceItem.tsx create mode 100644 lib/placeIconImages.ts diff --git a/app/index.tsx b/app/index.tsx index b584519..40a515b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -2,7 +2,7 @@ import CountBadge from "@/components/CountBadge"; import FloatingButton from "@/components/FloatingButton"; import HotspotDialog from "@/components/HotspotDialog"; import HotspotList from "@/components/HotspotList"; -import MapListIcon from "@/components/icons/MapListIcon"; +import MapListTogglePill from "@/components/MapListTogglePill"; import Mapbox, { MapboxMapRef } from "@/components/Mapbox"; import MenuBottomSheet from "@/components/MenuBottomSheet"; import PacksNotice from "@/components/PacksNotice"; @@ -10,7 +10,6 @@ import PlaceDialog from "@/components/PlaceDialog"; import SunIndicator from "@/components/SunIndicator"; import { useInstalledPacks } from "@/hooks/useInstalledPacks"; import { usePackUpdates } from "@/hooks/usePackUpdates"; -import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { useSavedLocation } from "@/hooks/useSavedLocation"; import tw from "@/lib/tw"; import { useMapStore } from "@/stores/mapStore"; @@ -42,7 +41,6 @@ export default function HomeScreen() { } = useMapStore(); const { data: installedPacks, isLoading: isLoadingInstalledPacks } = useInstalledPacks(); const { updateCount } = usePackUpdates(); - const activeFilterCount = useActiveFilterCount(); const handleMapPress = (_event: any) => { if (isMenuOpen) handleCloseBottomSheet(); @@ -124,6 +122,18 @@ export default function HomeScreen() { [setCustomPinCoordinates, setPlaceId, setHotspotId] ); + const handleSelectPlaceFromList = useCallback( + (selectedPlaceId: string, lat: number, lng: number) => { + setCustomPinCoordinates(null); + setHotspotId(null); + setPlaceId(selectedPlaceId); + setTimeout(() => { + mapRef.current?.centerOnCoordinates(lng, lat, 200); + }, 500); + }, + [setCustomPinCoordinates, setHotspotId, setPlaceId] + ); + if (isLoadingLocation) return null; const initialCenter = savedLocation?.center ?? [-98.5, 39.5]; @@ -174,12 +184,6 @@ export default function HomeScreen() { - - - - - - @@ -187,6 +191,14 @@ export default function HomeScreen() { + {!isHotspotListOpen && ( + + + + )} diff --git a/components/FilterSheet.tsx b/components/FilterSheet.tsx new file mode 100644 index 0000000..acbc9b1 --- /dev/null +++ b/components/FilterSheet.tsx @@ -0,0 +1,31 @@ +import tw from "@/lib/tw"; +import { useFiltersStore } from "@/stores/filtersStore"; +import { useSettingsStore } from "@/stores/settingsStore"; +import React from "react"; +import { Switch, Text, View } from "react-native"; +import BaseBottomSheet from "./BaseBottomSheet"; +import TargetRichHotspotControls from "./TargetRichHotspotControls"; + +type FilterSheetProps = { + isOpen: boolean; + onClose: () => void; +}; + +export default function FilterSheet({ isOpen, onClose }: FilterSheetProps) { + const showSavedOnly = useFiltersStore((state) => state.showSavedOnly); + const setShowSavedOnly = useFiltersStore((state) => state.setShowSavedOnly); + const lifelist = useSettingsStore((state) => state.lifelist); + const hasLifeList = (lifelist?.length ?? 0) > 0; + + return ( + + + + Show saved only + + + + + + ); +} diff --git a/components/HotspotItem.tsx b/components/HotspotItem.tsx index b290cd2..ca4b09b 100644 --- a/components/HotspotItem.tsx +++ b/components/HotspotItem.tsx @@ -1,29 +1,10 @@ import tw from "@/lib/tw"; import { Hotspot } from "@/lib/types"; -import { getMarkerColor } from "@/lib/utils"; +import { formatDistance, getMarkerColor } from "@/lib/utils"; import { Ionicons } from "@expo/vector-icons"; import React, { useCallback } from "react"; import { Pressable, Text, View } from "react-native"; -const MILES_COUNTRIES = ["US", "GB", "MM", "LR", "PR", "VI", "GU", "MP", "AS", "KY", "TC", "VG", "AI", "MS", "FK"]; - -const formatDistance = (distanceKm: number, country: string | null) => { - const useMiles = country && MILES_COUNTRIES.includes(country); - if (useMiles) { - const distanceMiles = distanceKm * 0.621371; - const rounded = Math.round(distanceMiles); - if (rounded >= 10) { - return `${rounded} mi`; - } - return `${distanceMiles.toFixed(1)} mi`; - } - const rounded = Math.round(distanceKm); - if (rounded >= 10) { - return `${rounded} km`; - } - return `${distanceKm.toFixed(1)} km`; -}; - type HotspotItemProps = { item: Hotspot & { distance?: number }; onSelect: (hotspot: Hotspot & { distance?: number }) => void; diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index 60924b7..e93fffd 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -1,137 +1,170 @@ import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; -import { useTargetRichHotspots } from "@/hooks/useTargetRichHotspots"; import { useLocation } from "@/hooks/useLocation"; import { useScrollRestore } from "@/hooks/useScrollRestore"; -import { getAllHotspots, getNearbyHotspots, searchHotspots } from "@/lib/database"; +import { useTargetRichHotspots } from "@/hooks/useTargetRichHotspots"; +import { getHotspotsWithinBounds, getSavedHotspots, getSavedPlaces } from "@/lib/database"; import tw from "@/lib/tw"; -import { Hotspot } from "@/lib/types"; -import { calculateDistance, getBoundingBoxFromLocation } from "@/lib/utils"; +import { Bounds, Hotspot, SavedPlace } from "@/lib/types"; +import { calculateDistance, isWithinBounds, padBoundsBySize } from "@/lib/utils"; import { useFiltersStore } from "@/stores/filtersStore"; import { useLocationPermissionStore } from "@/stores/locationPermissionStore"; -import { useSettingsStore } from "@/stores/settingsStore"; +import { useMapStore } from "@/stores/mapStore"; import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; -import debounce from "lodash/debounce"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ActivityIndicator, Switch, Text, View } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import BaseBottomSheet from "./BaseBottomSheet"; +import FilterSheet from "./FilterSheet"; import HotspotItem from "./HotspotItem"; import IconButton from "./IconButton"; import IconButtonGroup from "./IconButtonGroup"; -import TargetRichHotspotControls from "./TargetRichHotspotControls"; -import SearchInput from "./SearchInput"; +import PlaceItem from "./PlaceItem"; type HotspotListProps = { isOpen: boolean; onClose: () => void; onSelectHotspot: (hotspotId: string, lat: number, lng: number) => void; + onSelectPlace: (placeId: string, lat: number, lng: number) => void; }; -const NEARBY_LIMIT = 200; -const SEARCH_LIMIT = 100; -const ALL_HOTSPOTS_LIMIT = 1000; -const NEARBY_BUCKETS_KM = [50, 100, 200, 500]; +type HotspotRow = Hotspot & { kind: "hotspot"; distance?: number }; +type PlaceRow = SavedPlace & { kind: "place"; distance?: number }; +type ListRow = HotspotRow | PlaceRow; -export default function HotspotList({ isOpen, onClose, onSelectHotspot }: HotspotListProps) { +// Captured when the list opens so it reflects the map's viewport at that moment, +// rather than shifting if the map keeps reporting bounds during the animation. +type ListSnapshot = { + bounds: Bounds | null; + center: { lat: number; lng: number } | null; + zoomedTooFarOut: boolean; +}; + +export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelectPlace }: HotspotListProps) { const insets = useSafeAreaInsets(); - const { status: permissionStatus, isLoading: isLoadingPermission } = useLocationPermissionStore(); + const { status: permissionStatus } = useLocationPermissionStore(); const { location, isLoading: isLoadingUserLocation } = useLocation(isOpen); - const isLoadingLocation = isLoadingPermission || isLoadingUserLocation; - const { showSavedOnly, setShowSavedOnly } = useFiltersStore(); - const lifelist = useSettingsStore((state) => state.lifelist); - const hasLifeList = (lifelist?.length ?? 0) > 0; + const showSavedOnly = useFiltersStore((state) => state.showSavedOnly); const activeFilterCount = useActiveFilterCount(); const dismissRef = useRef<(() => Promise) | null>(null); - const [searchQuery, setSearchQuery] = useState(""); - const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); - const [debouncedQuery, setDebouncedQuery] = useState(""); + const storeBounds = useMapStore((state) => state.bounds); + const storeMapCenter = useMapStore((state) => state.mapCenter); + const storeZoomedTooFarOut = useMapStore((state) => state.isZoomedTooFarOut); - const debouncedSetQuery = useMemo(() => debounce(setDebouncedQuery, 150), []); + const [snapshot, setSnapshot] = useState(null); + const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false); + const wasOpenRef = useRef(false); + // Snapshot the current viewport on the closed -> open transition only. useEffect(() => { - debouncedSetQuery(searchQuery); - return () => debouncedSetQuery.cancel(); - }, [searchQuery, debouncedSetQuery]); + if (isOpen && !wasOpenRef.current) { + setSnapshot({ bounds: storeBounds, center: storeMapCenter, zoomedTooFarOut: storeZoomedTooFarOut }); + } + wasOpenRef.current = isOpen; + }, [isOpen, storeBounds, storeMapCenter, storeZoomedTooFarOut]); useEffect(() => { - if (!isOpen) { - setIsFilterPanelOpen(false); - } + if (!isOpen) setIsFilterSheetOpen(false); }, [isOpen]); - const hasLocationAccess = permissionStatus === "granted" && location !== null; - - const { - data: searchResults = [], - dataUpdatedAt: searchUpdatedAt, - isFetching: isFetchingSearchResults, - } = useQuery({ - queryKey: ["hotspotSearch", debouncedQuery, showSavedOnly], - queryFn: () => searchHotspots(debouncedQuery, SEARCH_LIMIT, showSavedOnly), - enabled: isOpen && debouncedQuery.length >= 2 && !isLoadingLocation, - staleTime: 60 * 1000, - placeholderData: (prev) => prev, - }); + const snapshotBounds = snapshot?.bounds ?? null; + const isZoomedOut = snapshot?.zoomedTooFarOut ?? false; - const { data: allHotspots = [], isFetching: isFetchingAllHotspots } = useQuery({ - queryKey: - hasLocationAccess && location - ? ["nearbyHotspots", location.lat, location.lng, showSavedOnly] - : ["allHotspots", showSavedOnly], + const { data: hotspots = [], isFetching: isFetchingHotspots } = useQuery({ + // Same key + padding as Mapbox so we hit the same cached viewport query. + queryKey: ["hotspots", snapshotBounds], queryFn: async () => { - if (hasLocationAccess && location) { - // Try the smallest buckets first to get a result quickly - let hotspots: Hotspot[] = []; - for (const radiusKm of NEARBY_BUCKETS_KM) { - const bbox = getBoundingBoxFromLocation(location.lat, location.lng, radiusKm); - hotspots = await getNearbyHotspots(bbox, showSavedOnly); - if (hotspots.length >= NEARBY_LIMIT) { - return hotspots; - } - } - return hotspots; - } - return getAllHotspots(ALL_HOTSPOTS_LIMIT, showSavedOnly); + if (!snapshotBounds) return []; + const padded = padBoundsBySize(snapshotBounds); + return getHotspotsWithinBounds(padded.west, padded.south, padded.east, padded.north); }, - enabled: isOpen && debouncedQuery.length < 2 && !isLoadingLocation, + enabled: isOpen && snapshotBounds !== null, staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, placeholderData: (prev) => prev, }); - const displayedHotspots = useMemo(() => { - const hotspots = debouncedQuery.length >= 2 ? searchResults : allHotspots; + const { data: savedHotspots = [] } = useQuery({ + queryKey: ["savedHotspots"], + queryFn: getSavedHotspots, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); - if (hasLocationAccess && location) { - const hotspotsWithDistance = hotspots.map((h) => ({ - ...h, - distance: calculateDistance(location.lat, location.lng, h.lat, h.lng), - })); - hotspotsWithDistance.sort((a, b) => a.distance - b.distance); - return hotspotsWithDistance.slice(0, debouncedQuery.length >= 2 ? hotspots.length : NEARBY_LIMIT); - } else { - const sorted = [...hotspots].sort((a, b) => a.name.localeCompare(b.name)); - return sorted.slice(0, debouncedQuery.length >= 2 ? hotspots.length : ALL_HOTSPOTS_LIMIT); - } - }, [debouncedQuery, searchResults, allHotspots, hasLocationAccess, location]); + const { data: savedPlaces = [] } = useQuery({ + queryKey: ["savedPlaces"], + queryFn: getSavedPlaces, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); + + const savedHotspotsSet = useMemo(() => new Set(savedHotspots.map((s) => s.hotspot_id)), [savedHotspots]); + const candidateHotspots = useMemo( + () => hotspots.filter((hotspot) => !showSavedOnly || savedHotspotsSet.has(hotspot.id)), + [hotspots, savedHotspotsSet, showSavedOnly] + ); - const isBaseResultsFetching = debouncedQuery.length >= 2 ? isFetchingSearchResults : isFetchingAllHotspots; - const targetRichFilter = useTargetRichHotspots(displayedHotspots.map((hotspot) => hotspot.id), { - enabled: !isBaseResultsFetching, + const targetRichFilter = useTargetRichHotspots(candidateHotspots.map((hotspot) => hotspot.id), { + enabled: isOpen && snapshotBounds !== null && !isFetchingHotspots, blockWhileDisabled: true, }); - const isTargetRichLoading = targetRichFilter.isActive && (isBaseResultsFetching || targetRichFilter.isLoading); - const displayedHotspotsSet = useMemo(() => new Set(targetRichFilter.filteredIds), [targetRichFilter.filteredIds]); - const filteredDisplayedHotspots = useMemo(() => { - if (!targetRichFilter.isActive) { - return displayedHotspots; + const targetRichIds = useMemo(() => new Set(targetRichFilter.filteredIds), [targetRichFilter.filteredIds]); + const isTargetRichLoading = targetRichFilter.isActive && (isFetchingHotspots || targetRichFilter.isLoading); + + const filteredHotspots = useMemo(() => { + if (!targetRichFilter.isActive) return candidateHotspots; + return candidateHotspots.filter((hotspot) => targetRichIds.has(hotspot.id)); + }, [candidateHotspots, targetRichFilter.isActive, targetRichIds]); + + // Saved places within the same viewport. Excluded from the list when the + // target-rich filter is active, since they have no species data to qualify. + const placesInView = useMemo(() => { + if (!snapshotBounds) return []; + const padded = padBoundsBySize(snapshotBounds); + return savedPlaces.filter((place) => isWithinBounds(place.lat, place.lng, padded)); + }, [savedPlaces, snapshotBounds]); + + const hasUserLocation = location !== null; + const originPoint = location ?? snapshot?.center ?? null; + + const rows = useMemo(() => { + // Places are excluded while the target-rich filter is active. + const placesForList = targetRichFilter.isActive ? [] : placesInView; + const base: ListRow[] = [ + ...filteredHotspots.map((hotspot) => ({ ...hotspot, kind: "hotspot" as const })), + ...placesForList.map((place) => ({ ...place, kind: "place" as const })), + ]; + + const withDistance = base.map((row) => ({ + row, + sortDistance: originPoint ? calculateDistance(originPoint.lat, originPoint.lng, row.lat, row.lng) : 0, + })); + + if (originPoint) { + withDistance.sort((a, b) => a.sortDistance - b.sortDistance); + } else { + withDistance.sort((a, b) => a.row.name.localeCompare(b.row.name)); } - return displayedHotspots.filter((hotspot) => displayedHotspotsSet.has(hotspot.id)); - }, [displayedHotspots, displayedHotspotsSet, targetRichFilter.isActive]); + return withDistance.map(({ row, sortDistance }) => { + const distance = hasUserLocation && originPoint ? sortDistance : undefined; + if (row.kind === "hotspot") { + return { ...row, distance }; + } + return { ...row, distance }; + }); + }, [filteredHotspots, placesInView, targetRichFilter.isActive, originPoint, hasUserLocation]); + + const matchingCount = rows.length; + + const isLocationLoading = isLoadingUserLocation && permissionStatus === "granted" && location === null; - const { listRef, onScroll } = useScrollRestore(isOpen, searchUpdatedAt); + const resetKey = useMemo(() => { + if (!snapshotBounds) return 0; + return Math.round((snapshotBounds.west + snapshotBounds.south + snapshotBounds.east + snapshotBounds.north) * 1000); + }, [snapshotBounds]); + const { listRef, onScroll } = useScrollRestore(isOpen, resetKey); const handleSelectHotspot = useCallback( async (hotspot: Hotspot & { distance?: number }) => { @@ -141,45 +174,69 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo [onSelectHotspot] ); - const renderHotspotItem = useCallback( - ({ item }: { item: Hotspot & { distance?: number } }) => , - [handleSelectHotspot] + const handleSelectPlace = useCallback( + async (place: SavedPlace & { distance?: number }) => { + await dismissRef.current?.(); + onSelectPlace(place.id, place.lat, place.lng); + }, + [onSelectPlace] ); + const renderItem = useCallback( + ({ item }: { item: ListRow }) => + item.kind === "hotspot" ? ( + + ) : ( + + ), + [handleSelectHotspot, handleSelectPlace] + ); + + const keyExtractor = useCallback((item: ListRow) => `${item.kind}:${item.id}`, []); + + const showEmptyState = isZoomedOut || isTargetRichLoading || isLocationLoading || rows.length === 0; + const listEmptyComponent = ( - {isTargetRichLoading ? ( + {isZoomedOut ? ( + Zoom in to list hotspots + ) : isTargetRichLoading ? ( <> Filtering hotspots... - ) : isLoadingLocation && permissionStatus === "granted" ? ( + ) : isLocationLoading ? ( <> Getting current location... + ) : activeFilterCount > 0 ? ( + No hotspots match your filters ) : ( - No hotspots found + No hotspots in view )} ); - const headerText = hasLocationAccess ? "Nearby Hotspots" : "Hotspots"; - - const keyExtractor = useCallback((item: Hotspot & { distance?: number }) => item.id, []); + let headerTitle: string; + if (isZoomedOut || snapshotBounds === null || isTargetRichLoading || isLocationLoading) { + headerTitle = "Locations"; + } else { + headerTitle = `${matchingCount} ${matchingCount === 1 ? "location" : "locations"}`; + } const headerContent = (dismiss: () => Promise) => { dismissRef.current = dismiss; return ( - + - {headerText} + {headerTitle} - setIsFilterPanelOpen((open) => !open)} /> + setIsFilterSheetOpen(true)} /> {activeFilterCount > 0 && ( - - - {isFilterPanelOpen && ( - - - Show saved only - - - - - )} - - ); }; @@ -213,7 +257,7 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot }: Hotspo + setIsFilterSheetOpen(false)} /> ); } diff --git a/components/MapListTogglePill.tsx b/components/MapListTogglePill.tsx new file mode 100644 index 0000000..c84e01c --- /dev/null +++ b/components/MapListTogglePill.tsx @@ -0,0 +1,39 @@ +import MapListIcon from "@/components/icons/MapListIcon"; +import tw from "@/lib/tw"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; +import React from "react"; +import { Platform, Text, TouchableOpacity, View } from "react-native"; + +type MapListTogglePillProps = { + onPress: () => void; +}; + +/** + * Bottom-center pill that opens the hotspot list scoped to the map's view. + */ +export default function MapListTogglePill({ onPress }: MapListTogglePillProps) { + const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); + + const content = ( + + + List + + ); + + if (useGlass) { + return ( + + + {content} + + + ); + } + + return ( + + {content} + + ); +} diff --git a/components/Mapbox.tsx b/components/Mapbox.tsx index 4292054..09e9634 100644 --- a/components/Mapbox.tsx +++ b/components/Mapbox.tsx @@ -157,6 +157,9 @@ const MapboxMap = forwardRef( const currentLayer = useMapStore((state) => state.currentLayer); const placeId = useMapStore((state) => state.placeId); const setMapCenter = useMapStore((state) => state.setMapCenter); + const setStoreBounds = useMapStore((state) => state.setBounds); + const setInViewCount = useMapStore((state) => state.setInViewCount); + const setDisplayedCount = useMapStore((state) => state.setDisplayedCount); const isZoomedTooFarOut = useMapStore((state) => state.isZoomedTooFarOut); const setIsZoomedTooFarOut = useMapStore((state) => state.setIsZoomedTooFarOut); const showAttribution = useMapStore((state) => state.isMapAttributionOpen); @@ -399,6 +402,22 @@ const MapboxMap = forwardRef( } }, [displayedHotspots, isTargetRichLoading]); + // Mirror the viewport + result counts into the store so the hotspot list can + // snapshot the same bounds and the toggle pill can show how many are in view. + useEffect(() => { + setStoreBounds(bounds); + }, [bounds, setStoreBounds]); + + useEffect(() => { + if (bounds === null) { + setInViewCount(null); + setDisplayedCount(null); + return; + } + setInViewCount(mapCandidateHotspots.length); + setDisplayedCount(displayedHotspots.length); + }, [bounds, mapCandidateHotspots.length, displayedHotspots.length, setInViewCount, setDisplayedCount]); + const handleFeaturePress = useCallback( (event: any) => { const pressEvent = event as OnPressEvent; diff --git a/components/PlaceEditSheet.tsx b/components/PlaceEditSheet.tsx index 80b197a..0fd353e 100644 --- a/components/PlaceEditSheet.tsx +++ b/components/PlaceEditSheet.tsx @@ -1,6 +1,7 @@ import IconButton from "@/components/IconButton"; import Input from "@/components/Input"; import { deletePlace, getSavedPlaceById, savePlace } from "@/lib/database"; +import { placeIconImages } from "@/lib/placeIconImages"; import { placeIcons, type PlaceIconT } from "@/lib/placeIcons"; import tw from "@/lib/tw"; import { generateId } from "@/lib/utils"; @@ -11,30 +12,6 @@ import { FlatList, Image, ScrollView, Text, TextInput, TouchableOpacity, View } import Toast from "react-native-toast-message"; import BaseBottomSheet from "./BaseBottomSheet"; -const placeIconImages: Record = { - hike: require("@/assets/markers/place-hike.png"), - mountain: require("@/assets/markers/place-mountain.png"), - tent: require("@/assets/markers/place-tent.png"), - house: require("@/assets/markers/place-house.png"), - airbnb: require("@/assets/markers/place-airbnb.png"), - bed: require("@/assets/markers/place-bed.png"), - bins: require("@/assets/markers/place-bins.png"), - camera: require("@/assets/markers/place-camera.png"), - airport: require("@/assets/markers/place-airport.png"), - boat: require("@/assets/markers/place-boat.png"), - car: require("@/assets/markers/place-car.png"), - bus: require("@/assets/markers/place-bus.png"), - utensils: require("@/assets/markers/place-utensils.png"), - mug: require("@/assets/markers/place-mug.png"), - trolley: require("@/assets/markers/place-trolley.png"), - bike: require("@/assets/markers/place-bike.png"), - dog: require("@/assets/markers/place-dog.png"), - fuel: require("@/assets/markers/place-fuel.png"), - parking: require("@/assets/markers/place-parking.png"), - building: require("@/assets/markers/place-building.png"), - hotspot: require("@/assets/markers/place-hotspot.png"), -}; - const placeIconKeys = Object.keys(placeIcons) as PlaceIconT[]; type PlaceEditSheetProps = { diff --git a/components/PlaceItem.tsx b/components/PlaceItem.tsx new file mode 100644 index 0000000..5f63665 --- /dev/null +++ b/components/PlaceItem.tsx @@ -0,0 +1,59 @@ +import { getPlaceIconImage } from "@/lib/placeIconImages"; +import tw from "@/lib/tw"; +import { SavedPlace } from "@/lib/types"; +import { formatDistance } from "@/lib/utils"; +import { Ionicons } from "@expo/vector-icons"; +import React, { useCallback } from "react"; +import { Image, Pressable, Text, View } from "react-native"; + +type PlaceItemProps = { + item: SavedPlace & { distance?: number }; + onSelect: (place: SavedPlace & { distance?: number }) => void; +}; + +const PlaceItem = React.memo( + ({ item, onSelect }: PlaceItemProps) => { + const handlePress = useCallback(() => { + onSelect(item); + }, [item, onSelect]); + + return ( + // Prevent the parent list from canceling row presses after the list is reopened. + [tw`flex-row items-center px-4 py-3 border-b border-gray-200/50`, pressed && tw`opacity-70`]} + > + + + + {item.name} + + + {item.notes ? item.notes : "Place"} + + + {item.distance !== undefined && ( + {formatDistance(item.distance, null)} + )} + + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.item.id === nextProps.item.id && + prevProps.item.lat === nextProps.item.lat && + prevProps.item.lng === nextProps.item.lng && + prevProps.item.name === nextProps.item.name && + prevProps.item.icon === nextProps.item.icon && + prevProps.item.notes === nextProps.item.notes && + prevProps.item.distance === nextProps.item.distance && + prevProps.onSelect === nextProps.onSelect + ); + } +); + +PlaceItem.displayName = "PlaceItem"; + +export default PlaceItem; diff --git a/lib/database.ts b/lib/database.ts index 2ba4d96..b992003 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -208,7 +208,7 @@ export async function getHotspotsWithinBounds( south: number, east: number, north: number -): Promise<{ id: string; lat: number; lng: number; species: number }[]> { +): Promise<{ id: string; name: string; lat: number; lng: number; species: number; country: string | null }[]> { if (!db) throw new Error("Database not initialized"); // When west > east, the bounding box crosses the international date line @@ -216,16 +216,18 @@ export async function getHotspotsWithinBounds( const lngCondition = crossesDateLine ? `(lng >= ? OR lng <= ?)` : `(lng >= ? AND lng <= ?)`; const result = await db.getAllAsync( - `SELECT id, lat, lng, species FROM hotspots + `SELECT id, name, lat, lng, species, country FROM hotspots WHERE lat >= ? AND lat <= ? AND ${lngCondition}`, [south, north, west, east] ); return result.map((row: any) => ({ id: row.id, + name: row.name, lat: row.lat, lng: row.lng, species: row.species, + country: row.country ?? null, })); } diff --git a/lib/placeIconImages.ts b/lib/placeIconImages.ts new file mode 100644 index 0000000..abe0aba --- /dev/null +++ b/lib/placeIconImages.ts @@ -0,0 +1,33 @@ +import { PlaceIconT } from "./placeIcons"; + +// Marker PNGs keyed by saved-place icon. Shared by the map markers, the place +// edit sheet, and the hotspot list's place rows. +export const placeIconImages: Record = { + hike: require("@/assets/markers/place-hike.png"), + mountain: require("@/assets/markers/place-mountain.png"), + tent: require("@/assets/markers/place-tent.png"), + house: require("@/assets/markers/place-house.png"), + airbnb: require("@/assets/markers/place-airbnb.png"), + bed: require("@/assets/markers/place-bed.png"), + bins: require("@/assets/markers/place-bins.png"), + camera: require("@/assets/markers/place-camera.png"), + airport: require("@/assets/markers/place-airport.png"), + boat: require("@/assets/markers/place-boat.png"), + car: require("@/assets/markers/place-car.png"), + bus: require("@/assets/markers/place-bus.png"), + utensils: require("@/assets/markers/place-utensils.png"), + mug: require("@/assets/markers/place-mug.png"), + trolley: require("@/assets/markers/place-trolley.png"), + bike: require("@/assets/markers/place-bike.png"), + dog: require("@/assets/markers/place-dog.png"), + fuel: require("@/assets/markers/place-fuel.png"), + parking: require("@/assets/markers/place-parking.png"), + building: require("@/assets/markers/place-building.png"), + hotspot: require("@/assets/markers/place-hotspot.png"), +}; + +// Resolve a (possibly legacy) saved-place icon value to an image, defaulting to +// the generic hotspot pin for unknown icons (e.g. the old "star" value). +export function getPlaceIconImage(icon: string): any { + return placeIconImages[icon as PlaceIconT] ?? placeIconImages.hotspot; +} diff --git a/lib/types.ts b/lib/types.ts index 183656f..64ca5c6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -49,6 +49,13 @@ export type BoundingBox = { maxLng: number; }; +export type Bounds = { + west: number; + south: number; + east: number; + north: number; +}; + export type MapFeature = { geometry: { coordinates: [number, number]; diff --git a/lib/utils.ts b/lib/utils.ts index f751de8..1f8dff2 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -155,6 +155,33 @@ export function calculateDistance(lat1: number, lon1: number, lat2: number, lon2 return R * c; } +export const MILES_COUNTRIES = ["US", "GB", "MM", "LR", "PR", "VI", "GU", "MP", "AS", "KY", "TC", "VG", "AI", "MS", "FK"]; + +export function formatDistance(distanceKm: number, country: string | null): string { + const useMiles = country && MILES_COUNTRIES.includes(country); + if (useMiles) { + const distanceMiles = distanceKm * 0.621371; + const rounded = Math.round(distanceMiles); + if (rounded >= 10) { + return `${rounded} mi`; + } + return `${distanceMiles.toFixed(1)} mi`; + } + const rounded = Math.round(distanceKm); + if (rounded >= 10) { + return `${rounded} km`; + } + return `${distanceKm.toFixed(1)} km`; +} + +// Whether a point falls inside a viewport bbox. Mirrors the SQL date-line logic +// in getHotspotsWithinBounds (west > east means the box crosses the date line). +export function isWithinBounds(lat: number, lng: number, bounds: Bbox): boolean { + if (lat < bounds.south || lat > bounds.north) return false; + const crossesDateLine = bounds.west > bounds.east; + return crossesDateLine ? lng >= bounds.west || lng <= bounds.east : lng >= bounds.west && lng <= bounds.east; +} + export function getBoundingBoxFromLocation(lat: number, lng: number, radiusKm: number): Bbox { const latDelta = radiusKm / 111; const lngDelta = radiusKm / (111 * Math.cos((lat * Math.PI) / 180)); diff --git a/stores/mapStore.ts b/stores/mapStore.ts index 1900876..a74f143 100644 --- a/stores/mapStore.ts +++ b/stores/mapStore.ts @@ -1,3 +1,4 @@ +import { Bounds } from "@/lib/types"; import { create } from "zustand"; type MapLayerType = "default" | "satellite"; @@ -5,6 +6,12 @@ type MapLayerType = "default" | "satellite"; type MapStore = { currentLayer: MapLayerType; setCurrentLayer: (layer: MapLayerType) => void; + bounds: Bounds | null; + setBounds: (bounds: Bounds | null) => void; + inViewCount: number | null; + setInViewCount: (count: number | null) => void; + displayedCount: number | null; + setDisplayedCount: (count: number | null) => void; hotspotId: string | null; setHotspotId: (id: string | null) => void; placeId: string | null; @@ -28,6 +35,12 @@ type MapStore = { export const useMapStore = create((set) => ({ currentLayer: "default", setCurrentLayer: (layer) => set({ currentLayer: layer }), + bounds: null, + setBounds: (bounds) => set({ bounds }), + inViewCount: null, + setInViewCount: (count) => set({ inViewCount: count }), + displayedCount: null, + setDisplayedCount: (count) => set({ displayedCount: count }), hotspotId: null, setHotspotId: (id) => set({ hotspotId: id }), placeId: null, From 31e6ca7d9a619abcdbcd1557d5f439595062cbfc Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 09:15:29 -0700 Subject: [PATCH 08/23] Switch to two-button approach --- app/index.tsx | 37 ++++++++++++++++++------ components/MapListTogglePill.tsx | 39 -------------------------- components/MapViewControls.tsx | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 48 deletions(-) delete mode 100644 components/MapListTogglePill.tsx create mode 100644 components/MapViewControls.tsx diff --git a/app/index.tsx b/app/index.tsx index 40a515b..5c9080b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,13 +1,15 @@ import CountBadge from "@/components/CountBadge"; +import FilterSheet from "@/components/FilterSheet"; import FloatingButton from "@/components/FloatingButton"; import HotspotDialog from "@/components/HotspotDialog"; import HotspotList from "@/components/HotspotList"; -import MapListTogglePill from "@/components/MapListTogglePill"; +import MapViewControls from "@/components/MapViewControls"; import Mapbox, { MapboxMapRef } from "@/components/Mapbox"; import MenuBottomSheet from "@/components/MenuBottomSheet"; import PacksNotice from "@/components/PacksNotice"; import PlaceDialog from "@/components/PlaceDialog"; import SunIndicator from "@/components/SunIndicator"; +import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { useInstalledPacks } from "@/hooks/useInstalledPacks"; import { usePackUpdates } from "@/hooks/usePackUpdates"; import { useSavedLocation } from "@/hooks/useSavedLocation"; @@ -21,9 +23,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function HomeScreen() { const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false); const mapRef = useRef(null); const isMapTouchActiveRef = useRef(false); const insets = useSafeAreaInsets(); + const activeFilterCount = useActiveFilterCount(); const { isLoadingLocation, savedLocation, updateLocation, hadSavedLocationOnInit } = useSavedLocation(); const { @@ -85,6 +89,14 @@ export default function HomeScreen() { setIsHotspotListOpen(true); }; + const handleOpenFilters = () => { + setIsFilterSheetOpen(true); + }; + + const handleCloseFilters = () => { + setIsFilterSheetOpen(false); + }; + const handleCloseHotspotList = () => { setIsHotspotListOpen(false); }; @@ -191,15 +203,22 @@ export default function HomeScreen() { - {!isHotspotListOpen && ( - - - - )} + {/* Inset by the right action-button zone (w-14 at right-6 ≈ 80px) on both + sides so the centered pill stays screen-centered yet can never collide + with the locate/menu buttons, even on narrow phones. The list sheet + covers this when open, so no need to conditionally hide it. */} + + + + void; -}; - -/** - * Bottom-center pill that opens the hotspot list scoped to the map's view. - */ -export default function MapListTogglePill({ onPress }: MapListTogglePillProps) { - const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); - - const content = ( - - - List - - ); - - if (useGlass) { - return ( - - - {content} - - - ); - } - - return ( - - {content} - - ); -} diff --git a/components/MapViewControls.tsx b/components/MapViewControls.tsx new file mode 100644 index 0000000..3b2a6bf --- /dev/null +++ b/components/MapViewControls.tsx @@ -0,0 +1,48 @@ +import tw from "@/lib/tw"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; +import React from "react"; +import { Platform, Text, TouchableOpacity, View } from "react-native"; + +type MapViewControlsProps = { + onOpenFilters: () => void; + onOpenList: () => void; + filterCount: number; +}; + +/** + * Bottom-center segmented control pairing the two actions that act on the + * current view: open the filter sheet (with an active-filter badge) and open + * the viewport-scoped list. Connected with a hairline divider. + */ +export default function MapViewControls({ onOpenFilters, onOpenList, filterCount }: MapViewControlsProps) { + const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); + + const inner = ( + + + Filters + {filterCount > 0 && ( + + {filterCount} + + )} + + + + List + + + ); + + if (useGlass) { + return ( + + + {inner} + + + ); + } + + return {inner}; +} From 3ad272e97946ff83c1c11bb0682534ee7015a932 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 09:18:22 -0700 Subject: [PATCH 09/23] Update MapViewControls.tsx --- components/MapViewControls.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/MapViewControls.tsx b/components/MapViewControls.tsx index 3b2a6bf..1028bfa 100644 --- a/components/MapViewControls.tsx +++ b/components/MapViewControls.tsx @@ -19,7 +19,7 @@ export default function MapViewControls({ onOpenFilters, onOpenList, filterCount const inner = ( - + Filters {filterCount > 0 && ( @@ -28,7 +28,7 @@ export default function MapViewControls({ onOpenFilters, onOpenList, filterCount )} - + List From fa9cc91b6f6a9fb9dac918ecf913aa95f4788814 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 09:26:55 -0700 Subject: [PATCH 10/23] Switch to filter icon --- components/HotspotList.tsx | 6 +++++- components/MapViewControls.tsx | 3 ++- components/icons/FilterSlidersIcon.tsx | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 components/icons/FilterSlidersIcon.tsx diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index e93fffd..829c864 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -14,6 +14,7 @@ import { useQuery } from "@tanstack/react-query"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import FilterSlidersIcon from "./icons/FilterSlidersIcon"; import BaseBottomSheet from "./BaseBottomSheet"; import FilterSheet from "./FilterSheet"; import HotspotItem from "./HotspotItem"; @@ -236,7 +237,10 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect - setIsFilterSheetOpen(true)} /> + } + onPress={() => setIsFilterSheetOpen(true)} + /> {activeFilterCount > 0 && ( - Filters + {filterCount > 0 && ( {filterCount} diff --git a/components/icons/FilterSlidersIcon.tsx b/components/icons/FilterSlidersIcon.tsx new file mode 100644 index 0000000..2c187d3 --- /dev/null +++ b/components/icons/FilterSlidersIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import Svg, { Path } from "react-native-svg"; + +type Props = { + color?: string; + size?: number; +}; + +// Font Awesome Pro "sliders" (filter) icon. +export default function FilterSlidersIcon({ color = "#374151", size = 20 }: Props) { + return ( + + + + ); +} From aa54d776b98d52bf64581ef0de0d0132bb215469 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 09:32:56 -0700 Subject: [PATCH 11/23] Remove centering tweaks --- app/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index 5c9080b..675183e 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -203,12 +203,8 @@ export default function HomeScreen() { - {/* Inset by the right action-button zone (w-14 at right-6 ≈ 80px) on both - sides so the centered pill stays screen-centered yet can never collide - with the locate/menu buttons, even on narrow phones. The list sheet - covers this when open, so no need to conditionally hide it. */} Date: Fri, 12 Jun 2026 10:04:41 -0700 Subject: [PATCH 12/23] Add search feature --- app/index.tsx | 19 +++ components/SearchInput.tsx | 10 +- components/SearchSheet.tsx | 263 +++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 components/SearchSheet.tsx diff --git a/app/index.tsx b/app/index.tsx index 675183e..ebad95b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -8,6 +8,7 @@ import Mapbox, { MapboxMapRef } from "@/components/Mapbox"; import MenuBottomSheet from "@/components/MenuBottomSheet"; import PacksNotice from "@/components/PacksNotice"; import PlaceDialog from "@/components/PlaceDialog"; +import SearchSheet from "@/components/SearchSheet"; import SunIndicator from "@/components/SunIndicator"; import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { useInstalledPacks } from "@/hooks/useInstalledPacks"; @@ -24,6 +25,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function HomeScreen() { const [isMenuOpen, setIsMenuOpen] = useState(false); const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const mapRef = useRef(null); const isMapTouchActiveRef = useRef(false); const insets = useSafeAreaInsets(); @@ -101,6 +103,14 @@ export default function HomeScreen() { setIsHotspotListOpen(false); }; + const handleOpenSearch = () => { + setIsSearchOpen(true); + }; + + const handleCloseSearch = () => { + setIsSearchOpen(false); + }; + const handleMapTouchActiveChange = useCallback((isActive: boolean) => { isMapTouchActiveRef.current = isActive; }, []); @@ -196,6 +206,9 @@ export default function HomeScreen() { + + + @@ -215,6 +228,12 @@ export default function HomeScreen() { + void; placeholder?: string; -} & Pick; + // When false, the inline clear (✕) button is never shown — useful when the + // surrounding UI already provides a close affordance and a second ✕ would clash. + clearable?: boolean; +} & Pick; const AnimatedPressable = Animated.createAnimatedComponent(Pressable); @@ -28,12 +31,14 @@ export default function SearchInput({ autoCapitalize = "none", autoComplete = "off", returnKeyType = "search", + autoFocus = false, + clearable = true, }: SearchInputProps) { const inputRef = useRef(null); const [isFocused, setIsFocused] = useState(false); const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); - const showClear = value.length > 0 || isFocused; + const showClear = clearable && (value.length > 0 || isFocused); const handleClear = () => { onChangeText(""); @@ -59,6 +64,7 @@ export default function SearchInput({ autoCapitalize={autoCapitalize} autoComplete={autoComplete} returnKeyType={returnKeyType} + autoFocus={autoFocus} /> ); diff --git a/components/SearchSheet.tsx b/components/SearchSheet.tsx new file mode 100644 index 0000000..952662d --- /dev/null +++ b/components/SearchSheet.tsx @@ -0,0 +1,263 @@ +import { useLocation } from "@/hooks/useLocation"; +import { getAllHotspots, getSavedPlaces, searchHotspots } from "@/lib/database"; +import tw from "@/lib/tw"; +import { Hotspot, SavedPlace } from "@/lib/types"; +import { calculateDistance } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; +import { FlashList } from "@shopify/flash-list"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Text, View } from "react-native"; +import BaseBottomSheet from "./BaseBottomSheet"; +import HotspotItem from "./HotspotItem"; +import IconButton from "./IconButton"; +import PlaceItem from "./PlaceItem"; +import SearchInput from "./SearchInput"; + +type SearchSheetProps = { + isOpen: boolean; + onClose: () => void; + onSelectHotspot: (hotspotId: string, lat: number, lng: number) => void; + onSelectPlace: (placeId: string, lat: number, lng: number) => void; +}; + +// Hotspots are only searched once the query is meaningful — a 1-char `LIKE %x%` +// against the full table matches almost everything. +const MIN_HOTSPOT_QUERY = 2; +const HOTSPOT_LIMIT = 30; +// Saved hotspots are user-curated, so this cap is effectively never hit. +const SAVED_HOTSPOT_LIMIT = 200; + +type PlaceWithDistance = SavedPlace & { distance?: number }; +type HotspotWithDistance = Hotspot & { distance?: number }; + +type SearchRow = + | { type: "section"; key: string; title: string } + | { type: "place"; key: string; place: PlaceWithDistance } + | { type: "hotspot"; key: string; hotspot: HotspotWithDistance }; + +export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelectPlace }: SearchSheetProps) { + const [query, setQuery] = useState(""); + const [debouncedQuery, setDebouncedQuery] = useState(""); + const dismissRef = useRef<(() => Promise) | null>(null); + const { location } = useLocation(isOpen); + + // Reset the query each time the sheet closes so it opens fresh. + useEffect(() => { + if (!isOpen) { + setQuery(""); + setDebouncedQuery(""); + } + }, [isOpen]); + + // Debounce the hotspot SQL search; place filtering stays live off `query`. + useEffect(() => { + const handle = setTimeout(() => setDebouncedQuery(query.trim()), 250); + return () => clearTimeout(handle); + }, [query]); + + const { data: savedPlaces = [] } = useQuery({ + queryKey: ["savedPlaces"], + queryFn: getSavedPlaces, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); + + // Initial view (before searching) surfaces the user's saved hotspots. + const { data: savedHotspots = [] } = useQuery({ + queryKey: ["savedHotspotsFull"], + queryFn: () => getAllHotspots(SAVED_HOTSPOT_LIMIT, true), + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); + + const hotspotQueryEnabled = isOpen && debouncedQuery.length >= MIN_HOTSPOT_QUERY; + const { data: hotspotResults = [] } = useQuery({ + queryKey: ["searchHotspots", debouncedQuery], + queryFn: () => searchHotspots(debouncedQuery, HOTSPOT_LIMIT), + enabled: hotspotQueryEnabled, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + placeholderData: (prev) => prev, + }); + + const withDistance = useCallback( + (item: T): T & { distance?: number } => { + if (!location) return item; + return { ...item, distance: calculateDistance(location.lat, location.lng, item.lat, item.lng) }; + }, + [location] + ); + + // Your places: filter by name client-side (small set, always available), then + // sort by distance when we have a location, otherwise alphabetically. + const matchingPlaces = useMemo(() => { + const trimmed = query.trim().toLowerCase(); + const matched = trimmed + ? savedPlaces.filter((place) => place.name.toLowerCase().includes(trimmed)) + : savedPlaces; + const decorated = matched.map(withDistance); + decorated.sort((a, b) => { + if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; + return a.name.localeCompare(b.name); + }); + return decorated; + }, [savedPlaces, query, withDistance]); + + // Hotspots come back alphabetical from SQL; surface prefix matches first so + // the most likely target sits at the top. + const rankedHotspots = useMemo(() => { + if (!hotspotQueryEnabled) return []; + const trimmed = debouncedQuery.toLowerCase(); + const decorated = hotspotResults.map(withDistance); + decorated.sort((a, b) => { + const aPrefix = a.name.toLowerCase().startsWith(trimmed) ? 0 : 1; + const bPrefix = b.name.toLowerCase().startsWith(trimmed) ? 0 : 1; + if (aPrefix !== bPrefix) return aPrefix - bPrefix; + return a.name.localeCompare(b.name); + }); + return decorated; + }, [hotspotResults, debouncedQuery, hotspotQueryEnabled, withDistance]); + + // Before the query reaches search length, the hotspots section shows the + // user's saved hotspots (filtered live by whatever they've typed so far). + const matchingSavedHotspots = useMemo(() => { + const trimmed = query.trim().toLowerCase(); + const matched = trimmed + ? savedHotspots.filter((hotspot) => hotspot.name.toLowerCase().includes(trimmed)) + : savedHotspots; + const decorated = matched.map(withDistance); + decorated.sort((a, b) => { + if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; + return a.name.localeCompare(b.name); + }); + return decorated; + }, [savedHotspots, query, withDistance]); + + // The user's saved places and saved hotspots, interwoven into one list and + // sorted together by distance (or name) — rendered under "Saved locations". + const savedRows = useMemo(() => { + const merged: { sortName: string; distance?: number; row: SearchRow }[] = [ + ...matchingPlaces.map((place) => ({ + sortName: place.name, + distance: place.distance, + row: { type: "place" as const, key: `place:${place.id}`, place }, + })), + ...matchingSavedHotspots.map((hotspot) => ({ + sortName: hotspot.name, + distance: hotspot.distance, + row: { type: "hotspot" as const, key: `hotspot:${hotspot.id}`, hotspot }, + })), + ]; + merged.sort((a, b) => { + if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; + return a.sortName.localeCompare(b.sortName); + }); + return merged.map((entry) => entry.row); + }, [matchingPlaces, matchingSavedHotspots]); + + const savedHotspotIds = useMemo(() => new Set(savedHotspots.map((hotspot) => hotspot.id)), [savedHotspots]); + + const rows = useMemo(() => { + const result: SearchRow[] = []; + if (savedRows.length > 0) { + result.push({ type: "section", key: "section:saved", title: "Saved locations" }); + result.push(...savedRows); + } + // Global search results, excluding saved hotspots already shown above. + if (hotspotQueryEnabled) { + const fresh = rankedHotspots.filter((hotspot) => !savedHotspotIds.has(hotspot.id)); + if (fresh.length > 0) { + result.push({ type: "section", key: "section:hotspots", title: "Hotspots" }); + for (const hotspot of fresh) { + result.push({ type: "hotspot", key: `hotspot:${hotspot.id}`, hotspot }); + } + } + } + return result; + }, [savedRows, rankedHotspots, savedHotspotIds, hotspotQueryEnabled]); + + const handleSelectHotspot = useCallback( + async (hotspot: HotspotWithDistance) => { + await dismissRef.current?.(); + onSelectHotspot(hotspot.id, hotspot.lat, hotspot.lng); + }, + [onSelectHotspot] + ); + + const handleSelectPlace = useCallback( + async (place: PlaceWithDistance) => { + await dismissRef.current?.(); + onSelectPlace(place.id, place.lat, place.lng); + }, + [onSelectPlace] + ); + + const renderItem = useCallback( + ({ item }: { item: SearchRow }) => { + if (item.type === "section") { + return ( + + {item.title} + + ); + } + if (item.type === "place") { + return ; + } + return ; + }, + [handleSelectHotspot, handleSelectPlace] + ); + + const keyExtractor = useCallback((item: SearchRow) => item.key, []); + + // Distinguish empty-because-nothing-typed from empty-because-no-matches. + const isSearching = query.trim().length > 0; + const emptyMessage = isSearching ? "No matches" : "Search hotspots and your saved locations"; + + const headerContent = (dismiss: () => Promise) => { + dismissRef.current = dismiss; + return ( + + + + + + + ); + }; + + return ( + + + {emptyMessage} + + } + /> + + ); +} From 9bd462c25f8baa82fa51c82da6ce2767f593e501 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 10:14:18 -0700 Subject: [PATCH 13/23] Clean up searching --- components/SearchSheet.tsx | 90 +++++++++----------------------------- 1 file changed, 20 insertions(+), 70 deletions(-) diff --git a/components/SearchSheet.tsx b/components/SearchSheet.tsx index 952662d..77c8874 100644 --- a/components/SearchSheet.tsx +++ b/components/SearchSheet.tsx @@ -1,5 +1,5 @@ import { useLocation } from "@/hooks/useLocation"; -import { getAllHotspots, getSavedPlaces, searchHotspots } from "@/lib/database"; +import { getSavedPlaces, searchHotspots } from "@/lib/database"; import tw from "@/lib/tw"; import { Hotspot, SavedPlace } from "@/lib/types"; import { calculateDistance } from "@/lib/utils"; @@ -20,12 +20,10 @@ type SearchSheetProps = { onSelectPlace: (placeId: string, lat: number, lng: number) => void; }; -// Hotspots are only searched once the query is meaningful — a 1-char `LIKE %x%` +// Nothing is searched until the query is meaningful — a 1-char `LIKE %x%` // against the full table matches almost everything. -const MIN_HOTSPOT_QUERY = 2; +const MIN_QUERY = 2; const HOTSPOT_LIMIT = 30; -// Saved hotspots are user-curated, so this cap is effectively never hit. -const SAVED_HOTSPOT_LIMIT = 200; type PlaceWithDistance = SavedPlace & { distance?: number }; type HotspotWithDistance = Hotspot & { distance?: number }; @@ -62,15 +60,7 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect gcTime: 10 * 60 * 1000, }); - // Initial view (before searching) surfaces the user's saved hotspots. - const { data: savedHotspots = [] } = useQuery({ - queryKey: ["savedHotspotsFull"], - queryFn: () => getAllHotspots(SAVED_HOTSPOT_LIMIT, true), - staleTime: 5 * 60 * 1000, - gcTime: 10 * 60 * 1000, - }); - - const hotspotQueryEnabled = isOpen && debouncedQuery.length >= MIN_HOTSPOT_QUERY; + const hotspotQueryEnabled = isOpen && debouncedQuery.length >= MIN_QUERY; const { data: hotspotResults = [] } = useQuery({ queryKey: ["searchHotspots", debouncedQuery], queryFn: () => searchHotspots(debouncedQuery, HOTSPOT_LIMIT), @@ -88,13 +78,13 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect [location] ); - // Your places: filter by name client-side (small set, always available), then - // sort by distance when we have a location, otherwise alphabetically. + // Saved places whose name matches the query (empty below the search threshold), + // sorted by distance when we have a location, otherwise alphabetically. const matchingPlaces = useMemo(() => { const trimmed = query.trim().toLowerCase(); - const matched = trimmed + const matched = trimmed.length >= MIN_QUERY ? savedPlaces.filter((place) => place.name.toLowerCase().includes(trimmed)) - : savedPlaces; + : []; const decorated = matched.map(withDistance); decorated.sort((a, b) => { if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; @@ -118,63 +108,23 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect return decorated; }, [hotspotResults, debouncedQuery, hotspotQueryEnabled, withDistance]); - // Before the query reaches search length, the hotspots section shows the - // user's saved hotspots (filtered live by whatever they've typed so far). - const matchingSavedHotspots = useMemo(() => { - const trimmed = query.trim().toLowerCase(); - const matched = trimmed - ? savedHotspots.filter((hotspot) => hotspot.name.toLowerCase().includes(trimmed)) - : savedHotspots; - const decorated = matched.map(withDistance); - decorated.sort((a, b) => { - if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; - return a.name.localeCompare(b.name); - }); - return decorated; - }, [savedHotspots, query, withDistance]); - - // The user's saved places and saved hotspots, interwoven into one list and - // sorted together by distance (or name) — rendered under "Saved locations". - const savedRows = useMemo(() => { - const merged: { sortName: string; distance?: number; row: SearchRow }[] = [ - ...matchingPlaces.map((place) => ({ - sortName: place.name, - distance: place.distance, - row: { type: "place" as const, key: `place:${place.id}`, place }, - })), - ...matchingSavedHotspots.map((hotspot) => ({ - sortName: hotspot.name, - distance: hotspot.distance, - row: { type: "hotspot" as const, key: `hotspot:${hotspot.id}`, hotspot }, - })), - ]; - merged.sort((a, b) => { - if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; - return a.sortName.localeCompare(b.sortName); - }); - return merged.map((entry) => entry.row); - }, [matchingPlaces, matchingSavedHotspots]); - - const savedHotspotIds = useMemo(() => new Set(savedHotspots.map((hotspot) => hotspot.id)), [savedHotspots]); - const rows = useMemo(() => { const result: SearchRow[] = []; - if (savedRows.length > 0) { + // Saved custom locations always sit above hotspot matches. + if (matchingPlaces.length > 0) { result.push({ type: "section", key: "section:saved", title: "Saved locations" }); - result.push(...savedRows); + for (const place of matchingPlaces) { + result.push({ type: "place", key: `place:${place.id}`, place }); + } } - // Global search results, excluding saved hotspots already shown above. - if (hotspotQueryEnabled) { - const fresh = rankedHotspots.filter((hotspot) => !savedHotspotIds.has(hotspot.id)); - if (fresh.length > 0) { - result.push({ type: "section", key: "section:hotspots", title: "Hotspots" }); - for (const hotspot of fresh) { - result.push({ type: "hotspot", key: `hotspot:${hotspot.id}`, hotspot }); - } + if (rankedHotspots.length > 0) { + result.push({ type: "section", key: "section:hotspots", title: "Hotspots" }); + for (const hotspot of rankedHotspots) { + result.push({ type: "hotspot", key: `hotspot:${hotspot.id}`, hotspot }); } } return result; - }, [savedRows, rankedHotspots, savedHotspotIds, hotspotQueryEnabled]); + }, [matchingPlaces, rankedHotspots]); const handleSelectHotspot = useCallback( async (hotspot: HotspotWithDistance) => { @@ -212,8 +162,8 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect const keyExtractor = useCallback((item: SearchRow) => item.key, []); // Distinguish empty-because-nothing-typed from empty-because-no-matches. - const isSearching = query.trim().length > 0; - const emptyMessage = isSearching ? "No matches" : "Search hotspots and your saved locations"; + const isSearching = query.trim().length >= MIN_QUERY; + const emptyMessage = isSearching ? "No matches" : "Search for hotspots or saved locations"; const headerContent = (dismiss: () => Promise) => { dismissRef.current = dismiss; From 0284db5ee6bdcfe6abdc67af2b6511899da80f89 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 10:18:50 -0700 Subject: [PATCH 14/23] Deslop comments --- components/CountBadge.tsx | 5 ----- components/HotspotList.tsx | 1 - components/MapViewControls.tsx | 5 ----- components/SearchSheet.tsx | 4 ---- hooks/useActiveFilterCount.ts | 6 +----- 5 files changed, 1 insertion(+), 20 deletions(-) diff --git a/components/CountBadge.tsx b/components/CountBadge.tsx index 158fd02..a0d8ba0 100644 --- a/components/CountBadge.tsx +++ b/components/CountBadge.tsx @@ -2,11 +2,6 @@ import tw from "@/lib/tw"; import React from "react"; import { Text, View } from "react-native"; -/** - * Small count badge overlaid on the top-right corner of a floating map button. - * Renders nothing when count is zero. Used for active filters (Hotspots button) - * and pending pack updates (Map Options button). - */ export default function CountBadge({ count }: { count: number }) { if (count <= 0) return null; diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index 829c864..bc8ea31 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -130,7 +130,6 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect const originPoint = location ?? snapshot?.center ?? null; const rows = useMemo(() => { - // Places are excluded while the target-rich filter is active. const placesForList = targetRichFilter.isActive ? [] : placesInView; const base: ListRow[] = [ ...filteredHotspots.map((hotspot) => ({ ...hotspot, kind: "hotspot" as const })), diff --git a/components/MapViewControls.tsx b/components/MapViewControls.tsx index 0212f28..8c0478b 100644 --- a/components/MapViewControls.tsx +++ b/components/MapViewControls.tsx @@ -10,11 +10,6 @@ type MapViewControlsProps = { filterCount: number; }; -/** - * Bottom-center segmented control pairing the two actions that act on the - * current view: open the filter sheet (with an active-filter badge) and open - * the viewport-scoped list. Connected with a hairline divider. - */ export default function MapViewControls({ onOpenFilters, onOpenList, filterCount }: MapViewControlsProps) { const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); diff --git a/components/SearchSheet.tsx b/components/SearchSheet.tsx index 77c8874..0bf18cd 100644 --- a/components/SearchSheet.tsx +++ b/components/SearchSheet.tsx @@ -39,7 +39,6 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect const dismissRef = useRef<(() => Promise) | null>(null); const { location } = useLocation(isOpen); - // Reset the query each time the sheet closes so it opens fresh. useEffect(() => { if (!isOpen) { setQuery(""); @@ -78,8 +77,6 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect [location] ); - // Saved places whose name matches the query (empty below the search threshold), - // sorted by distance when we have a location, otherwise alphabetically. const matchingPlaces = useMemo(() => { const trimmed = query.trim().toLowerCase(); const matched = trimmed.length >= MIN_QUERY @@ -110,7 +107,6 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect const rows = useMemo(() => { const result: SearchRow[] = []; - // Saved custom locations always sit above hotspot matches. if (matchingPlaces.length > 0) { result.push({ type: "section", key: "section:saved", title: "Saved locations" }); for (const place of matchingPlaces) { diff --git a/hooks/useActiveFilterCount.ts b/hooks/useActiveFilterCount.ts index 6920656..4d210b6 100644 --- a/hooks/useActiveFilterCount.ts +++ b/hooks/useActiveFilterCount.ts @@ -1,11 +1,7 @@ import { useFiltersStore } from "@/stores/filtersStore"; import { useSettingsStore } from "@/stores/settingsStore"; -/** - * Number of hotspot filters currently active. Drives the badge on both the - * Nearby Hotspots map button and the filter toggle inside the hotspot list. - * The target-rich filter only counts when a life list is actually imported. - */ +// The target-rich filter only counts as active once a life list is imported. export function useActiveFilterCount() { const showSavedOnly = useFiltersStore((state) => state.showSavedOnly); const targetRichEnabled = useFiltersStore((state) => state.targetRichEnabled); From 5432a5e157c4a1accd0c6396d2ae12e04d770b14 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 12:07:58 -0700 Subject: [PATCH 15/23] Code review fixes --- hooks/useManagePack.ts | 5 ++++ hooks/useTargetRichHotspots.ts | 13 +++++++-- lib/targetRichHotspots.ts | 53 ++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/hooks/useManagePack.ts b/hooks/useManagePack.ts index 8b8f08b..d446b8d 100644 --- a/hooks/useManagePack.ts +++ b/hooks/useManagePack.ts @@ -1,5 +1,6 @@ import { cleanupPartialInstall, installPackWithTargets, uninstallPack } from "@/lib/database"; import { downloadWithProgress } from "@/lib/download"; +import { resetTargetRichHotspotCache } from "@/lib/targetRichHotspots"; import { StaticPack, StaticPackResponse } from "@/lib/types"; import { refreshTaxonomy } from "@/lib/taxonomy"; import { API_URL } from "@/lib/utils"; @@ -69,6 +70,8 @@ export function useManagePack(packId: number) { downloadStore.setProgress(100); + resetTargetRichHotspotCache(); + await queryClient.invalidateQueries({ queryKey: ["installed-packs"] }); queryClient.invalidateQueries({ queryKey: ["hotspots"], refetchType: "active" }); queryClient.invalidateQueries({ queryKey: ["hotspotSearch"] }); @@ -111,6 +114,8 @@ export function useManagePack(packId: number) { await uninstallPack(packId); + resetTargetRichHotspotCache(); + queryClient.invalidateQueries({ queryKey: ["installed-packs"] }); queryClient.invalidateQueries({ queryKey: ["hotspots"], refetchType: "active" }); queryClient.invalidateQueries({ queryKey: ["hotspotSearch"] }); diff --git a/hooks/useTargetRichHotspots.ts b/hooks/useTargetRichHotspots.ts index 32b3ceb..90d6e07 100644 --- a/hooks/useTargetRichHotspots.ts +++ b/hooks/useTargetRichHotspots.ts @@ -1,11 +1,13 @@ import { createTargetRichHotspotBasis, + getTargetRichHotspotCacheGeneration, + subscribeToTargetRichHotspotCacheReset, targetRichHotspotCache, syncTargetRichHotspotCacheBasis, } from "@/lib/targetRichHotspots"; import { useFiltersStore } from "@/stores/filtersStore"; import { useSettingsStore } from "@/stores/settingsStore"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; type UseTargetRichHotspotsOptions = { enabled?: boolean; @@ -59,6 +61,11 @@ export function useTargetRichHotspots( [lifelist, lifelistExclusions, targetMonths, minTargets, minTargetFrequency] ); + const cacheGeneration = useSyncExternalStore( + subscribeToTargetRichHotspotCacheReset, + getTargetRichHotspotCacheGeneration + ); + const hasLifeList = basis !== null; const isActive = targetRichEnabled && hasLifeList; const isEnabled = options.enabled ?? true; @@ -115,7 +122,7 @@ export function useTargetRichHotspots( })); void targetRichHotspotCache - .evaluateMany(unresolvedHotspotIds, basisForRun, abortController.signal) + .evaluateMany(stableHotspotIds, basisForRun, abortController.signal) .then(() => { if (abortController.signal.aborted) { return; @@ -157,7 +164,7 @@ export function useTargetRichHotspots( return () => { abortController.abort(); }; - }, [basis?.cacheKey, candidateKey, hasLifeList, isActive, isEnabled, stableHotspotIds]); + }, [basis?.cacheKey, cacheGeneration, candidateKey, hasLifeList, isActive, isEnabled, stableHotspotIds]); return useMemo(() => { if (!isActive) { diff --git a/lib/targetRichHotspots.ts b/lib/targetRichHotspots.ts index 49c11cf..4415005 100644 --- a/lib/targetRichHotspots.ts +++ b/lib/targetRichHotspots.ts @@ -144,7 +144,8 @@ class TargetRichHotspotCache { basis: TargetRichHotspotBasis, signal?: AbortSignal ): Promise { - const missingHotspotIds = [...new Set(hotspotIds)].filter((hotspotId) => !this.cache.has(hotspotId)); + const requestedHotspotIds = new Set(hotspotIds); + const missingHotspotIds = [...requestedHotspotIds].filter((hotspotId) => !this.cache.has(hotspotId)); if (missingHotspotIds.length === 0) { return; } @@ -179,6 +180,12 @@ class TargetRichHotspotCache { await new Promise((resolve) => setTimeout(resolve, 0)); } } + + // Trim only AFTER the run completes, and never evict entries belonging to + // the viewport we just evaluated. This keeps a single over-capacity viewport + // fully resolved (it would otherwise evict its own earliest results mid-run + // and never converge), while still bounding memory across distinct viewports. + this.trim(requestedHotspotIds); } finally { this.activeSignals.delete(controller); } @@ -190,13 +197,23 @@ class TargetRichHotspotCache { } this.cache.set(hotspotId, value); + } + + private trim(protectedHotspotIds: ReadonlySet) { + if (this.cache.size <= CACHE_CAPACITY) { + return; + } - while (this.cache.size > CACHE_CAPACITY) { - const oldestHotspotId = this.cache.keys().next().value; - if (!oldestHotspotId) { + let removable = this.cache.size - CACHE_CAPACITY; + for (const hotspotId of [...this.cache.keys()]) { + if (removable <= 0) { break; } - this.cache.delete(oldestHotspotId); + if (protectedHotspotIds.has(hotspotId)) { + continue; + } + this.cache.delete(hotspotId); + removable -= 1; } } } @@ -214,3 +231,29 @@ export function syncTargetRichHotspotCacheBasis(nextBasisKey: string | null) { targetRichHotspotCache.cancelActiveRun(); targetRichHotspotCache.clear(); } + +// Bumped whenever the underlying target data changes (e.g. a pack install / +// update / uninstall). Cached per-hotspot results are keyed only by hotspot id, +// so they must be discarded when the data behind those ids is rewritten. +let cacheGeneration = 0; +const cacheResetListeners = new Set<() => void>(); + +export function getTargetRichHotspotCacheGeneration(): number { + return cacheGeneration; +} + +export function subscribeToTargetRichHotspotCacheReset(listener: () => void): () => void { + cacheResetListeners.add(listener); + return () => { + cacheResetListeners.delete(listener); + }; +} + +export function resetTargetRichHotspotCache() { + cacheGeneration += 1; + targetRichHotspotCache.cancelActiveRun(); + targetRichHotspotCache.clear(); + for (const listener of cacheResetListeners) { + listener(); + } +} From 0904b4b9774ff9f26c82567777e8f8acda0ef94e Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 12:19:18 -0700 Subject: [PATCH 16/23] Update and fix types --- components/ActionButtonRow.tsx | 2 +- components/ui/IconSymbol.tsx | 2 +- package-lock.json | 3020 +++++++++++++------------------- package.json | 43 +- 4 files changed, 1276 insertions(+), 1791 deletions(-) diff --git a/components/ActionButtonRow.tsx b/components/ActionButtonRow.tsx index 50e2f9c..da81ee7 100644 --- a/components/ActionButtonRow.tsx +++ b/components/ActionButtonRow.tsx @@ -17,7 +17,7 @@ export default function ActionButtonRow({ children, stacked = false }: ActionBut {child} )); - const containerStyle = [tw`w-full mt-2`, stacked ? tw`flex-col` : tw`flex-row`, { gap: GAP }] as const; + const containerStyle = [tw`w-full mt-2`, stacked ? tw`flex-col` : tw`flex-row`, { gap: GAP }]; return useGlass ? ( diff --git a/components/ui/IconSymbol.tsx b/components/ui/IconSymbol.tsx index f3687fa..e22a33a 100644 --- a/components/ui/IconSymbol.tsx +++ b/components/ui/IconSymbol.tsx @@ -5,7 +5,7 @@ import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; import { ComponentProps } from 'react'; import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; -type IconMapping = Record['name']>; +type IconMapping = Record, ComponentProps['name']>; type IconSymbolName = keyof typeof MAPPING; /** diff --git a/package-lock.json b/package-lock.json index 0936298..cf0e98f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "openbirding", "version": "1.0.0", "dependencies": { - "@expo/metro-runtime": "~55.0.6", + "@expo/metro-runtime": "~55.0.11", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^15.0.2", "@lodev09/react-native-true-sheet": "^3.9.2", @@ -21,31 +21,31 @@ "@tanstack/react-query": "^5.87.1", "@types/lodash": "^4.17.20", "dayjs": "^1.11.19", - "expo": "~55.0.15", + "expo": "~55.0.26", "expo-blur": "~55.0.14", - "expo-constants": "~55.0.9", - "expo-dev-client": "~55.0.27", + "expo-constants": "~55.0.16", + "expo-dev-client": "~55.0.35", "expo-document-picker": "~55.0.13", - "expo-file-system": "~55.0.11", - "expo-font": "~55.0.4", - "expo-glass-effect": "~55.0.8", + "expo-file-system": "~55.0.22", + "expo-font": "~55.0.8", + "expo-glass-effect": "~55.0.11", "expo-haptics": "~55.0.14", - "expo-image": "~55.0.6", - "expo-linking": "~55.0.13", - "expo-location": "~55.1.8", - "expo-router": "~55.0.12", - "expo-splash-screen": "~55.0.18", - "expo-sqlite": "~55.0.15", - "expo-status-bar": "~55.0.4", - "expo-symbols": "~55.0.5", - "expo-system-ui": "~55.0.15", - "expo-updates": "~55.0.20", - "expo-web-browser": "~55.0.14", + "expo-image": "~55.0.11", + "expo-linking": "~55.0.15", + "expo-location": "~55.1.10", + "expo-router": "~55.0.16", + "expo-splash-screen": "~55.0.21", + "expo-sqlite": "~55.0.16", + "expo-status-bar": "~55.0.6", + "expo-symbols": "~55.0.9", + "expo-system-ui": "~55.0.18", + "expo-updates": "~55.0.24", + "expo-web-browser": "~55.0.16", "lodash": "^4.17.21", "nativewind": "^4.1.23", "react": "19.2.0", "react-dom": "19.2.0", - "react-native": "0.83.4", + "react-native": "0.83.6", "react-native-gesture-handler": "~2.30.0", "react-native-popover-view": "^6.1.0", "react-native-reanimated": "4.2.1", @@ -55,7 +55,7 @@ "react-native-toast-message": "^2.3.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.0", - "react-native-worklets": "0.7.2", + "react-native-worklets": "0.7.4", "suncalc": "^1.9.0", "tailwindcss": "^3.4.17", "twrnc": "^4.9.1", @@ -63,10 +63,11 @@ }, "devDependencies": { "@types/react": "~19.2.10", + "@types/sharp": "^0.31.1", "@types/suncalc": "^1.9.2", "dotenv-cli": "^11.0.0", "eslint": "^9.25.0", - "eslint-config-expo": "~55.0.0", + "eslint-config-expo": "~55.0.1", "tsx": "^4.21.0", "typescript": "~5.9.2" } @@ -84,12 +85,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -98,9 +99,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -137,13 +138,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -153,25 +154,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -181,17 +182,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -202,12 +203,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -256,49 +257,49 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -308,35 +309,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -346,14 +347,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -363,54 +364,54 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -429,13 +430,99 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -445,14 +532,14 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", - "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-decorators": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -462,12 +549,12 @@ } }, "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", - "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -528,12 +615,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", - "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -555,12 +642,12 @@ } }, "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", - "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -570,12 +657,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", - "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -624,12 +711,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -741,12 +828,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -771,14 +858,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -788,14 +875,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -805,12 +892,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -820,13 +907,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -836,13 +923,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -852,17 +939,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -872,13 +959,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -888,13 +975,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -904,12 +991,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -919,13 +1006,13 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -935,13 +1022,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -951,14 +1038,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -968,12 +1055,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -983,12 +1070,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -998,13 +1085,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1014,13 +1101,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1030,12 +1117,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1045,12 +1132,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1060,16 +1147,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1079,12 +1166,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1094,13 +1181,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1110,12 +1197,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1125,13 +1212,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1141,14 +1228,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1158,12 +1245,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1173,16 +1260,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1192,12 +1279,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" + "@babel/plugin-transform-react-jsx": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1207,12 +1294,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1222,12 +1309,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1237,13 +1324,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1253,12 +1340,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1268,13 +1355,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1303,13 +1390,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1319,12 +1406,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1349,16 +1436,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1384,17 +1471,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1404,16 +1491,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1432,31 +1519,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1464,13 +1551,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1529,7 +1616,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1546,7 +1632,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1563,7 +1648,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1580,7 +1664,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1597,7 +1680,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1614,7 +1696,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1631,7 +1712,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1648,7 +1728,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1665,7 +1744,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1682,7 +1760,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1699,7 +1776,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1716,7 +1792,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1733,7 +1808,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1750,7 +1824,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1767,7 +1840,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1784,7 +1856,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1801,7 +1872,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1818,7 +1888,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1835,7 +1904,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1852,7 +1920,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1869,7 +1936,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1886,7 +1952,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1903,7 +1968,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1920,7 +1984,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1937,7 +2000,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1954,7 +2016,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2109,9 +2170,9 @@ } }, "node_modules/@expo-google-fonts/material-symbols": { - "version": "0.4.33", - "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.33.tgz", - "integrity": "sha512-GQFy5tx7LBiRJttLYhz+KPmoVCQSVXiJPXVgMt/d8BWYbnJmw0nFixZtOaZQJE9F/L6vni3totwPNmbrdMi3ow==", + "version": "0.4.38", + "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.38.tgz", + "integrity": "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==", "license": "MIT AND Apache-2.0" }, "node_modules/@expo/code-signing-certificates": { @@ -2124,15 +2185,15 @@ } }, "node_modules/@expo/config": { - "version": "55.0.15", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.15.tgz", - "integrity": "sha512-lHc0ELIQ8126jYOMZpLv3WIuvordW98jFg5aT/J1/12n2ycuXu01XLZkJsdw0avO34cusUYb1It+MvY8JiMduA==", + "version": "55.0.17", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.17.tgz", + "integrity": "sha512-Y3VaRg7Jllg3MhlUOTQqHm6/dttsqcjYlnS9enhAllZvPUpTHnRA4YPETtUZlxkdMJy6y3UZe986pd/KfJ6OTg==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~55.0.8", + "@expo/config-plugins": "~55.0.9", "@expo/config-types": "^55.0.5", - "@expo/json-file": "^10.0.13", - "@expo/require-utils": "^55.0.4", + "@expo/json-file": "^10.0.14", + "@expo/require-utils": "^55.0.5", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", @@ -2142,14 +2203,14 @@ } }, "node_modules/@expo/config-plugins": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.8.tgz", - "integrity": "sha512-8WfWTRntTCcowfOS+tHdB0z98gKetTwktg4G5TWkCkXVa8Jt1NUnvzaaU4UHk2vbR2U4N84RyZJFizSwfF6C9g==", + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.10.tgz", + "integrity": "sha512-1txnRnMLIO5lM/Of/VyvDkCwZap0YFvCyfSTIlUQamhwhx6Rh7r8TXfcIstaDYUQ7X6GTMkNxLXWbcYS6ZAFDw==", "license": "MIT", "dependencies": { "@expo/config-types": "^55.0.5", - "@expo/json-file": "~10.0.13", - "@expo/plist": "^0.5.2", + "@expo/json-file": "~10.0.15", + "@expo/plist": "^0.5.4", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", @@ -2180,6 +2241,16 @@ "integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==", "license": "MIT" }, + "node_modules/@expo/config/node_modules/@expo/json-file": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", + "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, "node_modules/@expo/config/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -2212,9 +2283,9 @@ } }, "node_modules/@expo/devtools": { - "version": "55.0.2", - "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-55.0.2.tgz", - "integrity": "sha512-4VsFn9MUriocyuhyA+ycJP3TJhUsOFHDc270l9h3LhNpXMf6wvIdGcA0QzXkZtORXmlDybWXRP2KT1k36HcQkA==", + "version": "55.0.3", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-55.0.3.tgz", + "integrity": "sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg==", "license": "MIT", "dependencies": { "chalk": "^4.1.2" @@ -2233,9 +2304,9 @@ } }, "node_modules/@expo/dom-webview": { - "version": "55.0.5", - "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-55.0.5.tgz", - "integrity": "sha512-lt3uxYOCk3wmWvtOOvsC35CKGbDAOx5C2EaY8SH1JVSfBzqmF8Cs0Xp1MPxncDPMyxpMiWx5SvvV/iLF1rJU4A==", + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-55.0.6.tgz", + "integrity": "sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -2244,9 +2315,9 @@ } }, "node_modules/@expo/env": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.1.tgz", - "integrity": "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.2.tgz", + "integrity": "sha512-RJtGFfj/ygO/6zcVbV3cckHf4THcEkv5IZft1GjCB3dfT6axvzvIwXE9EiQqQYmGHcQ+ZrvC8xZcIhiHba0pYg==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -2258,12 +2329,12 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.16.6.tgz", - "integrity": "sha512-nRITNbnu3RKSHPvKVehrSU4KG2VY9V8nvULOHBw98ukHCAU4bGrU5APvcblOkX3JAap+xEHsg/mZvqlvkLInmQ==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.16.7.tgz", + "integrity": "sha512-BH8sicYOqZ1iBMwCVEGIz6uTTfylosjc49FoMmCYIzKOiYdiVehsfoYBwyfxwWIiya1VMhm1gv0cgOP8fxHpDw==", "license": "MIT", "dependencies": { - "@expo/env": "^2.0.11", + "@expo/env": "^2.1.2", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", @@ -2279,19 +2350,33 @@ "fingerprint": "bin/cli.js" } }, - "node_modules/@expo/fingerprint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@expo/fingerprint/node_modules/@expo/env": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.3.0.tgz", + "integrity": "sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "getenv": "^2.0.0" + }, + "engines": { + "node": ">=20.12.0" + } + }, + "node_modules/@expo/fingerprint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@expo/fingerprint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2316,9 +2401,9 @@ } }, "node_modules/@expo/fingerprint/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2328,12 +2413,12 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.13.tgz", - "integrity": "sha512-1I//yBQeTY6p0u1ihqGNDAr35EbSG8uFEupFrIF0jd++h9EWH33521yZJU1yE+mwGlzCb61g3ehu78siMhXBlA==", + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.14.tgz", + "integrity": "sha512-5Sn+jG4Cw+shC2wDMXoqSAJnvERbiwzHn05FpWtD5IBflfTIs5gUmjzwiGVyjOdlMSQhgRrw/AymPbmO9h9mpQ==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^55.0.4", + "@expo/require-utils": "^55.0.5", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", @@ -2355,106 +2440,80 @@ } }, "node_modules/@expo/json-file": { - "version": "10.0.13", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.13.tgz", - "integrity": "sha512-pX/XjQn7tgNw6zuuV2ikmegmwe/S7uiwhrs2wXrANMkq7ozrA+JcZwgW9Q/8WZgciBzfAhNp5hnackHcrmapQA==", + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", + "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.20.0", + "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, + "node_modules/@expo/json-file/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, "node_modules/@expo/local-build-cache-provider": { - "version": "55.0.11", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.11.tgz", - "integrity": "sha512-rJ4RTCrkeKaXaido/bVyhl90ZRtVTOEbj59F1PWVjIEIVgjdlfc1J3VD9v7hEsbf/+8Tbr/PgvWhT6Visi5sLQ==", + "version": "55.0.13", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.13.tgz", + "integrity": "sha512-Vg5BE10UL+0yg3BVtIeiSoeHU31Qe1m3UxhBPS478ACY1zzKuxZE30x2sym/B2OIWypjmPzXDRt8J9TOGFuFNw==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.15", + "@expo/config": "~55.0.17", "chalk": "^4.1.2" } }, "node_modules/@expo/log-box": { - "version": "55.0.11", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.11.tgz", - "integrity": "sha512-JQHFLWkskIbJi6cxYMjErx8lQqfFJilDQLKmdTO3m3YkdmN9GE/CrzjOfVlCG0DGEGZJ90br0pGKvGPdXNsHKw==", + "version": "55.0.12", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.12.tgz", + "integrity": "sha512-f9ARS8J60cq3LLNdIqmUjYwyerBzVS5Ecp7KjIf3GOIPjW0571rkcwLz4/U18l/1DeSkSzIkYsNl2TC9oTdWaQ==", "license": "MIT", "dependencies": { - "@expo/dom-webview": "^55.0.5", + "@expo/dom-webview": "^55.0.6", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { - "@expo/dom-webview": "^55.0.5", + "@expo/dom-webview": "^55.0.6", "expo": "*", "react": "*", "react-native": "*" } }, "node_modules/@expo/metro": { - "version": "55.1.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-55.1.0.tgz", - "integrity": "sha512-bb/LOncsz9KiP6cHmMy0MCDG1COZOn+k+pRpDrvJUmxLdOOuniJSYyCc/Dgv1bR9E/6YR+fh3EXGg9MUrVNy4Q==", - "license": "MIT", - "dependencies": { - "metro": "0.83.6", - "metro-babel-transformer": "0.83.6", - "metro-cache": "0.83.6", - "metro-cache-key": "0.83.6", - "metro-config": "0.83.6", - "metro-core": "0.83.6", - "metro-file-map": "0.83.6", - "metro-minify-terser": "0.83.6", - "metro-resolver": "0.83.6", - "metro-runtime": "0.83.6", - "metro-source-map": "0.83.6", - "metro-symbolicate": "0.83.6", - "metro-transform-plugins": "0.83.6", - "metro-transform-worker": "0.83.6" - } - }, - "node_modules/@expo/metro-config": { - "version": "55.0.17", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.17.tgz", - "integrity": "sha512-o11VyNoRDXv0T5320D9cH+nSsrR/OMHTjtysKLIfDlidsBswDk1DMApPv9Kw0/gluArCSnbx8JC1G0Yh2Y4P3g==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~55.0.15", - "@expo/env": "~2.1.1", - "@expo/json-file": "~10.0.13", - "@expo/metro": "~55.1.0", - "@expo/spawn-async": "^1.7.2", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.32.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.3", - "postcss": "~8.4.32", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } + "version": "55.1.1", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-55.1.1.tgz", + "integrity": "sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg==", + "license": "MIT", + "dependencies": { + "metro": "0.83.7", + "metro-babel-transformer": "0.83.7", + "metro-cache": "0.83.7", + "metro-cache-key": "0.83.7", + "metro-config": "0.83.7", + "metro-core": "0.83.7", + "metro-file-map": "0.83.7", + "metro-minify-terser": "0.83.7", + "metro-resolver": "0.83.7", + "metro-runtime": "0.83.7", + "metro-source-map": "0.83.7", + "metro-symbolicate": "0.83.7", + "metro-transform-plugins": "0.83.7", + "metro-transform-worker": "0.83.7" } }, "node_modules/@expo/metro-runtime": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-55.0.10.tgz", - "integrity": "sha512-7v+ldTvMWRa1ml83Jel9W2f8qT/NZZWrlHaEjf29nb72JTEO50+Xac9PWLo+X3LCDAAuyYuBGKYXOJwfqxV0fQ==", + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-55.0.11.tgz", + "integrity": "sha512-4KKi/jGrIEXi2YGu0hYTVr0CEeRJy5SXbCrz9+KDZkuD3ROwKNpM1DBawni5rhPVovFnR323HBck9GaxhnfrRw==", "license": "MIT", "dependencies": { - "@expo/log-box": "55.0.11", + "@expo/log-box": "55.0.12", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", @@ -2473,35 +2532,45 @@ } }, "node_modules/@expo/osascript": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", - "integrity": "sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.6.0.tgz", + "integrity": "sha512-QvqDBlJXa8CS2vRORJ4wEflY1m0vVI07uSJdIRgBrLxRPBcsrXxrtU7+wXRXMqfq9zLwNP9XbvRsXF2omoDylg==", "license": "MIT", "dependencies": { - "@expo/spawn-async": "^1.7.2" + "@expo/spawn-async": "^1.8.0" }, "engines": { "node": ">=12" } }, "node_modules/@expo/package-manager": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.10.4.tgz", - "integrity": "sha512-y9Mr4Kmpk4abAVZrNNPCdzOZr8nLLyi18p1SXr0RCVA8IfzqZX/eY4H+50a0HTmXqIsPZrQdcdb4I3ekMS9GvQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.12.1.tgz", + "integrity": "sha512-fQLiFAcFRWF53mtuLK32SUJQ1ahhrTcBZPZPedYTiUT5ha5FF+UO6bPtCc0Y/hgj0/m3HCGBAuSHjbg2kI9oPQ==", "license": "MIT", "dependencies": { - "@expo/json-file": "^10.0.13", - "@expo/spawn-async": "^1.7.2", + "@expo/json-file": "^10.2.0", + "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, + "node_modules/@expo/package-manager/node_modules/@expo/json-file": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", + "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, "node_modules/@expo/plist": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.2.tgz", - "integrity": "sha512-o4xdVdBpe4aTl3sPMZ2u3fJH4iG1I768EIRk1xRZP+GaFI93MaR3JvoFibYqxeTmLQ1p1kNEVqylfUjezxx45g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.4.tgz", + "integrity": "sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -2510,16 +2579,16 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "55.0.16", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.16.tgz", - "integrity": "sha512-o4EAVgDGk1lISirtMD8hciO2vyMp7cWlPdfTtjjd5AXSfODVYDIDhygXrfvVQHmJXAztVqPUTKJT+BYOsVkYGQ==", + "version": "55.0.18", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.18.tgz", + "integrity": "sha512-2oKXyy5pyM87DJqXW5Z+Sakle6rApFFtpPhWOiNsOdoh6rOAD+EqVgyrs2OEEic8CE0tTt27w3SRfSZe/PZrxg==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.15", - "@expo/config-plugins": "~55.0.8", + "@expo/config": "~55.0.17", + "@expo/config-plugins": "~55.0.9", "@expo/config-types": "^55.0.5", - "@expo/image-utils": "^0.8.13", - "@expo/json-file": "^10.0.13", + "@expo/image-utils": "^0.8.14", + "@expo/json-file": "^10.0.14", "@react-native/normalize-colors": "0.83.6", "debug": "^4.3.1", "resolve-from": "^5.0.0", @@ -2531,9 +2600,9 @@ } }, "node_modules/@expo/prebuild-config/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2556,9 +2625,9 @@ } }, "node_modules/@expo/require-utils": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.4.tgz", - "integrity": "sha512-JAANvXqV7MOysWeVWgaiDzikoyDjJWOV/ulOW60Zb3kXJfrx2oZOtGtDXDFKD1mXuahQgoM5QOjuZhF7gFRNjA==", + "version": "55.0.5", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.5.tgz", + "integrity": "sha512-U4K/CQ2VpXuwfNGsN+daKmYOt15hCP8v/pXaYH6eut7kdYZo6SfJ1yr67BIcJ+1Gzzs+QzTxswAZChKpXmceyw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -2575,9 +2644,9 @@ } }, "node_modules/@expo/schema-utils": { - "version": "55.0.3", - "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.3.tgz", - "integrity": "sha512-l9KHVjTo6MvoeyvwNr6AjckGJm8NIcqZ3QSAh51cWozXW9v2AUjyCyqYtFtyntLWRZ0x/ByYJishpQo4ZQq45Q==", + "version": "55.0.4", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.4.tgz", + "integrity": "sha512-65IdeeE8dAZR3n3J5Eq7LYiQ8BFGeEYCWPBCzycvafL7PkskbCyIclTQarRwf/HXFoRvezKCjaLwy/8v9Prk6g==", "license": "MIT" }, "node_modules/@expo/sdk-runtime-versions": { @@ -2587,12 +2656,12 @@ "license": "MIT" }, "node_modules/@expo/spawn-async": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", - "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.8.0.tgz", + "integrity": "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3" + "cross-spawn": "^7.0.6" }, "engines": { "node": ">=12" @@ -2622,9 +2691,9 @@ "license": "MIT" }, "node_modules/@expo/xcpretty": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.3.tgz", - "integrity": "sha512-wC562eD3gS6vO2tWHToFhlFnmHKfKHgF1oyvojeSkLK/ZYop1bMU+7cOMiF9Sq70CzcsLy/EMRy/uRc76QmNRw==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz", + "integrity": "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==", "license": "BSD-3-Clause", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -3564,132 +3633,68 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.4.tgz", - "integrity": "sha512-aqKtpbJDSQeSX/Dwv0yMe1/Rd2QfXi12lnyZDXNn/OEKz59u6+LuPBVgO/9CRyclHmdlvwg8c7PJ9eX2ZMnjWg==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.6.tgz", + "integrity": "sha512-iljb4ue1yWJ3EhySz7EjV6CzSVrI2uNtR8BI2jzP5+QS5E4Cl3fdIJRmVwDEx1pu8uE97PGEusGRHnoaZ9Q3jg==", "license": "MIT", "engines": { "node": ">= 20.19.4" } }, - "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.85.2.tgz", - "integrity": "sha512-5Dqn08kRTUIxPLYju9hExI0cR1ESX+P5tEv5yv0q0UZcisRTw0VB8iUWDIph2LdY1i5Dc8PIvuaWMRNCw3vnKg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.85.2" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/babel-preset": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.2.tgz", - "integrity": "sha512-7d2yW23eKkVt0FbbnZLxqO7KybGLtQXOuvvcO1NUOYGtjzVh6ihNKn0TIHrhSNpMyHwYLDoiiuj95wLtcg3IwQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.85.2", - "babel-plugin-syntax-hermes-parser": "0.33.3", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, "node_modules/@react-native/codegen": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.2.tgz", - "integrity": "sha512-XCginmxh0//++EXVOEJHBVZxHla294FzLCFF6jXwAUjvXVhqyIKyxhABfz+r4OOmaiuWk4Rtd4arqdAzeHeprg==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.6.tgz", + "integrity": "sha512-doB/Pq6Cf6IjF3wlQXTIiZOnsX9X8mEEk+CdGfyuCwZjWrf7IB8KaZEXXckJmfUcIwvJ9u/a72ZoTTCIoxAc9A==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.33.3", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", "yargs": "^17.6.2" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { "@babel/core": "*" } }, - "node_modules/@react-native/codegen/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/codegen/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { - "hermes-estree": "0.33.3" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.83.4.tgz", - "integrity": "sha512-8os0weQEnjUhWy7Db881+JKRwNHVGM40VtTRvltAyA/YYkrGg4kPCqiTybMxQDEcF3rnviuxHyI+ITiglfmgmQ==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.83.6.tgz", + "integrity": "sha512-Mko6mywoHYJmpBnjwAC95vQWaUUh//71knFadH0BrhHDq2m7i/IrpLwcQsPAy8855ucXflBs5zQyGTpNbPBAaw==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.83.4", + "@react-native/dev-middleware": "0.83.6", "debug": "^4.4.0", "invariant": "^2.2.4", - "metro": "^0.83.3", - "metro-config": "^0.83.3", - "metro-core": "^0.83.3", + "metro": "^0.83.6", + "metro-config": "^0.83.6", + "metro-core": "^0.83.6", "semver": "^7.1.3" }, "engines": { @@ -3709,9 +3714,9 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3721,454 +3726,66 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz", - "integrity": "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.6.tgz", + "integrity": "sha512-TyWXEpAjVundrc87fPWg91piOUg75+X9iutcfDe7cO3NrAEYCsl7Z09rKHuiAGkxfG9/rFD13dPsYIixUFkSFA==", "license": "BSD-3-Clause", "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz", - "integrity": "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.6.tgz", + "integrity": "sha512-684TJMBCU0l0ZjJWzrnK0HH+ERaM9KLyxyArE1k7BrP+gVl4X9GO0Pi94RoInOxvW/nyV65sOU6Ip1F3ygS0cg==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/dev-middleware": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz", - "integrity": "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==", - "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.83.4", - "@react-native/debugger-shell": "0.83.4", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.4.tgz", - "integrity": "sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/js-polyfills": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.2.tgz", - "integrity": "sha512-esGEAmKVM40DV/yVmNljCKZTIeUo7qXqc+Hwffkv3TG+b3E24xyFovHrbP98gGxZr2ZsEyx+2sKLdXF5asY5nw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-babel-transformer": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.2.tgz", - "integrity": "sha512-lU9XOGahpHvQff30H5lnvh9RYbVwC1zpSHpl84E+7BD2zj0FvW+pD7MBh7CWbmbWmegjtAb+U/2bokXcDVA+jA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.85.2", - "hermes-parser": "0.33.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/@react-native/metro-config": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.2.tgz", - "integrity": "sha512-YkTIMfTPeyMUrtpQo/7zd3oybVYJCfTp8626PqoakOvEiWi9PxsUpZ8j44a5GFtOIq8Nc6WWVBiFRn/6qdi1uQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@react-native/js-polyfills": "0.85.2", - "@react-native/metro-babel-transformer": "0.85.2", - "metro-config": "^0.84.0", - "metro-runtime": "^0.84.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/hermes-estree": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", - "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-config/node_modules/hermes-parser": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", - "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.35.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.3.tgz", - "integrity": "sha512-1h3lbVrE6hGf1e/764HfhPGg/bGrWMJDDh7G2rc4gFYZboVuI40BlG/y+UhtbhQDNlO/csMvrcnK0YrTlHUVew==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "accepts": "^2.0.0", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "error-stack-parser": "^2.0.6", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.35.0", - "image-size": "^1.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "jsc-safe-url": "^0.2.2", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-config": "0.84.3", - "metro-core": "0.84.3", - "metro-file-map": "0.84.3", - "metro-resolver": "0.84.3", - "metro-runtime": "0.84.3", - "metro-source-map": "0.84.3", - "metro-symbolicate": "0.84.3", - "metro-transform-plugins": "0.84.3", - "metro-transform-worker": "0.84.3", - "mime-types": "^3.0.1", - "nullthrows": "^1.1.1", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "throat": "^5.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "metro": "src/cli.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-babel-transformer": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.3.tgz", - "integrity": "sha512-svAA+yMLpeMiGcz/jKJs4oHpIGEx4nBqNEJ5AGj4CYIg1efvK+A0TjR6tgIuc6tKO5e8JmN/1lglpN2+f3/z/w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-cache": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.3.tgz", - "integrity": "sha512-0QElxwLaHqLZf+Xqio8QrjVbuXP/8sJfQBGSPiITlKDVXrVLefuzYVSH9Sj+QL6lrPj2gYZd/iwQh1yZuVKnLA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "exponential-backoff": "^3.1.1", - "flow-enums-runtime": "^0.0.6", - "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-cache-key": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.3.tgz", - "integrity": "sha512-TnSL1Fdvrw+2glTdBSRmA5TL8l/i16ECjsrUdf3E5HncA+sNx8KcwDG8r+3ct1UhfYcusJypzZqTN55FZZcwGg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-config": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.3.tgz", - "integrity": "sha512-JmCzZWOETR+O22q8oPBWyQppx3roU9EbkbGzD8Gf1jukQ4b5T1fTzqqHruu6K4sTiNq5zVQySmKF6bp4kVARew==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "connect": "^3.6.5", - "flow-enums-runtime": "^0.0.6", - "jest-validate": "^29.7.0", - "metro": "0.84.3", - "metro-cache": "0.84.3", - "metro-core": "0.84.3", - "metro-runtime": "0.84.3", - "yaml": "^2.6.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-core": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.3.tgz", - "integrity": "sha512-cc0pvAa80ai1nDmqqz0P59a+0ZqCZ/YHU/3jEekZL6spFnYDfX8iDLdn9FR6kX+67rmzKxHNrbrSRFLX2AYocw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-file-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.3.tgz", - "integrity": "sha512-1cL4m4Jv1yRUt9RJExZQLfccscdlMNOcRG6LHLtmJhf3BG9j3MujPVc7CIpKYdFl+KUl+sdjge6oO3+meKCHQA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "fb-watchman": "^2.0.0", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "nullthrows": "^1.1.1", - "walker": "^1.0.7" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-minify-terser": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.3.tgz", - "integrity": "sha512-3ofrG2OQyJbO9RNhCfOcl8QU7EE2WrSsnN5dFkuZaJO5+4Imujr9bUXmspeNlXRsOVk0F/rVRbEFH98lFSCkBQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "terser": "^5.15.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-resolver": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.3.tgz", - "integrity": "sha512-pjEzGDtoM8DTHAIPK/9u9ZxszEiuRohYUVImWvgbnB91V4gqYJpQcoEYUugf2NIm1lrX5HNu0OvNqWmPBnGYjA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-runtime": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.3.tgz", - "integrity": "sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-source-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.3.tgz", - "integrity": "sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.84.3", - "nullthrows": "^1.1.1", - "ob1": "0.84.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-symbolicate": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.3.tgz", - "integrity": "sha512-J9Tpo8NCycYrozRvBIUyOwGAu4xkawOsAppmTscFiaegK0WvuDGwIM53GbzVSnytCHjVAF0io5GQxpkrKTuc7g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.84.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" + "fb-dotslash": "0.5.8" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/metro-transform-plugins": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.3.tgz", - "integrity": "sha512-8S3baq2XhBaafHEH5Q8sJW6tmzsEJk80qKc3RU/nZV1MsnYq94RdjTUR6AyKjQd6Rfsk1BtBxhtiNnk7mgslCg==", + "node_modules/@react-native/dev-middleware": { + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.6.tgz", + "integrity": "sha512-22xoddLTelpcVnF385SNH2hdP7X2av5pu7yRl/WnM5jBznbcl0+M9Ce94cj+WVeomsoUF/vlfuB0Ooy+RMlRiA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "nullthrows": "^1.1.1" + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.83.6", + "@react-native/debugger-shell": "0.83.6", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^7.5.10" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/metro-transform-worker": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.3.tgz", - "integrity": "sha512-Wjba7PyYktNRsHbPmkx2J2UX32rAzcDXjCu49zPHeF/viJlYJhwRaNePQcHaCRqQ+kmgQT4ThprsnJfDj71ZMA==", + "node_modules/@react-native/gradle-plugin": { + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.6.tgz", + "integrity": "sha512-5prXv7WWR1RgZ/kWGZP+mi7/y/IE2ymfOHIZO5Pv14tMOmRAcQSgSYogcRmOiWw5mJs2K0UFeMiQD49ZO9oCug==", "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "metro": "0.84.3", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-minify-terser": "0.84.3", - "metro-source-map": "0.84.3", - "metro-transform-plugins": "0.84.3", - "nullthrows": "^1.1.1" - }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/ob1": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.3.tgz", - "integrity": "sha512-J7554Ef8bzmKaDY365Afq6PF+qtdnY/d5PKUQFrsKlZHV/N3OGZewVrvDrQDyX5V5NJjTpcAKtlrFZcDr+HvpQ==", + "node_modules/@react-native/js-polyfills": { + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.83.6.tgz", + "integrity": "sha512-VSev0LV2i5X0ibduHBSLqKj0YU2F+waCgjl2uvaGHMGCSV1ZRKNFX/vJFqvLwjvdzLbkAZoFT1Rg7k7jDv44UA==", "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, "node_modules/@react-native/normalize-color": { @@ -4741,6 +4358,16 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -5819,299 +5446,65 @@ "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-react-compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", - "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - } - }, - "node_modules/babel-plugin-react-native-web": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", - "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", - "license": "MIT" - }, - "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz", - "integrity": "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-parser": "0.33.3" - } - }, - "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/babel-plugin-transform-flow-enums": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", - "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-flow": "^7.12.1" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-expo": { - "version": "55.0.18", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.18.tgz", - "integrity": "sha512-zmDwKxCFBTe4e/jQXuITRUZlbl8HTZOhsUlwcHGjwEUB0lKQfRdaSYXZckQ+jMOBC34MrOl3Cs7/6F6vNbj5Pw==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-preset": "0.83.6", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.0", - "babel-plugin-syntax-hermes-parser": "^0.32.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^55.0.14", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/babel-plugin-codegen": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.6.tgz", - "integrity": "sha512-qfRXsHGeucT5c6mK+8Q7v4Ly3zmygfVmFlEtkiq7q07W1OTreld6nib4rJ/DBEeNiKBoBTuHjWliYGNuDjLFQA==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.83.6" - }, - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/babel-preset": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.6.tgz", - "integrity": "sha512-4/fXFDUvGOObETZq4+SUFkafld6OGgQWut5cQiqVghlhCB5z/p2lVhPgEUr/aTxTzeS3AmN+ztC+GpYPQ7tsTw==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.83.6", - "babel-plugin-syntax-hermes-parser": "0.32.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": ">= 20.19.4" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", - "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", - "license": "MIT", - "dependencies": { - "hermes-parser": "0.32.0" - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/babel-preset/node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.32.0" - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/codegen": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.6.tgz", - "integrity": "sha512-doB/Pq6Cf6IjF3wlQXTIiZOnsX9X8mEEk+CdGfyuCwZjWrf7IB8KaZEXXckJmfUcIwvJ9u/a72ZoTTCIoxAc9A==", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.25.3", - "glob": "^7.1.1", - "hermes-parser": "0.32.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">= 20.19.4" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { - "@babel/core": "*" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-preset-expo/node_modules/@react-native/codegen/node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "license": "MIT", "dependencies": { - "hermes-estree": "0.32.0" + "@babel/types": "^7.26.0" } }, - "node_modules/babel-preset-expo/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.1.tgz", - "integrity": "sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==", + "node_modules/babel-plugin-react-native-web": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", + "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", + "license": "MIT" + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", "license": "MIT", "dependencies": { - "hermes-parser": "0.32.1" + "@babel/plugin-syntax-flow": "^7.12.1" } }, - "node_modules/babel-preset-expo/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, - "node_modules/babel-preset-expo/node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "license": "MIT" - }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -7067,9 +6460,9 @@ "license": "MIT" }, "node_modules/dnssd-advertise": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.4.tgz", - "integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz", + "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, "node_modules/doctrine": { @@ -7567,16 +6960,16 @@ } }, "node_modules/eslint-config-expo": { - "version": "55.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-55.0.0.tgz", - "integrity": "sha512-YvhaKrp1g7pR/qjdI12E5nw9y0DJZWgYr815vyW8wskGLsFvxATY3mtKL8zm3ZYzWj3Bvc37tRIS661TEkrv9A==", + "version": "55.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-55.0.1.tgz", + "integrity": "sha512-sfVHQ0GKZofMA0zUu5lah7PJ2LdcQwExGK+BLM9YvVPMRf6kezFYCpC6atT1ddX3L9oeCxS6FygDXbvCwjocNg==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-expo": "^1.0.0", + "eslint-plugin-expo": "^1.0.3", "eslint-plugin-import": "^2.30.0", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", @@ -7685,9 +7078,9 @@ } }, "node_modules/eslint-plugin-expo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.0.0.tgz", - "integrity": "sha512-qLtunR+cNFtC+jwYCBia5c/PJurMjSLMOV78KrEOyQK02ohZapU4dCFFnS2hfrJuw0zxfsjVkjqg3QBqi933QA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.0.3.tgz", + "integrity": "sha512-C1v9NPvpDET36+7Klpp/+53Jl+VzOfpbDxpKtL/pAPhCDwTX0kW6Swo425PT0uc4AMT5jpQbB7hSKFjKOGMl4A==", "dev": true, "license": "MIT", "dependencies": { @@ -7918,34 +7311,34 @@ } }, "node_modules/expo": { - "version": "55.0.17", - "resolved": "https://registry.npmjs.org/expo/-/expo-55.0.17.tgz", - "integrity": "sha512-yVF2phiPw5XgOCedC/oQaL3j0XbwzsBLst3JiAF8bi9aFlxLOVvuDEM8BDg3E09XGSLaGCAclY4q5L+sFerXlQ==", + "version": "55.0.26", + "resolved": "https://registry.npmjs.org/expo/-/expo-55.0.26.tgz", + "integrity": "sha512-MuVW6Uzd/Jh6E37ICOYAiTOm9nflNMUNzf6wH5ld/IXFyuF2Lo86a8fCSMgHcvTGsSjRsJ5Uxhf+WHZcvGPfrg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "55.0.26", - "@expo/config": "~55.0.15", - "@expo/config-plugins": "~55.0.8", - "@expo/devtools": "55.0.2", - "@expo/fingerprint": "0.16.6", - "@expo/local-build-cache-provider": "55.0.11", - "@expo/log-box": "55.0.11", - "@expo/metro": "~55.1.0", - "@expo/metro-config": "55.0.17", + "@expo/cli": "55.0.32", + "@expo/config": "~55.0.17", + "@expo/config-plugins": "~55.0.10", + "@expo/devtools": "55.0.3", + "@expo/fingerprint": "0.16.7", + "@expo/local-build-cache-provider": "55.0.13", + "@expo/log-box": "55.0.12", + "@expo/metro": "~55.1.1", + "@expo/metro-config": "55.0.23", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~55.0.18", - "expo-asset": "~55.0.16", - "expo-constants": "~55.0.15", - "expo-file-system": "~55.0.17", - "expo-font": "~55.0.6", - "expo-keep-awake": "~55.0.6", - "expo-modules-autolinking": "55.0.18", - "expo-modules-core": "55.0.23", + "babel-preset-expo": "~55.0.22", + "expo-asset": "~55.0.17", + "expo-constants": "~55.0.16", + "expo-file-system": "~55.0.22", + "expo-font": "~55.0.8", + "expo-keep-awake": "~55.0.8", + "expo-modules-autolinking": "55.0.24", + "expo-modules-core": "55.0.25", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", - "whatwg-url-minimum": "^0.1.1" + "whatwg-url-minimum": "^0.1.2" }, "bin": { "expo": "bin/cli", @@ -7971,21 +7364,6 @@ } } }, - "node_modules/expo-asset": { - "version": "55.0.16", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.16.tgz", - "integrity": "sha512-5IJyfJtYqvKGg04NKGQWiCIoK/fULDL9m15mXPPyfabD1jsToVj2hnWmo1r2SWNNmMwtQxi6jTpcGwVo2nLDxg==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.8.13", - "expo-constants": "~55.0.15" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/expo-blur": { "version": "55.0.14", "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-55.0.14.tgz", @@ -7998,12 +7376,12 @@ } }, "node_modules/expo-constants": { - "version": "55.0.15", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.15.tgz", - "integrity": "sha512-w394fcZLJjeKN+9ZnJzL/HiarE1nwZFDa+3S9frevh6Ur+MAAs9QDrcXhDrV8T3xqRzzYaqsP6Z8TFZ4efWN1A==", + "version": "55.0.16", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.16.tgz", + "integrity": "sha512-Z15/No94UHoogD+pulxjudGAeOHTEIWZgb/vnX48Wx5D+apWTeCbnKxQZZtGQlosvduYL5kaic2/W8U+NHfBQQ==", "license": "MIT", "dependencies": { - "@expo/env": "~2.1.1" + "@expo/env": "~2.1.2" }, "peerDependencies": { "expo": "*", @@ -8011,15 +7389,15 @@ } }, "node_modules/expo-dev-client": { - "version": "55.0.28", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-55.0.28.tgz", - "integrity": "sha512-QZK6Ylx8Jg7lhOOHCxwC10g+i34ggMBAqV497JXFqla1tuuYiEw1poNJS5pD/60ZLe8kyy5PYPB4E9ezDHA9yQ==", + "version": "55.0.35", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-55.0.35.tgz", + "integrity": "sha512-DN50x9gqWYAfnJpxgiJm3zK2bFvDhxJ5JjFq0wFot7o4knZ7H3BVwiL6zZMHG29g6gfxdgpzGG69WPiSR/Ipgg==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "55.0.29", - "expo-dev-menu": "55.0.24", + "expo-dev-launcher": "55.0.36", + "expo-dev-menu": "55.0.30", "expo-dev-menu-interface": "55.0.2", - "expo-manifests": "~55.0.16", + "expo-manifests": "~55.0.17", "expo-updates-interface": "~55.1.6" }, "peerDependencies": { @@ -8027,23 +7405,23 @@ } }, "node_modules/expo-dev-launcher": { - "version": "55.0.29", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-55.0.29.tgz", - "integrity": "sha512-Rusz6VfVUAXPArkQhnxC5yY70RCfGNZv+06qCGIkm2boQ3wOiSUwJic8oIt7kW6yD2rkpm24q/7F/6r5joPfng==", + "version": "55.0.36", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-55.0.36.tgz", + "integrity": "sha512-Dn2om4J71aavWqi1jLzK3QlGZjDiFv7nIBZkQyzy2zW62IOD9kLwOOvHHj07Ra/6n9cqFEpNYzwpPkR7KHuYZA==", "license": "MIT", "dependencies": { - "@expo/schema-utils": "^55.0.3", - "expo-dev-menu": "55.0.24", - "expo-manifests": "~55.0.16" + "@expo/schema-utils": "^55.0.4", + "expo-dev-menu": "55.0.30", + "expo-manifests": "~55.0.17" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-dev-menu": { - "version": "55.0.24", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-55.0.24.tgz", - "integrity": "sha512-/J93rADODlKpmaN9uywTd/RMywPDeUo/bAnrZNxlHrFUuO1VCGqYLhacITg2zebU8hucaou8pa8zVsTQaUCv6w==", + "version": "55.0.30", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-55.0.30.tgz", + "integrity": "sha512-uwDI4cEPzpRemf06Ts5O41azJcz8BBcE6QOkNaTX8JlzdJ05eq9jWxmbA1WhoSoE5C+NFo8njHSvmHqUqTpOng==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "55.0.2" @@ -8077,9 +7455,9 @@ "license": "MIT" }, "node_modules/expo-file-system": { - "version": "55.0.17", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.17.tgz", - "integrity": "sha512-d27K1cagUOt2BwxwPka9KW8Znu5kN1tnairozCzzCRZviZFtWnBxwFuJ3KU6MAbav/9UhSMkp5Ve/oZ+SR0UgQ==", + "version": "55.0.22", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.22.tgz", + "integrity": "sha512-T5Rfv3vqcFyhVrl/tEEeglc/J8LJbcZQgC3TMT5jxzIgUgWmIgJEgncGYqB/YNXFgUTL2LiuCvqrU51Dzp83NQ==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -8087,9 +7465,9 @@ } }, "node_modules/expo-font": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.6.tgz", - "integrity": "sha512-x9czUA3UQWjIwa0ZUEs/eWJNqB4mAue/m4ltESlNPLZhHL0nWWqIfsyHmklTLFH7mVfcHSJvew6k+pR2FE1zVw==", + "version": "55.0.8", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.8.tgz", + "integrity": "sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -8101,9 +7479,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-55.0.10.tgz", - "integrity": "sha512-5kL/jATvgJWdrqPdxixrECJqD2l8cfQ4ALr1DK7qi9XkyI97ejXvUjB2VsfEePNy3Fg+/VwzA3n3L7Nv3tAPkw==", + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-55.0.11.tgz", + "integrity": "sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -8121,9 +7499,9 @@ } }, "node_modules/expo-image": { - "version": "55.0.9", - "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-55.0.9.tgz", - "integrity": "sha512-+NVgWv+tr7a6EpBEaIIVVp+XfruRA2JL5xOxvd6ajvFGdH0rOhagwX1m1piAII6w7sh6uAnBr8X+fDZsav7B2w==", + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-55.0.11.tgz", + "integrity": "sha512-PVIBYQJW/h1f6Zb9xnoWlgfqyOPVm2yb6eo6ZogaKbvMrhb/Q/fiERbagi4oqmR6IPljWPEpkXXQyFBUh7TjpQ==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.2.0" @@ -8146,23 +7524,13 @@ "integrity": "sha512-QJMOZOPOG7CTnKcrdVaiummn2va1MCO56z++eyWkDv3GBRODldM6MFMDf/jTREWthFc2Nxo6TuyWRrEV9S6n/Q==", "license": "MIT" }, - "node_modules/expo-keep-awake": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.6.tgz", - "integrity": "sha512-acJjeHqkNxMVckEcJhGQeIksqqsarscSHJtT559bNgyiM4r14dViQ66su7bb6qDVeBt0K7z3glXI1dHVck1Zgg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, "node_modules/expo-linking": { - "version": "55.0.14", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.14.tgz", - "integrity": "sha512-ZSqOvJyEquf04M5/ZpQo2diK9QRnNrzgqZo7p8gzxaPPHxP6IyUJnmcd12qT+dTxnRTVmUpxFQVHHWbvwPNIwQ==", + "version": "55.0.15", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.15.tgz", + "integrity": "sha512-/RQh2vkNqV8Bim9Owm/evVqn2fqTvCDYHkpYPoSKbLAdydSGdHC2xZNw7Odl4wu1i1/3L4Xz//LKd3NsPWYWBQ==", "license": "MIT", "dependencies": { - "expo-constants": "~55.0.15", + "expo-constants": "~55.0.16", "invariant": "^2.2.4" }, "peerDependencies": { @@ -8171,21 +7539,21 @@ } }, "node_modules/expo-location": { - "version": "55.1.8", - "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.8.tgz", - "integrity": "sha512-mEExFf84nmWLwi14GFfUsFLrCm10gbcqFn9EPXpuruQ28YMtJWgCD+jJtESYPQkYF44N21fVok3T28fLuCqydA==", + "version": "55.1.10", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.10.tgz", + "integrity": "sha512-MkcFucsZ567Bn8ChElVTYVbOs2QXn27IKaBrVKogw7ZcbooImdj3L/UR6E7s3LkgF33YubKynAp9Opvixdwl7g==", "license": "MIT", "dependencies": { - "@expo/image-utils": "^0.8.13" + "@expo/image-utils": "^0.8.14" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-manifests": { - "version": "55.0.16", - "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-55.0.16.tgz", - "integrity": "sha512-BR9BPcNsSnCKlQ/d7ECywr+2T54+bTSr26HjRjSua949o4mO/iPIrLjK0lOAa1oIczju6a6oUFckZD2OljxP0g==", + "version": "55.0.17", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-55.0.17.tgz", + "integrity": "sha512-vKZvFivX3usVJKfBODKQcFHso0g38zlGbRGqGAppz+il0zKvG6umpJ47OZbzLod7iJpjd+ZDD2AGuOxacixonA==", "license": "MIT", "dependencies": { "expo-json-utils": "~55.0.2" @@ -8195,12 +7563,12 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "55.0.18", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.18.tgz", - "integrity": "sha512-olGTCWYkwVPj/momcgnF+z8MTzurGNFjopqPztQ4F53UkGPJnOFEuaM2/z4KbZtKbwHqeBv34OA5hxZP8uLdaQ==", + "version": "55.0.24", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.24.tgz", + "integrity": "sha512-A0OyMbTPZqibYrwqj98HFYTNSvl4NSS4Zt+R5A8qiAx3nM0mc81e6Iqw7Wl4J8M/t36lJ+cT3WuVTz5Oszj6Hw==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^55.0.4", + "@expo/require-utils": "^55.0.5", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" @@ -8210,13 +7578,13 @@ } }, "node_modules/expo-router": { - "version": "55.0.13", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.13.tgz", - "integrity": "sha512-cIBR5RmQtbr+b535mlbMhmm7lweVZXFtjzJOgJTutoxIApRztl816kFRFNesnVyqQ0LZrEU0a6vqa3i0wdlRQw==", + "version": "55.0.16", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.16.tgz", + "integrity": "sha512-xVwWsDz3Ar2+3hRpMMrZMYFzkJak322vCA5/XCP7WOL0hEXnWhgQGhv5IEYZyz/TXZbl2IYD6/1MnH9mBhjwKQ==", "license": "MIT", "dependencies": { - "@expo/metro-runtime": "^55.0.10", - "@expo/schema-utils": "^55.0.3", + "@expo/metro-runtime": "^55.0.11", + "@expo/schema-utils": "^55.0.4", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.15.5", @@ -8225,10 +7593,10 @@ "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "expo-glass-effect": "^55.0.10", - "expo-image": "^55.0.9", - "expo-server": "^55.0.8", - "expo-symbols": "^55.0.7", + "expo-glass-effect": "^55.0.11", + "expo-image": "^55.0.11", + "expo-server": "^55.0.11", + "expo-symbols": "^55.0.9", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", @@ -8243,13 +7611,13 @@ "vaul": "^1.1.2" }, "peerDependencies": { - "@expo/log-box": "55.0.11", - "@expo/metro-runtime": "^55.0.10", + "@expo/log-box": "55.0.12", + "@expo/metro-runtime": "^55.0.11", "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^55.0.15", - "expo-linking": "^55.0.14", + "expo-constants": "^55.0.16", + "expo-linking": "^55.0.15", "react": "*", "react-dom": "*", "react-native": "*", @@ -8297,30 +7665,30 @@ } }, "node_modules/expo-server": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.8.tgz", - "integrity": "sha512-AoV5TKuO4biSzrhe/OVLyInfTT0pV9/OOc/g/oVq5vmCjL8SaSYTkES8PLt+67Tm7VqX+Dn0+kSx1nQcjEKaPw==", + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.11.tgz", + "integrity": "sha512-AxRdHqcv0H1g4s923vu+5n1Nrhne23bjXbP+Vl7+Lwfpe7MG9PuU1IS95IJK6a+7BVV1mRN6QlZvs8Yv7EEXNQ==", "license": "MIT", "engines": { "node": ">=20.16.0" } }, "node_modules/expo-splash-screen": { - "version": "55.0.19", - "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-55.0.19.tgz", - "integrity": "sha512-l8BWI/inLJW46Ojz5NgwvaM8LftrdXeFfZBUXhAoZxg44Qo2xKY76s0S1h3WIxWXT4sRKwK8YQzGr4k+zHubxQ==", + "version": "55.0.21", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-55.0.21.tgz", + "integrity": "sha512-hFGEap69ggCckbHIdDXMe5rqfBR9TwcnY5gBhyaACUxU64w827T6prOQcIvLmAdv00kp3Gqt7hgE+mNn37EF+A==", "license": "MIT", "dependencies": { - "@expo/prebuild-config": "^55.0.16" + "@expo/prebuild-config": "^55.0.18" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-sqlite": { - "version": "55.0.15", - "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-55.0.15.tgz", - "integrity": "sha512-vxE5fs6l953QSIyievQ8TuSstj62eC7zUREjNzbUOwRWaHGGnhnlPJM1HLoTIv+oIt3+b1m7k2fmcDGkpK5t3w==", + "version": "55.0.16", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-55.0.16.tgz", + "integrity": "sha512-v6EIL4ygqWt/+ZfI76jIIv+IIaU8PnWPNjkmIN95vEQgh0FrWqzwssqe5ffQmm79kIfqIPTtAgTdl8MuZv88gg==", "license": "MIT", "dependencies": { "await-lock": "^2.2.2" @@ -8332,9 +7700,9 @@ } }, "node_modules/expo-status-bar": { - "version": "55.0.5", - "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.5.tgz", - "integrity": "sha512-qb0c3rJO2b7CC0gUVGi1JYp92oLenWdYGyk8l4YQs6U+uaXUTPv6aaFa3KkT2HON10re3AxxPNJci8rsz6kPxg==", + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.6.tgz", + "integrity": "sha512-ijOUptfdiqYt7rObZ6jrPQ8sE5YN/8MxKCIJx0b7TY4nGkSJxhPIxeoW4GXcXCA8mTQ9PiOHH/ThLZgRVZvUlQ==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" @@ -8351,9 +7719,9 @@ "license": "MIT" }, "node_modules/expo-symbols": { - "version": "55.0.7", - "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-55.0.7.tgz", - "integrity": "sha512-y4ALLbncSGQzhFLw1PaIBbO39xzaw3ie249HmK6zK/WLJYfw4Z/9UU4iPKO3KCE4FyCKIzd+yRsvzvlri23YrQ==", + "version": "55.0.9", + "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-55.0.9.tgz", + "integrity": "sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg==", "license": "MIT", "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", @@ -8367,9 +7735,9 @@ } }, "node_modules/expo-system-ui": { - "version": "55.0.16", - "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-55.0.16.tgz", - "integrity": "sha512-LwFBpFzy7L4j0ZqHZaxNU4tewQXkH37N4afXu6ZrkyKsH9q5V3jOT1way/N+Hylgyx5+jGpzvae9OcphS/+iDQ==", + "version": "55.0.18", + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-55.0.18.tgz", + "integrity": "sha512-Fbc0HJgqMpABeA/gI7NJFnSXwUeLrEMjjXq8Nl+4gTXyacIK2iOOrzCkvq41rKBBde0CR6kVnB1DXj0j9ZYnjg==", "license": "MIT", "dependencies": { "@react-native/normalize-colors": "0.83.6", @@ -8387,19 +7755,19 @@ } }, "node_modules/expo-updates": { - "version": "55.0.21", - "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-55.0.21.tgz", - "integrity": "sha512-wpWQAqNeBw1LLjqSK85/P9aHB+2R0nuuFPHb8ZRPRMJLhRUIk7IF0FaOdEy2NbiRJvrnGfRW3SK4NVQqrT8ULQ==", + "version": "55.0.24", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-55.0.24.tgz", + "integrity": "sha512-aqbsRT5GyKG8++RndIb4+jFUknsPgqWImzYUG20PiPjwPlQ25MSfz5+r1IAI8YfvGuLRIIRt8yDQ2Ob+RV+fyg==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", - "@expo/plist": "^0.5.2", + "@expo/plist": "^0.5.4", "@expo/spawn-async": "^1.7.2", "arg": "^4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", "expo-eas-client": "~55.0.5", - "expo-manifests": "~55.0.16", + "expo-manifests": "~55.0.17", "expo-structured-headers": "~55.0.2", "expo-updates-interface": "~55.1.6", "getenv": "^2.0.0", @@ -8432,9 +7800,9 @@ "license": "MIT" }, "node_modules/expo-web-browser": { - "version": "55.0.14", - "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-55.0.14.tgz", - "integrity": "sha512-bTDkBSQBnrlnYcM7Aak72AOvJuvdgA3M8p//Lazrm0Nfa77T9cRXzQ6KhLrB08V39n1+00d1dvuTWznJslkmdg==", + "version": "55.0.16", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-55.0.16.tgz", + "integrity": "sha512-eeGs3439ewO/Q56Pzg3qbAVZSE0oH/R7XW9VCXI59k0m78ZIYbBtPT4PMFL/+sBgRkXm546Lq/DFcJQPTOfXJg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -8442,28 +7810,28 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "55.0.26", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.26.tgz", - "integrity": "sha512-Ud9gpeGMF5RIL42LXvCw3k3mWK8rf/P2wu+Yrzz9Do1kcFKZeT9Vy2D/xukjdr/Xw+ELba87ThOot17GsPiWjw==", + "version": "55.0.32", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.32.tgz", + "integrity": "sha512-fq+/yUYBVw5ZudT4igNyJ3WaF17R39iS7EZlrkfHkLI7Y1kmUlivabwKviLoAfepJOKjKODKpViti9EPfmG3SQ==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~55.0.15", - "@expo/config-plugins": "~55.0.8", + "@expo/config": "~55.0.17", + "@expo/config-plugins": "~55.0.10", "@expo/devcert": "^1.2.1", - "@expo/env": "~2.1.1", - "@expo/image-utils": "^0.8.13", - "@expo/json-file": "^10.0.13", - "@expo/log-box": "55.0.11", - "@expo/metro": "~55.1.0", - "@expo/metro-config": "~55.0.17", - "@expo/osascript": "^2.4.2", - "@expo/package-manager": "^1.10.4", - "@expo/plist": "^0.5.2", - "@expo/prebuild-config": "^55.0.16", - "@expo/require-utils": "^55.0.4", - "@expo/router-server": "^55.0.15", - "@expo/schema-utils": "^55.0.3", + "@expo/env": "~2.1.2", + "@expo/image-utils": "^0.8.14", + "@expo/json-file": "^10.0.15", + "@expo/log-box": "55.0.12", + "@expo/metro": "~55.1.1", + "@expo/metro-config": "~55.0.23", + "@expo/osascript": "^2.4.4", + "@expo/package-manager": "^1.10.5", + "@expo/plist": "^0.5.4", + "@expo/prebuild-config": "^55.0.18", + "@expo/require-utils": "^55.0.5", + "@expo/router-server": "^55.0.18", + "@expo/schema-utils": "^55.0.4", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", @@ -8479,7 +7847,7 @@ "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", - "expo-server": "^55.0.8", + "expo-server": "^55.0.11", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", @@ -8523,103 +7891,189 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "55.0.15", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.15.tgz", - "integrity": "sha512-6LksYO4Pg13qroL138KfUebt/x/EO07zVhdyT/nTgcxnpn6CS4ecTl3DciSKhxbaH+0BVLdANkxYeGdp43TMwQ==", + "version": "55.0.18", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.18.tgz", + "integrity": "sha512-W0VsvIiR48OvdlAOUlag4qspGYT/DV4srfYowlbYxwZh5Qw0MjiZAID4Zt7F0qynGZZxx8OZPpFhIX7XsqtRmg==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^55.0.10", + "@expo/metro-runtime": "^55.0.11", "expo": "*", - "expo-constants": "^55.0.15", - "expo-font": "^55.0.6", + "expo-constants": "^55.0.16", + "expo-font": "^55.0.8", "expo-router": "*", - "expo-server": "^55.0.8", + "expo-server": "^55.0.11", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, - "peerDependenciesMeta": { - "@expo/metro-runtime": { - "optional": true - }, - "expo-router": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-server-dom-webpack": { - "optional": true - } - } - }, - "node_modules/expo/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", + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/cli/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/node_modules/@expo/metro-config": { + "version": "55.0.23", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.23.tgz", + "integrity": "sha512-Mkw3Ss/1LFlafH3iie3r9E13yKMyJgZqGTEkGviGf6LYp51eY5fR8ATbXrNsH69wVc2z+ty4lT/8lEA18YJv7g==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~55.0.17", + "@expo/env": "~2.1.2", + "@expo/json-file": "~10.0.15", + "@expo/metro": "~55.1.1", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.32.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.6.tgz", + "integrity": "sha512-qfRXsHGeucT5c6mK+8Q7v4Ly3zmygfVmFlEtkiq7q07W1OTreld6nib4rJ/DBEeNiKBoBTuHjWliYGNuDjLFQA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.83.6" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/expo/node_modules/@react-native/babel-preset": { + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.6.tgz", + "integrity": "sha512-4/fXFDUvGOObETZq4+SUFkafld6OGgQWut5cQiqVghlhCB5z/p2lVhPgEUr/aTxTzeS3AmN+ztC+GpYPQ7tsTw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.83.6", + "babel-plugin-syntax-hermes-parser": "0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@react-native/debugger-frontend": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.6.tgz", - "integrity": "sha512-TyWXEpAjVundrc87fPWg91piOUg75+X9iutcfDe7cO3NrAEYCsl7Z09rKHuiAGkxfG9/rFD13dPsYIixUFkSFA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 20.19.4" + "@babel/core": "*" } }, - "node_modules/expo/node_modules/@react-native/debugger-shell": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.6.tgz", - "integrity": "sha512-684TJMBCU0l0ZjJWzrnK0HH+ERaM9KLyxyArE1k7BrP+gVl4X9GO0Pi94RoInOxvW/nyV65sOU6Ip1F3ygS0cg==", + "node_modules/expo/node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": ">= 20.19.4" + "hermes-parser": "0.32.0" } }, - "node_modules/expo/node_modules/@react-native/dev-middleware": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.6.tgz", - "integrity": "sha512-22xoddLTelpcVnF385SNH2hdP7X2av5pu7yRl/WnM5jBznbcl0+M9Ce94cj+WVeomsoUF/vlfuB0Ooy+RMlRiA==", + "node_modules/expo/node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/expo/node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "license": "MIT", "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.83.6", - "@react-native/debugger-shell": "0.83.6", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">= 20.19.4" + "hermes-estree": "0.32.0" } }, "node_modules/expo/node_modules/accepts": { @@ -8635,6 +8089,63 @@ "node": ">= 0.6" } }, + "node_modules/expo/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.1.tgz", + "integrity": "sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.32.1" + } + }, + "node_modules/expo/node_modules/babel-preset-expo": { + "version": "55.0.22", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.22.tgz", + "integrity": "sha512-Se6kPnvCNN13jJVIa6JJvlmImVoVRzu9stagAbivCPcfrq2VNrsEiYpJZ1+H32kXinKW/y797/wctGuxPy0APw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.83.6", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^55.0.19", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, "node_modules/expo/node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -8650,10 +8161,35 @@ "node": ">=8" } }, + "node_modules/expo/node_modules/expo-asset": { + "version": "55.0.17", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.17.tgz", + "integrity": "sha512-pK9HHJuFqjE8kDUcbMFsZj3Cz8WdXpvZHZmYl7ouFQp59P83BvHln6VnqPDGlO+/4929G0Lm8ZUzbONuNRhi9w==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.8.14", + "expo-constants": "~55.0.16" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-keep-awake": { + "version": "55.0.8", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.8.tgz", + "integrity": "sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo/node_modules/expo-modules-core": { - "version": "55.0.23", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-55.0.23.tgz", - "integrity": "sha512-IGWT5N9MoV4zgWyrv686bElnKhzhE7E6pSazhaBNh3vgViAah5nnAz2o5h5YoUMR2B+ZTdHumRbGHN6gHLgwPA==", + "version": "55.0.25", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-55.0.25.tgz", + "integrity": "sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -8669,6 +8205,21 @@ } } }, + "node_modules/expo/node_modules/hermes-estree": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.1.tgz", + "integrity": "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==", + "license": "MIT" + }, + "node_modules/expo/node_modules/hermes-parser": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.1.tgz", + "integrity": "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.1" + } + }, "node_modules/expo/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8699,37 +8250,10 @@ "node": ">= 0.6" } }, - "node_modules/expo/node_modules/react-native-worklets": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.1.tgz", - "integrity": "sha512-oWP/lStsAHU6oYCaWDXrda/wOHVdhusQJz1e6x9gPnXdFf4ndNDAOtWCmk2zGrAnlapfyA3rM6PCQq94mPg9cw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/preset-typescript": "^7.27.1", - "convert-source-map": "^2.0.0", - "semver": "^7.7.3" - }, - "peerDependencies": { - "@babel/core": "*", - "@react-native/metro-config": "*", - "react": "*", - "react-native": "0.81 - 0.85" - } - }, "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==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9433,18 +8957,18 @@ "license": "MIT" }, "node_modules/hermes-estree": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.1.tgz", - "integrity": "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", "license": "MIT" }, "node_modules/hermes-parser": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.1.tgz", - "integrity": "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "license": "MIT", "dependencies": { - "hermes-estree": "0.32.1" + "hermes-estree": "0.32.0" } }, "node_modules/hoist-non-react-statics": { @@ -10665,6 +10189,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10685,6 +10212,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10705,6 +10235,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10725,6 +10258,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11006,9 +10542,9 @@ } }, "node_modules/metro": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.6.tgz", - "integrity": "sha512-pbdndsAZ2F/ceopDdhVbttpa/hfLzXPJ/husc+QvQ33R0D9UXJKzTn5+OzOXx4bpQNtAKF2bY88cCI3Zl44xDQ==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.7.tgz", + "integrity": "sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -11019,7 +10555,6 @@ "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", - "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", @@ -11032,18 +10567,18 @@ "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.83.6", - "metro-cache": "0.83.6", - "metro-cache-key": "0.83.6", - "metro-config": "0.83.6", - "metro-core": "0.83.6", - "metro-file-map": "0.83.6", - "metro-resolver": "0.83.6", - "metro-runtime": "0.83.6", - "metro-source-map": "0.83.6", - "metro-symbolicate": "0.83.6", - "metro-transform-plugins": "0.83.6", - "metro-transform-worker": "0.83.6", + "metro-babel-transformer": "0.83.7", + "metro-cache": "0.83.7", + "metro-cache-key": "0.83.7", + "metro-config": "0.83.7", + "metro-core": "0.83.7", + "metro-file-map": "0.83.7", + "metro-resolver": "0.83.7", + "metro-runtime": "0.83.7", + "metro-source-map": "0.83.7", + "metro-symbolicate": "0.83.7", + "metro-transform-plugins": "0.83.7", + "metro-transform-worker": "0.83.7", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", @@ -11060,15 +10595,15 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.6.tgz", - "integrity": "sha512-1AnuazBpzY3meRMr04WUw14kRBkV0W3Ez+AA75FAeNpRyWNN5S3M3PHLUbZw7IXq7ZeOzceyRsHStaFrnWd+8w==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.7.tgz", + "integrity": "sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", - "metro-cache-key": "0.83.6", + "metro-cache-key": "0.83.7", "nullthrows": "^1.1.1" }, "engines": { @@ -11091,24 +10626,24 @@ } }, "node_modules/metro-cache": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.6.tgz", - "integrity": "sha512-DpvZE32feNkqfZkI4Fic7YI/Kw8QP9wdl1rC4YKPrA77wQbI9vXbxjmfkCT/EGwBTFOPKqvIXo+H3BNe93YyiQ==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.7.tgz", + "integrity": "sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.83.6" + "metro-core": "0.83.7" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-cache-key": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.6.tgz", - "integrity": "sha512-5gdK4PVpgNOHi7xCGrgesNP1AuOA2TiPqpcirGXZi4RLLzX1VMowpkgTVtBfpQQCqWoosQF9yrSo9/KDQg1eBg==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.7.tgz", + "integrity": "sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -11118,18 +10653,18 @@ } }, "node_modules/metro-config": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.6.tgz", - "integrity": "sha512-G5622400uNtnAMlppEA5zkFAZltEf7DSGhOu09BkisCxOlVMWfdosD/oPyh4f2YVQsc1MBYyp4w6OzbExTYarg==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.7.tgz", + "integrity": "sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==", "license": "MIT", "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.83.6", - "metro-cache": "0.83.6", - "metro-core": "0.83.6", - "metro-runtime": "0.83.6", + "metro": "0.83.7", + "metro-cache": "0.83.7", + "metro-core": "0.83.7", + "metro-runtime": "0.83.7", "yaml": "^2.6.1" }, "engines": { @@ -11137,23 +10672,23 @@ } }, "node_modules/metro-core": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.6.tgz", - "integrity": "sha512-l+yQ2fuIgR//wszUlMrrAa9+Z+kbKazd0QOh0VQY7jC4ghb7yZBBSla/UMYRBZZ6fPg9IM+wD3+h+37a5f9etw==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.7.tgz", + "integrity": "sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.83.6" + "metro-resolver": "0.83.7" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-file-map": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.6.tgz", - "integrity": "sha512-Jg3oN604C7GWbQwFAUXt8KsbMXeKfsxbZ5HFy4XFM3ggTS+ja9QgUmq9B613kgXv3G4M6rwiI6cvh9TRly4x3w==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.7.tgz", + "integrity": "sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -11171,9 +10706,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.6.tgz", - "integrity": "sha512-Vx3/Ne9Q+EIEDLfKzZUOtn/rxSNa/QjlYxc42nvK4Mg8mB6XUgd3LXX5ZZVq7lzQgehgEqLrbgShJPGfeF8PnQ==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.7.tgz", + "integrity": "sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -11184,9 +10719,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.6.tgz", - "integrity": "sha512-lAwR/FsT1uJ5iCt4AIsN3boKfJ88aN8bjvDT5FwBS0tKeKw4/sbdSTWlFxc7W/MUTN5RekJ3nQkJRIWsvs28tA==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.7.tgz", + "integrity": "sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -11196,9 +10731,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.6.tgz", - "integrity": "sha512-WQPua1G2VgYbwRn6vSKxOhTX7CFbSf/JdUu6Nd8bZnPXckOf7HQ2y51NXNQHoEsiuawathrkzL8pBhv+zgZFmg==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.7.tgz", + "integrity": "sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -11209,18 +10744,18 @@ } }, "node_modules/metro-source-map": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.6.tgz", - "integrity": "sha512-AqJbOMMpeyyM4iNI91pchqDIszzNuuHApEhg6OABqZ+9mjLEqzcIEQ/fboZ7x74fNU5DBd2K36FdUQYPqlGClA==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.7.tgz", + "integrity": "sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.83.6", + "metro-symbolicate": "0.83.7", "nullthrows": "^1.1.1", - "ob1": "0.83.6", + "ob1": "0.83.7", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -11229,14 +10764,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.6.tgz", - "integrity": "sha512-4nvkmv9T7ozhprlPwk/+xm0SVPsxly5kYyMHdNaOlFemFz4df9BanvD46Ac6OISu/4Idinzfk2KVb++6OfzPAQ==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.7.tgz", + "integrity": "sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.83.6", + "metro-source-map": "0.83.7", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -11249,9 +10784,9 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.6.tgz", - "integrity": "sha512-V+zoY2Ul0v0BW6IokJkTud3raXmDdbdwkUQ/5eiSoy0jKuKMhrDjdH+H5buCS5iiJdNbykOn69Eip+Sqymkodg==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.7.tgz", + "integrity": "sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -11266,9 +10801,9 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.6.tgz", - "integrity": "sha512-G5kDJ/P0ZTIf57t3iyAd5qIXbj2Wb1j7WtIDh82uTFQHe2Mq2SO9aXG9j1wI+kxZlIe58Z22XEXIKMl89z0ibQ==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.7.tgz", + "integrity": "sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -11276,13 +10811,13 @@ "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.83.6", - "metro-babel-transformer": "0.83.6", - "metro-cache": "0.83.6", - "metro-cache-key": "0.83.6", - "metro-minify-terser": "0.83.6", - "metro-source-map": "0.83.6", - "metro-transform-plugins": "0.83.6", + "metro": "0.83.7", + "metro-babel-transformer": "0.83.7", + "metro-cache": "0.83.7", + "metro-cache-key": "0.83.7", + "metro-minify-terser": "0.83.7", + "metro-source-map": "0.83.7", + "metro-transform-plugins": "0.83.7", "nullthrows": "^1.1.1" }, "engines": { @@ -11442,9 +10977,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -11593,9 +11128,9 @@ } }, "node_modules/npm-package-arg/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11623,9 +11158,9 @@ "license": "MIT" }, "node_modules/ob1": { - "version": "0.83.6", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.6.tgz", - "integrity": "sha512-m/xZYkwcjo6UqLMrUICEB3iHk7Bjt3RSR7KXMi6Y1MO/kGkPhoRmfUDF6KAan3rLAZ7ABRqnQyKUTwaqZgUV4w==", + "version": "0.83.7", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.7.tgz", + "integrity": "sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -12171,9 +11706,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -12190,7 +11725,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -12583,19 +12118,19 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.83.4.tgz", - "integrity": "sha512-H5Wco3UJyY6zZsjoBayY8RM9uiAEQ3FeG4G2NAt+lr9DO43QeqPlVe9xxxYEukMkEmeIhNjR70F6bhXuWArOMQ==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.83.6.tgz", + "integrity": "sha512-H513+8VzviNFXOdPnStRzX9S3/jiJGg++QZ1zd+ROyAvBEKqFqKUPHH0d82y3QyRPct5qKjdOa7J6vNehCvXYA==", "license": "MIT", "dependencies": { "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.83.4", - "@react-native/codegen": "0.83.4", - "@react-native/community-cli-plugin": "0.83.4", - "@react-native/gradle-plugin": "0.83.4", - "@react-native/js-polyfills": "0.83.4", - "@react-native/normalize-colors": "0.83.4", - "@react-native/virtualized-lists": "0.83.4", + "@react-native/assets-registry": "0.83.6", + "@react-native/codegen": "0.83.6", + "@react-native/community-cli-plugin": "0.83.6", + "@react-native/gradle-plugin": "0.83.6", + "@react-native/js-polyfills": "0.83.6", + "@react-native/normalize-colors": "0.83.6", + "@react-native/virtualized-lists": "0.83.6", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -12609,8 +12144,8 @@ "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", - "metro-runtime": "^0.83.3", - "metro-source-map": "^0.83.3", + "metro-runtime": "^0.83.6", + "metro-source-map": "^0.83.6", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", @@ -13091,9 +12626,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.2.tgz", - "integrity": "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.4.tgz", + "integrity": "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", @@ -13212,46 +12747,10 @@ "node": ">=10" } }, - "node_modules/react-native/node_modules/@react-native/codegen": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.4.tgz", - "integrity": "sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.25.3", - "glob": "^7.1.1", - "hermes-parser": "0.32.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">= 20.19.4" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/react-native/node_modules/@react-native/js-polyfills": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.83.4.tgz", - "integrity": "sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/react-native/node_modules/@react-native/normalize-colors": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.4.tgz", - "integrity": "sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw==", - "license": "MIT" - }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.83.4.tgz", - "integrity": "sha512-vNF/8kokMW8JEjG4n+j7veLTjHRRABlt4CaTS6+wtqzvWxCJHNIC8fhCqrDPn9fIn8sNePd8DyiFVX5L9TBBRA==", + "version": "0.83.6", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.83.6.tgz", + "integrity": "sha512-gNSFXeb4P7qHtauLvl+zESroULIyX6Ltpvau3dhwy/QmfanBv0KUcrIU/7aVXxtWcXgp+54oWJyu2LIrsZ9+LQ==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -13310,21 +12809,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-native/node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "license": "MIT" - }, - "node_modules/react-native/node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.32.0" - } - }, "node_modules/react-native/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -15270,9 +14754,9 @@ } }, "node_modules/whatwg-url-minimum": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", - "integrity": "sha512-u2FNVjFVFZhdjb502KzXy1gKn1mEisQRJssmSJT8CPhZdZa0AP6VCbWlXERKyGu0l09t0k50FiDiralpGhBxgA==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.2.tgz", + "integrity": "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==", "license": "MIT" }, "node_modules/which": { diff --git a/package.json b/package.json index a86ac3c..a5dc826 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "update-avicommons": "tsx ./scripts/update-avicommons.ts" }, "dependencies": { - "@expo/metro-runtime": "~55.0.6", + "@expo/metro-runtime": "~55.0.11", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^15.0.2", "@lodev09/react-native-true-sheet": "^3.9.2", @@ -27,31 +27,31 @@ "@tanstack/react-query": "^5.87.1", "@types/lodash": "^4.17.20", "dayjs": "^1.11.19", - "expo": "~55.0.15", + "expo": "~55.0.26", "expo-blur": "~55.0.14", - "expo-constants": "~55.0.9", - "expo-dev-client": "~55.0.27", + "expo-constants": "~55.0.16", + "expo-dev-client": "~55.0.35", "expo-document-picker": "~55.0.13", - "expo-file-system": "~55.0.11", - "expo-font": "~55.0.4", - "expo-glass-effect": "~55.0.8", + "expo-file-system": "~55.0.22", + "expo-font": "~55.0.8", + "expo-glass-effect": "~55.0.11", "expo-haptics": "~55.0.14", - "expo-image": "~55.0.6", - "expo-linking": "~55.0.13", - "expo-location": "~55.1.8", - "expo-router": "~55.0.12", - "expo-splash-screen": "~55.0.18", - "expo-sqlite": "~55.0.15", - "expo-status-bar": "~55.0.4", - "expo-symbols": "~55.0.5", - "expo-system-ui": "~55.0.15", - "expo-updates": "~55.0.20", - "expo-web-browser": "~55.0.14", + "expo-image": "~55.0.11", + "expo-linking": "~55.0.15", + "expo-location": "~55.1.10", + "expo-router": "~55.0.16", + "expo-splash-screen": "~55.0.21", + "expo-sqlite": "~55.0.16", + "expo-status-bar": "~55.0.6", + "expo-symbols": "~55.0.9", + "expo-system-ui": "~55.0.18", + "expo-updates": "~55.0.24", + "expo-web-browser": "~55.0.16", "lodash": "^4.17.21", "nativewind": "^4.1.23", "react": "19.2.0", "react-dom": "19.2.0", - "react-native": "0.83.4", + "react-native": "0.83.6", "react-native-gesture-handler": "~2.30.0", "react-native-popover-view": "^6.1.0", "react-native-reanimated": "4.2.1", @@ -61,7 +61,7 @@ "react-native-toast-message": "^2.3.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.0", - "react-native-worklets": "0.7.2", + "react-native-worklets": "0.7.4", "suncalc": "^1.9.0", "tailwindcss": "^3.4.17", "twrnc": "^4.9.1", @@ -69,10 +69,11 @@ }, "devDependencies": { "@types/react": "~19.2.10", + "@types/sharp": "^0.31.1", "@types/suncalc": "^1.9.2", "dotenv-cli": "^11.0.0", "eslint": "^9.25.0", - "eslint-config-expo": "~55.0.0", + "eslint-config-expo": "~55.0.1", "tsx": "^4.21.0", "typescript": "~5.9.2" }, From e5b5486214b43340cf1225af843bf7aedc8d5d6b Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 12:30:35 -0700 Subject: [PATCH 17/23] Update SearchSheet.tsx --- components/SearchSheet.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/components/SearchSheet.tsx b/components/SearchSheet.tsx index 0bf18cd..1a155ec 100644 --- a/components/SearchSheet.tsx +++ b/components/SearchSheet.tsx @@ -3,6 +3,7 @@ import { getSavedPlaces, searchHotspots } from "@/lib/database"; import tw from "@/lib/tw"; import { Hotspot, SavedPlace } from "@/lib/types"; import { calculateDistance } from "@/lib/utils"; +import { useMapStore } from "@/stores/mapStore"; import { useQuery } from "@tanstack/react-query"; import { FlashList } from "@shopify/flash-list"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -38,6 +39,12 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect const [debouncedQuery, setDebouncedQuery] = useState(""); const dismissRef = useRef<(() => Promise) | null>(null); const { location } = useLocation(isOpen); + const mapCenter = useMapStore((state) => state.mapCenter); + + // Sort origin falls back to map center so duplicate-named saved places (e.g. + // two "Airport"s) still order nearest-first without GPS. The displayed + // distance label stays gated on real location only — see `withDistance`. + const originPoint = location ?? mapCenter ?? null; useEffect(() => { if (!isOpen) { @@ -82,13 +89,16 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect const matched = trimmed.length >= MIN_QUERY ? savedPlaces.filter((place) => place.name.toLowerCase().includes(trimmed)) : []; - const decorated = matched.map(withDistance); + const decorated = matched.map((place) => ({ + place: withDistance(place), + sortDistance: originPoint ? calculateDistance(originPoint.lat, originPoint.lng, place.lat, place.lng) : null, + })); decorated.sort((a, b) => { - if (a.distance !== undefined && b.distance !== undefined) return a.distance - b.distance; - return a.name.localeCompare(b.name); + if (a.sortDistance !== null && b.sortDistance !== null) return a.sortDistance - b.sortDistance; + return a.place.name.localeCompare(b.place.name); }); - return decorated; - }, [savedPlaces, query, withDistance]); + return decorated.map((d) => d.place); + }, [savedPlaces, query, withDistance, originPoint]); // Hotspots come back alphabetical from SQL; surface prefix matches first so // the most likely target sits at the top. From d2fa83ffab2c72bf61f339a6ed0f8d9421a455c3 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 14:38:23 -0700 Subject: [PATCH 18/23] General cleanup --- components/HotspotDialog.tsx | 2 +- components/HotspotList.tsx | 15 +- components/PlaceItem.tsx | 8 +- package-lock.json | 320 ----------------------------------- package.json | 1 - 5 files changed, 16 insertions(+), 330 deletions(-) diff --git a/components/HotspotDialog.tsx b/components/HotspotDialog.tsx index 13be887..d71e55a 100644 --- a/components/HotspotDialog.tsx +++ b/components/HotspotDialog.tsx @@ -96,7 +96,7 @@ function HotspotDialogContent({ isOpen, hotspotId, onClose }: HotspotDialogProps const handleViewDetails = () => { if (!hotspot) return; - const url = `https://ebird.org/hotspot/${hotspot.id}`; + const url = `https://ebird.org/hotspot/${hotspot.id}/about`; Linking.openURL(url).catch(() => { Alert.alert("Error", "Could not open eBird hotspot page"); }); diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index bc8ea31..bd4f28f 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -101,9 +101,17 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect }); const savedHotspotsSet = useMemo(() => new Set(savedHotspots.map((s) => s.hotspot_id)), [savedHotspots]); + // Fetch uses padded bounds (shared cache with the map), but clip the list back to the + // unpadded viewport so it shows only what's actually visible on the map — not the + // off-screen margin hotspots the padding loads ahead of a pan. const candidateHotspots = useMemo( - () => hotspots.filter((hotspot) => !showSavedOnly || savedHotspotsSet.has(hotspot.id)), - [hotspots, savedHotspotsSet, showSavedOnly] + () => + hotspots.filter( + (hotspot) => + (!showSavedOnly || savedHotspotsSet.has(hotspot.id)) && + (!snapshotBounds || isWithinBounds(hotspot.lat, hotspot.lng, snapshotBounds)) + ), + [hotspots, savedHotspotsSet, showSavedOnly, snapshotBounds] ); const targetRichFilter = useTargetRichHotspots(candidateHotspots.map((hotspot) => hotspot.id), { @@ -122,8 +130,7 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect // target-rich filter is active, since they have no species data to qualify. const placesInView = useMemo(() => { if (!snapshotBounds) return []; - const padded = padBoundsBySize(snapshotBounds); - return savedPlaces.filter((place) => isWithinBounds(place.lat, place.lng, padded)); + return savedPlaces.filter((place) => isWithinBounds(place.lat, place.lng, snapshotBounds)); }, [savedPlaces, snapshotBounds]); const hasUserLocation = location !== null; diff --git a/components/PlaceItem.tsx b/components/PlaceItem.tsx index 5f63665..2b1e31f 100644 --- a/components/PlaceItem.tsx +++ b/components/PlaceItem.tsx @@ -24,14 +24,14 @@ const PlaceItem = React.memo( cancelable={false} style={({ pressed }) => [tw`flex-row items-center px-4 py-3 border-b border-gray-200/50`, pressed && tw`opacity-70`]} > - {item.name} - - {item.notes ? item.notes : "Place"} - + + + {item.notes ? item.notes : "Saved Pin"} + {item.distance !== undefined && ( {formatDistance(item.distance, null)} diff --git a/package-lock.json b/package-lock.json index cf0e98f..389196f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "expo-updates": "~55.0.24", "expo-web-browser": "~55.0.16", "lodash": "^4.17.21", - "nativewind": "^4.1.23", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.6", @@ -5200,12 +5199,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-timsort": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", - "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", - "license": "MIT" - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6009,19 +6002,6 @@ "node": ">= 10" } }, - "node_modules/comment-json": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", - "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", - "license": "MIT", - "dependencies": { - "array-timsort": "^1.0.3", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -11010,23 +10990,6 @@ "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/nativewind": { - "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.2.3" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "tailwindcss": ">3.3.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -12175,289 +12138,6 @@ } } }, - "node_modules/react-native-css-interop": { - "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", - "semver": "^7.6.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": ">=18", - "react-native": "*", - "react-native-reanimated": ">=3.6.2", - "tailwindcss": "~3" - }, - "peerDependenciesMeta": { - "react-native-safe-area-context": { - "optional": true - }, - "react-native-svg": { - "optional": true - } - } - }, - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/react-native-gesture-handler": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.1.tgz", diff --git a/package.json b/package.json index a5dc826..f2c9470 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "expo-updates": "~55.0.24", "expo-web-browser": "~55.0.16", "lodash": "^4.17.21", - "nativewind": "^4.1.23", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.6", From 8d8a4a5135a4b68fabc8e0d563ccc9c3a926eff3 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 14:40:07 -0700 Subject: [PATCH 19/23] Remove scroll restoration --- components/HotspotList.tsx | 9 --------- hooks/useScrollRestore.ts | 33 --------------------------------- 2 files changed, 42 deletions(-) delete mode 100644 hooks/useScrollRestore.ts diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index bd4f28f..5d0a7a6 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -1,6 +1,5 @@ import { useActiveFilterCount } from "@/hooks/useActiveFilterCount"; import { useLocation } from "@/hooks/useLocation"; -import { useScrollRestore } from "@/hooks/useScrollRestore"; import { useTargetRichHotspots } from "@/hooks/useTargetRichHotspots"; import { getHotspotsWithinBounds, getSavedHotspots, getSavedPlaces } from "@/lib/database"; import tw from "@/lib/tw"; @@ -167,12 +166,6 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect const isLocationLoading = isLoadingUserLocation && permissionStatus === "granted" && location === null; - const resetKey = useMemo(() => { - if (!snapshotBounds) return 0; - return Math.round((snapshotBounds.west + snapshotBounds.south + snapshotBounds.east + snapshotBounds.north) * 1000); - }, [snapshotBounds]); - const { listRef, onScroll } = useScrollRestore(isOpen, resetKey); - const handleSelectHotspot = useCallback( async (hotspot: Hotspot & { distance?: number }) => { await dismissRef.current?.(); @@ -274,7 +267,6 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect dimmed > diff --git a/hooks/useScrollRestore.ts b/hooks/useScrollRestore.ts deleted file mode 100644 index 0cc5f15..0000000 --- a/hooks/useScrollRestore.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useCallback, useEffect, useRef } from "react"; -import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"; - -export function useScrollRestore(isOpen: boolean, resetKey: number) { - const listRef = useRef(null); - const scrollOffsetRef = useRef(0); - const prevResetKeyRef = useRef(resetKey); - - // Restore scroll position when modal opens - useEffect(() => { - if (isOpen && scrollOffsetRef.current > 0) { - const timeoutId = setTimeout(() => { - listRef.current?.scrollToOffset({ offset: scrollOffsetRef.current, animated: false }); - }, 100); - return () => clearTimeout(timeoutId); - } - }, [isOpen]); - - // Reset scroll when resetKey changes - useEffect(() => { - if (resetKey !== prevResetKeyRef.current) { - prevResetKeyRef.current = resetKey; - scrollOffsetRef.current = 0; - listRef.current?.scrollToOffset({ offset: 0, animated: false }); - } - }, [resetKey]); - - const onScroll = useCallback((e: NativeSyntheticEvent) => { - scrollOffsetRef.current = e.nativeEvent.contentOffset.y; - }, []); - - return { listRef, onScroll }; -} From 82f61e13b42c7b016ab758fb894828d0da1378f4 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 14:54:58 -0700 Subject: [PATCH 20/23] Include saved hotspot icon in lists --- components/HotspotItem.tsx | 17 ++++++++++++++--- components/HotspotList.tsx | 4 ++-- components/SearchSheet.tsx | 15 ++++++++++++--- lib/hotspotIconImages.ts | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 lib/hotspotIconImages.ts diff --git a/components/HotspotItem.tsx b/components/HotspotItem.tsx index ca4b09b..139dd7f 100644 --- a/components/HotspotItem.tsx +++ b/components/HotspotItem.tsx @@ -1,17 +1,19 @@ import tw from "@/lib/tw"; import { Hotspot } from "@/lib/types"; +import { getSavedHotspotIconImage } from "@/lib/hotspotIconImages"; import { formatDistance, getMarkerColor } from "@/lib/utils"; import { Ionicons } from "@expo/vector-icons"; import React, { useCallback } from "react"; -import { Pressable, Text, View } from "react-native"; +import { Image, Pressable, Text, View } from "react-native"; type HotspotItemProps = { item: Hotspot & { distance?: number }; onSelect: (hotspot: Hotspot & { distance?: number }) => void; + isSaved?: boolean; }; const HotspotItem = React.memo( - ({ item, onSelect }: HotspotItemProps) => { + ({ item, onSelect, isSaved = false }: HotspotItemProps) => { const handlePress = useCallback(() => { onSelect(item); }, [item, onSelect]); @@ -28,7 +30,15 @@ const HotspotItem = React.memo( {item.name} - + {isSaved ? ( + + ) : ( + + )} {item.species} species @@ -48,6 +58,7 @@ const HotspotItem = React.memo( prevProps.item.species === nextProps.item.species && prevProps.item.distance === nextProps.item.distance && prevProps.item.country === nextProps.item.country && + prevProps.isSaved === nextProps.isSaved && prevProps.onSelect === nextProps.onSelect ); } diff --git a/components/HotspotList.tsx b/components/HotspotList.tsx index 5d0a7a6..2dc73ab 100644 --- a/components/HotspotList.tsx +++ b/components/HotspotList.tsx @@ -185,11 +185,11 @@ export default function HotspotList({ isOpen, onClose, onSelectHotspot, onSelect const renderItem = useCallback( ({ item }: { item: ListRow }) => item.kind === "hotspot" ? ( - + ) : ( ), - [handleSelectHotspot, handleSelectPlace] + [handleSelectHotspot, handleSelectPlace, savedHotspotsSet] ); const keyExtractor = useCallback((item: ListRow) => `${item.kind}:${item.id}`, []); diff --git a/components/SearchSheet.tsx b/components/SearchSheet.tsx index 1a155ec..763ae78 100644 --- a/components/SearchSheet.tsx +++ b/components/SearchSheet.tsx @@ -1,5 +1,5 @@ import { useLocation } from "@/hooks/useLocation"; -import { getSavedPlaces, searchHotspots } from "@/lib/database"; +import { getSavedHotspots, getSavedPlaces, searchHotspots } from "@/lib/database"; import tw from "@/lib/tw"; import { Hotspot, SavedPlace } from "@/lib/types"; import { calculateDistance } from "@/lib/utils"; @@ -66,6 +66,15 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect gcTime: 10 * 60 * 1000, }); + const { data: savedHotspots = [] } = useQuery({ + queryKey: ["savedHotspots"], + queryFn: getSavedHotspots, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); + + const savedHotspotsSet = useMemo(() => new Set(savedHotspots.map((s) => s.hotspot_id)), [savedHotspots]); + const hotspotQueryEnabled = isOpen && debouncedQuery.length >= MIN_QUERY; const { data: hotspotResults = [] } = useQuery({ queryKey: ["searchHotspots", debouncedQuery], @@ -160,9 +169,9 @@ export default function SearchSheet({ isOpen, onClose, onSelectHotspot, onSelect if (item.type === "place") { return ; } - return ; + return ; }, - [handleSelectHotspot, handleSelectPlace] + [handleSelectHotspot, handleSelectPlace, savedHotspotsSet] ); const keyExtractor = useCallback((item: SearchRow) => item.key, []); diff --git a/lib/hotspotIconImages.ts b/lib/hotspotIconImages.ts new file mode 100644 index 0000000..a9f25f4 --- /dev/null +++ b/lib/hotspotIconImages.ts @@ -0,0 +1,22 @@ +import { getMarkerColorIndex } from "./utils"; + +// Saved-hotspot marker PNGs keyed by species-color index. These are the circle +// markers (colored disc + star) used on the map; the hotspot list reuses them +// so a saved row's icon matches its map marker. +const savedHotspotImages: Record = { + 0: require("@/assets/markers/saved-hotspot-0.png"), + 1: require("@/assets/markers/saved-hotspot-1.png"), + 2: require("@/assets/markers/saved-hotspot-2.png"), + 3: require("@/assets/markers/saved-hotspot-3.png"), + 4: require("@/assets/markers/saved-hotspot-4.png"), + 5: require("@/assets/markers/saved-hotspot-5.png"), + 6: require("@/assets/markers/saved-hotspot-6.png"), + 7: require("@/assets/markers/saved-hotspot-7.png"), + 8: require("@/assets/markers/saved-hotspot-8.png"), + 9: require("@/assets/markers/saved-hotspot-9.png"), +}; + +export function getSavedHotspotIconImage(species: number): any { + const index = getMarkerColorIndex(species || 0); + return savedHotspotImages[index] ?? savedHotspotImages[0]; +} From 06fac3d209f72bb04c1184f69dc6ff2a0d18ebeb Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 15:02:29 -0700 Subject: [PATCH 21/23] Delay loading badge display --- components/Mapbox.tsx | 7 ++++++- hooks/useDelayedFlag.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 hooks/useDelayedFlag.ts diff --git a/components/Mapbox.tsx b/components/Mapbox.tsx index 09e9634..26c2382 100644 --- a/components/Mapbox.tsx +++ b/components/Mapbox.tsx @@ -1,3 +1,4 @@ +import { useDelayedFlag } from "@/hooks/useDelayedFlag"; import { useTargetRichHotspots } from "@/hooks/useTargetRichHotspots"; import { getHotspotsWithinBounds, getSavedHotspots, getSavedPlaces } from "@/lib/database"; import { @@ -106,6 +107,8 @@ export type MapboxMapRef = { const THROTTLE_DELAY = 750; const THROTTLE_DELAY_WITH_OPEN_HOTSPOT = 250; // Load hotspots faster when jumping to a hotspot from list modal const SETTLED_BOUNDS_DELAY = 300; +// Only surface the "Filtering hotspots..." badge if filtering outlasts this; incremental pans resolve faster and shouldn't flicker it. +const TARGET_RICH_BADGE_DELAY = 350; const MIN_ZOOM = 8; const DEFAULT_USER_ZOOM = 14; const DEFAULT_HOTSPOT_ZOOM = 13; @@ -373,6 +376,8 @@ const MapboxMap = forwardRef( const isTargetRichLoading = targetRichFilter.isActive && (isInitialTargetRichFetch || unresolvedCandidateCount > 0 || targetRichFilter.isLoading); + // Delay the badge so brief filtering during incremental pans doesn't flash it; markers still use isTargetRichLoading directly. + const isTargetRichBadgeVisible = useDelayedFlag(isTargetRichLoading, TARGET_RICH_BADGE_DELAY); const displayedHotspots = useMemo(() => { if (showSavedOnly || targetRichFilter.isActive) { const resolvedHotspots = mapCandidateHotspots.filter((hotspot) => @@ -719,7 +724,7 @@ const MapboxMap = forwardRef( )} - {isTargetRichLoading && !isZoomedTooFarOut && ( + {isTargetRichBadgeVisible && !isZoomedTooFarOut && ( {Platform.OS === "ios" && isLiquidGlassAvailable() ? ( diff --git a/hooks/useDelayedFlag.ts b/hooks/useDelayedFlag.ts new file mode 100644 index 0000000..97fe06c --- /dev/null +++ b/hooks/useDelayedFlag.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef, useState } from "react"; + +/** + * Delays surfacing a transient `true` flag so brief flickers never render. + * + * The returned value only becomes `true` after `source` has stayed `true` + * continuously for `delayMs`. It clears immediately when `source` goes `false`, + * so quick on/off cycles (e.g. a loading badge during incremental map pans) + * never appear. Use to suppress loading indicators for work that usually + * resolves faster than the delay. + */ +export function useDelayedFlag(source: boolean, delayMs: number): boolean { + const [delayed, setDelayed] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + if (!source) { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setDelayed(false); + return; + } + + timeoutRef.current = setTimeout(() => { + timeoutRef.current = null; + setDelayed(true); + }, delayMs); + + return () => { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, [source, delayMs]); + + return delayed; +} From a75515f754f33a1c77eec38df97a01ddba62f8c1 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 15:02:34 -0700 Subject: [PATCH 22/23] Increment app version --- app.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.config.js b/app.config.js index 72c73e9..7773496 100644 --- a/app.config.js +++ b/app.config.js @@ -4,7 +4,7 @@ module.exports = ({ config }) => ({ ...(config.expo || {}), name: "OpenBirding", slug: "OpenBirding", - version: "1.8.0", + version: "2.0.0", orientation: "portrait", icon: "./assets/images/logo.png", scheme: "openbirding", From 292449ae14968481f62a303af7b903087b8a0ce4 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Fri, 12 Jun 2026 15:50:06 -0700 Subject: [PATCH 23/23] Store units in settings --- app.config.js | 1 + app/_layout.tsx | 9 ++++ app/settings-units.tsx | 90 ++++++++++++++++++++++++++++++++++++++ app/settings.tsx | 11 ++++- components/HotspotItem.tsx | 4 +- components/PlaceItem.tsx | 4 +- lib/utils.ts | 5 +-- package-lock.json | 20 +++++++++ package.json | 1 + stores/settingsStore.ts | 27 ++++++++++++ 10 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 app/settings-units.tsx diff --git a/app.config.js b/app.config.js index 7773496..63a41ea 100644 --- a/app.config.js +++ b/app.config.js @@ -58,6 +58,7 @@ module.exports = ({ config }) => ({ "expo-web-browser", "expo-font", "expo-image", + "expo-localization", ], experiments: { typedRoutes: true, reactCompiler: true }, extra: { diff --git a/app/_layout.tsx b/app/_layout.tsx index 0747e93..ecadf68 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -104,6 +104,15 @@ export default function RootLayout() { headerShadowVisible: false, }} /> + void; + isLast?: boolean; +}; + +function OptionRow({ label, selected, onPress, isLast }: OptionRowProps) { + const borderStyle = isLast ? {} : tw`border-b border-gray-200/50`; + + return ( + + {label} + {selected && } + + ); +} + +type OptionsGroupProps = { + children: React.ReactNode; + footer?: string; +}; + +function OptionsGroup({ children, footer }: OptionsGroupProps) { + const useGlass = Platform.OS === "ios" && isLiquidGlassAvailable(); + + const cardStyle: ViewStyle = { + borderRadius: 12, + overflow: "hidden", + }; + + const content = useGlass ? ( + + {children} + + ) : ( + {children} + ); + + return ( + + {content} + {footer && {footer}} + + ); +} + +const OPTIONS: { value: DistanceUnits; label: string }[] = [ + { value: "metric", label: "Kilometers" }, + { value: "imperial", label: "Miles" }, +]; + +export default function UnitsPage() { + const router = useRouter(); + const distanceUnits = useSettingsStore((state) => state.distanceUnits); + const setDistanceUnits = useSettingsStore((state) => state.setDistanceUnits); + + const handleSelect = (units: DistanceUnits) => { + setDistanceUnits(units); + router.back(); + }; + + return ( + + + {OPTIONS.map((option, index) => ( + handleSelect(option.value)} + isLast={index === OPTIONS.length - 1} + /> + ))} + + + ); +} diff --git a/app/settings.tsx b/app/settings.tsx index 0ce4496..f59d0c7 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -112,6 +112,7 @@ export default function SettingsPage() { const lifelistExclusions = useSettingsStore((state) => state.lifelistExclusions); const disableSunTimes = useSettingsStore((state) => state.disableSunTimes); const setDisableSunTimes = useSettingsStore((state) => state.setDisableSunTimes); + const distanceUnits = useSettingsStore((state) => state.distanceUnits); const providers = getExternalMapProviders(); const getProviderName = (providerId: string | null) => { @@ -157,6 +158,12 @@ export default function SettingsPage() { + router.push("/settings-units" as Href)} + icon={{ family: "fontawesome5", name: "ruler", bgColor: "#5856D6" }} + /> router.push("/settings-life-list-exclusions" as Href)} - icon={{ name: "eye-off", bgColor: "#FF9500" }} + icon={{ name: "eye-off", bgColor: "#FF3B30" }} /> router.push("/settings-import-life-list" as Href)} - icon={{ name: "cloud-upload", bgColor: "#5856D6" }} + icon={{ name: "cloud-upload", bgColor: "#30B0C7" }} isLast /> diff --git a/components/HotspotItem.tsx b/components/HotspotItem.tsx index 139dd7f..7b0bee7 100644 --- a/components/HotspotItem.tsx +++ b/components/HotspotItem.tsx @@ -2,6 +2,7 @@ import tw from "@/lib/tw"; import { Hotspot } from "@/lib/types"; import { getSavedHotspotIconImage } from "@/lib/hotspotIconImages"; import { formatDistance, getMarkerColor } from "@/lib/utils"; +import { useSettingsStore } from "@/stores/settingsStore"; import { Ionicons } from "@expo/vector-icons"; import React, { useCallback } from "react"; import { Image, Pressable, Text, View } from "react-native"; @@ -14,6 +15,7 @@ type HotspotItemProps = { const HotspotItem = React.memo( ({ item, onSelect, isSaved = false }: HotspotItemProps) => { + const useMiles = useSettingsStore((state) => state.distanceUnits === "imperial"); const handlePress = useCallback(() => { onSelect(item); }, [item, onSelect]); @@ -43,7 +45,7 @@ const HotspotItem = React.memo( {item.distance !== undefined && ( - {formatDistance(item.distance, item.country)} + {formatDistance(item.distance, useMiles)} )} diff --git a/components/PlaceItem.tsx b/components/PlaceItem.tsx index 2b1e31f..192e5e7 100644 --- a/components/PlaceItem.tsx +++ b/components/PlaceItem.tsx @@ -2,6 +2,7 @@ import { getPlaceIconImage } from "@/lib/placeIconImages"; import tw from "@/lib/tw"; import { SavedPlace } from "@/lib/types"; import { formatDistance } from "@/lib/utils"; +import { useSettingsStore } from "@/stores/settingsStore"; import { Ionicons } from "@expo/vector-icons"; import React, { useCallback } from "react"; import { Image, Pressable, Text, View } from "react-native"; @@ -13,6 +14,7 @@ type PlaceItemProps = { const PlaceItem = React.memo( ({ item, onSelect }: PlaceItemProps) => { + const useMiles = useSettingsStore((state) => state.distanceUnits === "imperial"); const handlePress = useCallback(() => { onSelect(item); }, [item, onSelect]); @@ -34,7 +36,7 @@ const PlaceItem = React.memo( {item.distance !== undefined && ( - {formatDistance(item.distance, null)} + {formatDistance(item.distance, useMiles)} )} diff --git a/lib/utils.ts b/lib/utils.ts index 1f8dff2..a593375 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -155,10 +155,7 @@ export function calculateDistance(lat1: number, lon1: number, lat2: number, lon2 return R * c; } -export const MILES_COUNTRIES = ["US", "GB", "MM", "LR", "PR", "VI", "GU", "MP", "AS", "KY", "TC", "VG", "AI", "MS", "FK"]; - -export function formatDistance(distanceKm: number, country: string | null): string { - const useMiles = country && MILES_COUNTRIES.includes(country); +export function formatDistance(distanceKm: number, useMiles: boolean): string { if (useMiles) { const distanceMiles = distanceKm * 0.621371; const rounded = Math.round(distanceMiles); diff --git a/package-lock.json b/package-lock.json index 389196f..8ed2b89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "expo-haptics": "~55.0.14", "expo-image": "~55.0.11", "expo-linking": "~55.0.15", + "expo-localization": "~55.0.15", "expo-location": "~55.1.10", "expo-router": "~55.0.16", "expo-splash-screen": "~55.0.21", @@ -7518,6 +7519,19 @@ "react-native": "*" } }, + "node_modules/expo-localization": { + "version": "55.0.15", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-55.0.15.tgz", + "integrity": "sha512-+HD55LeeIWyVRLvpQ909Am89XS16dUBkbB4/ruCJXS9oWv1K8W+FoXuOPTpmdvwHfC9cxt0loiwPWUiw2fdgbg==", + "license": "MIT", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-location": { "version": "55.1.10", "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.10.tgz", @@ -12833,6 +12847,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", + "license": "BSD-3-Clause" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index f2c9470..781c3c6 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "expo-haptics": "~55.0.14", "expo-image": "~55.0.11", "expo-linking": "~55.0.15", + "expo-localization": "~55.0.15", "expo-location": "~55.1.10", "expo-router": "~55.0.16", "expo-splash-screen": "~55.0.21", diff --git a/stores/settingsStore.ts b/stores/settingsStore.ts index 3dc7f4c..d103435 100644 --- a/stores/settingsStore.ts +++ b/stores/settingsStore.ts @@ -1,4 +1,5 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; +import { getLocales } from "expo-localization"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -10,6 +11,15 @@ export type LifeListEntry = { isManual?: boolean; }; +export type DistanceUnits = "metric" | "imperial"; + +// Default distance units from the device's region. measurementSystem is "us" | "uk" | "metric" | null; +// both the US and UK use miles for road distances, everything else is metric. +function getDeviceDistanceUnits(): DistanceUnits { + const system = getLocales()[0]?.measurementSystem; + return system === "us" || system === "uk" ? "imperial" : "metric"; +} + type SettingsState = { version: number; directionsProvider: string | null; @@ -18,6 +28,7 @@ type SettingsState = { disableSunTimes: boolean; showAllSpecies: boolean; targetMonths: number[]; + distanceUnits: DistanceUnits; }; type SettingsActions = { @@ -27,6 +38,7 @@ type SettingsActions = { setDisableSunTimes: (value: boolean) => void; setShowAllSpecies: (value: boolean) => void; setTargetMonths: (months: number[]) => void; + setDistanceUnits: (units: DistanceUnits) => void; }; type SettingsStore = SettingsState & SettingsActions; @@ -50,6 +62,17 @@ const migrations: Migration[] = [ return state; }, }, + { + version: 2, + migrate: async (state) => { + // Seed distanceUnits from the device region for users upgrading from a build that + // predates the setting, and persist it so it no longer relies on the rehydrate-merge default. + if (state.distanceUnits == null) { + return { ...state, distanceUnits: getDeviceDistanceUnits() }; + } + return state; + }, + }, ]; const LATEST_VERSION = migrations[migrations.length - 1]?.version ?? 0; @@ -77,12 +100,16 @@ export const useSettingsStore = create()( disableSunTimes: false, showAllSpecies: false, targetMonths: [], + // Seeded from the device region; persisted values from earlier installs fall back to this default + // via the shallow rehydrate merge, so existing users also pick up their locale's units. + distanceUnits: getDeviceDistanceUnits(), setDirectionsProvider: (provider) => set({ directionsProvider: provider || null }), setLifelist: (lifelist) => set({ lifelist }), setLifelistExclusions: (exclusions) => set({ lifelistExclusions: exclusions }), setDisableSunTimes: (value) => set({ disableSunTimes: value }), setShowAllSpecies: (value) => set({ showAllSpecies: value }), setTargetMonths: (months) => set({ targetMonths: months }), + setDistanceUnits: (units) => set({ distanceUnits: units }), }), { name: "settings",