diff --git a/App.tsx b/App.tsx
index 5e963b1..b36bd21 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,45 +1,21 @@
-/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * @format
- */
-
-import { NewAppScreen } from '@react-native/new-app-screen';
-import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
-import {
- SafeAreaProvider,
- useSafeAreaInsets,
-} from 'react-native-safe-area-context';
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+import { FightRateApp } from './src/app/FightRateApp';
function App() {
- const isDarkMode = useColorScheme() === 'dark';
-
return (
-
-
-
-
- );
-}
-
-function AppContent() {
- const safeAreaInsets = useSafeAreaInsets();
-
- return (
-
-
-
+
+
+
+
+
);
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
+ root: { flex: 1 },
});
export default App;
diff --git a/__tests__/App.test.tsx b/__tests__/App.test.tsx
index e532f70..2c2ac84 100644
--- a/__tests__/App.test.tsx
+++ b/__tests__/App.test.tsx
@@ -3,8 +3,61 @@
*/
import React from 'react';
+import { NativeModules } from 'react-native';
import ReactTestRenderer from 'react-test-renderer';
-import App from '../App';
+
+const emptySnapshot = {
+ phase: 'idle',
+ isCapturing: false,
+ isMonitoring: false,
+ startedAt: 0,
+ statusMessage: 'Captura parada.',
+ connectedDevice: null,
+ discoveredDevices: [],
+ samples: [],
+ sessions: [],
+ services: [],
+ characteristics: [],
+ batteryOptimizationIgnored: true,
+};
+
+NativeModules.FightRateCapture = {
+ addListener: jest.fn(),
+ removeListeners: jest.fn(),
+ getSnapshot: jest.fn(async () => emptySnapshot),
+ startMonitoring: jest.fn(async () => emptySnapshot),
+ startCapture: jest.fn(async () => emptySnapshot),
+ startWorkout: jest.fn(async () => ({
+ ...emptySnapshot,
+ isMonitoring: true,
+ isCapturing: true,
+ })),
+ connectToDevice: jest.fn(async () => emptySnapshot),
+ stopCapture: jest.fn(async () => emptySnapshot),
+ stopWorkout: jest.fn(async () => ({
+ ...emptySnapshot,
+ isMonitoring: true,
+ })),
+ stopMonitoring: jest.fn(async () => emptySnapshot),
+ getSessionSamples: jest.fn(async () => []),
+ getContinuousSamples: jest.fn(async () => []),
+ isIgnoringBatteryOptimizations: jest.fn(async () => true),
+ openBatteryOptimizationSettings: jest.fn(async () => true),
+};
+
+// react-native-gesture-handler requires a native module; mock it for tests.
+NativeModules.RNGestureHandlerModule = {
+ attachGestureHandler: jest.fn(),
+ createGestureHandler: jest.fn(),
+ dropGestureHandler: jest.fn(),
+ updateGestureHandler: jest.fn(),
+ flushOperations: jest.fn(),
+ install: jest.fn(),
+ State: { UNDETERMINED: 0, FAILED: 1, BEGAN: 2, CANCELLED: 3, ACTIVE: 4, END: 5 },
+ Directions: { RIGHT: 1, LEFT: 2, UP: 4, DOWN: 8 },
+};
+
+const App = require('../App').default;
test('renders correctly', async () => {
await ReactTestRenderer.act(() => {
diff --git a/__tests__/chartViewport.test.ts b/__tests__/chartViewport.test.ts
new file mode 100644
index 0000000..b8f737d
--- /dev/null
+++ b/__tests__/chartViewport.test.ts
@@ -0,0 +1,312 @@
+import { type HeartRateSample } from '../src/training/trainingTypes';
+import {
+ applyPan,
+ applyZoomIn,
+ applyZoomOut,
+ buildChartViewport,
+ formatTickLabel,
+ granularityForSpan,
+ isLiveTailSnap,
+ pickSampleAtTimestamp,
+ xFromTimestampMs,
+ timestampMsFromX,
+ type Viewport,
+} from '../src/training/chartViewport';
+
+const MIN_SPAN = 1_000;
+const MAX_SPAN = 24 * 60 * 60 * 1_000;
+
+function sample(timestamp: number, bpm: number): HeartRateSample {
+ return {
+ timestamp,
+ bpm,
+ contactDetected: null,
+ rrIntervalsMs: [],
+ deviceName: 'Magene H003',
+ };
+}
+
+function viewport(startMs: number, endMs: number, liveTail = true): Viewport {
+ return { startMs, endMs, liveTail };
+}
+
+describe('granularityForSpan', () => {
+ test('returns minute granularity for spans >= 5 minutes', () => {
+ const g = granularityForSpan(5 * 60 * 1_000);
+ expect(g.kind).toBe('minute');
+ expect(g.stepMs).toBe(60_000);
+ });
+
+ test('returns halfMinute granularity for spans >= 1 minute and < 5 minutes', () => {
+ expect(granularityForSpan(60 * 1_000).kind).toBe('halfMinute');
+ expect(granularityForSpan(4 * 60 * 1_000).kind).toBe('halfMinute');
+ expect(granularityForSpan(5 * 60 * 1_000).kind).toBe('minute');
+ });
+
+ test('returns quarterMinute granularity for spans >= 30s and < 1 minute', () => {
+ expect(granularityForSpan(30 * 1_000).kind).toBe('quarterMinute');
+ expect(granularityForSpan(59 * 1_000).kind).toBe('quarterMinute');
+ });
+
+ test('returns tenSeconds granularity for spans >= 10s and < 30s', () => {
+ expect(granularityForSpan(10 * 1_000).kind).toBe('tenSeconds');
+ expect(granularityForSpan(29 * 1_000).kind).toBe('tenSeconds');
+ });
+
+ test('returns second granularity for spans >= 2s and < 10s', () => {
+ expect(granularityForSpan(2 * 1_000).kind).toBe('second');
+ expect(granularityForSpan(9 * 1_000).kind).toBe('second');
+ });
+
+ test('returns halfSecond granularity for spans < 2s', () => {
+ expect(granularityForSpan(1_999).kind).toBe('halfSecond');
+ expect(granularityForSpan(0).kind).toBe('halfSecond');
+ });
+});
+
+describe('formatTickLabel', () => {
+ const T = new Date('2026-06-14T09:30:45.500Z').getTime();
+
+ test('minute granularity formats HH:mm in local time', () => {
+ const g = granularityForSpan(5 * 60 * 1_000);
+ const label = formatTickLabel(T, g);
+ expect(label).toMatch(/^\d{2}:\d{2}$/);
+ });
+
+ test('second granularity formats HH:mm:ss', () => {
+ const g = granularityForSpan(2 * 1_000);
+ const label = formatTickLabel(T, g);
+ expect(label).toMatch(/^\d{2}:\d{2}:\d{2}$/);
+ });
+
+ test('halfSecond granularity formats HH:mm:ss.S', () => {
+ const g = granularityForSpan(500);
+ const label = formatTickLabel(T, g);
+ expect(label).toMatch(/^\d{2}:\d{2}:\d{2}\.\d$/);
+ });
+});
+
+describe('buildChartViewport', () => {
+ test('returns null for fewer than 2 samples', () => {
+ expect(buildChartViewport([], { defaultSpanMs: 60_000 })).toBeNull();
+ expect(
+ buildChartViewport([sample(1_000, 90)], { defaultSpanMs: 60_000 }),
+ ).toBeNull();
+ });
+
+ test('initial viewport fits the dataset when total span is smaller than defaultSpan', () => {
+ const samples = [
+ sample(1_000, 90),
+ sample(11_000, 100),
+ sample(21_000, 110),
+ ];
+ const vp = buildChartViewport(samples, { defaultSpanMs: 60_000 });
+ expect(vp).not.toBeNull();
+ expect(vp!.startMs).toBe(1_000);
+ expect(vp!.endMs).toBe(21_000);
+ expect(vp!.liveTail).toBe(true);
+ });
+
+ test('initial viewport keeps defaultSpan ending at the last sample when dataset is larger', () => {
+ const samples = [
+ sample(0, 90),
+ sample(10_000, 100),
+ sample(20_000, 110),
+ sample(30_000, 120),
+ sample(40_000, 130),
+ ];
+ const vp = buildChartViewport(samples, { defaultSpanMs: 30_000 });
+ expect(vp).not.toBeNull();
+ expect(vp!.endMs).toBe(40_000);
+ expect(vp!.endMs - vp!.startMs).toBe(30_000);
+ expect(vp!.liveTail).toBe(true);
+ });
+
+ test('initial viewport is non-live when liveTail: false is passed', () => {
+ const samples = [sample(0, 90), sample(10_000, 100)];
+ const vp = buildChartViewport(samples, {
+ defaultSpanMs: 60_000,
+ liveTail: false,
+ });
+ expect(vp!.liveTail).toBe(false);
+ });
+
+ test('sorts samples defensively before computing viewport', () => {
+ const samples = [sample(30_000, 130), sample(0, 90), sample(15_000, 110)];
+ const vp = buildChartViewport(samples, { defaultSpanMs: 60_000 });
+ expect(vp!.startMs).toBe(0);
+ expect(vp!.endMs).toBe(30_000);
+ });
+});
+
+describe('applyZoomIn', () => {
+ test('halves the span around the focus point', () => {
+ const start = viewport(0, 60_000);
+ const z = applyZoomIn(start, 30_000, {
+ minSpanMs: MIN_SPAN,
+ maxSpanMs: MAX_SPAN,
+ });
+ expect(z.endMs - z.startMs).toBe(30_000);
+ expect(z.startMs + (z.endMs - z.startMs) / 2).toBeCloseTo(30_000, 0);
+ expect(z.liveTail).toBe(false);
+ });
+
+ test('never goes below minSpanMs', () => {
+ const start = viewport(0, 2_000);
+ const z = applyZoomIn(start, 1_000, {
+ minSpanMs: MIN_SPAN,
+ maxSpanMs: MAX_SPAN,
+ });
+ expect(z.endMs - z.startMs).toBe(MIN_SPAN);
+ });
+
+ test('preserves focus when minSpan is hit', () => {
+ const start = viewport(0, 2_000);
+ const z = applyZoomIn(start, 1_000, {
+ minSpanMs: MIN_SPAN,
+ maxSpanMs: MAX_SPAN,
+ });
+ expect(z.startMs).toBeLessThanOrEqual(1_000);
+ expect(z.endMs).toBeGreaterThanOrEqual(1_000);
+ });
+
+ test('forces liveTail=false (zoom is always a user interaction)', () => {
+ const start = viewport(0, 60_000, true);
+ expect(applyZoomIn(start, 30_000).liveTail).toBe(false);
+ });
+});
+
+describe('applyZoomOut', () => {
+ test('doubles the span around the focus point', () => {
+ const start = viewport(0, 30_000, false);
+ const z = applyZoomOut(start, 15_000, {
+ minSpanMs: MIN_SPAN,
+ maxSpanMs: MAX_SPAN,
+ });
+ expect(z.endMs - z.startMs).toBe(60_000);
+ expect(z.startMs + (z.endMs - z.startMs) / 2).toBeCloseTo(15_000, 0);
+ });
+
+ test('never goes above maxSpanMs', () => {
+ const start = viewport(0, 12 * 60 * 60 * 1_000, false);
+ const z = applyZoomOut(start, 6 * 60 * 60 * 1_000, {
+ minSpanMs: MIN_SPAN,
+ maxSpanMs: MAX_SPAN,
+ });
+ expect(z.endMs - z.startMs).toBe(MAX_SPAN);
+ });
+
+ test('forces liveTail=false', () => {
+ expect(applyZoomOut(viewport(0, 30_000, true), 15_000).liveTail).toBe(
+ false,
+ );
+ });
+});
+
+describe('applyPan', () => {
+ test('dx > 0 shifts the viewport forward in time and disables liveTail', () => {
+ const start = viewport(0, 60_000, true);
+ const p = applyPan(start, 100, 1); // 1 pixel per ms
+ expect(p.startMs).toBe(100);
+ expect(p.endMs).toBe(60_100);
+ expect(p.liveTail).toBe(false);
+ });
+
+ test('dx < 0 shifts the viewport backward in time', () => {
+ const start = viewport(0, 60_000, true);
+ const p = applyPan(start, -50, 1);
+ expect(p.startMs).toBe(-50);
+ expect(p.endMs).toBe(59_950);
+ expect(p.liveTail).toBe(false);
+ });
+
+ test('keeps liveTail false even if input was live (any pan detaches)', () => {
+ const start = viewport(0, 60_000, true);
+ expect(applyPan(start, 0.5, 1).liveTail).toBe(false);
+ });
+});
+
+describe('isLiveTailSnap', () => {
+ test('returns true when endMs is within 5% of last sample timestamp', () => {
+ const vp = viewport(0, 100_000, false);
+ expect(isLiveTailSnap(vp, 104_000)).toBe(true);
+ expect(isLiveTailSnap(vp, 105_000, 0.05)).toBe(true);
+ });
+
+ test('returns false when endMs is more than 5% behind last sample timestamp', () => {
+ const vp = viewport(0, 100_000, false);
+ expect(isLiveTailSnap(vp, 110_000, 0.05)).toBe(false);
+ });
+
+ test('uses a custom snap fraction', () => {
+ const vp = viewport(0, 100_000, false);
+ expect(isLiveTailSnap(vp, 150_000, 0.5)).toBe(true);
+ expect(isLiveTailSnap(vp, 250_000, 0.5)).toBe(false);
+ });
+});
+
+describe('pickSampleAtTimestamp', () => {
+ const samples = [
+ sample(0, 80),
+ sample(1_000, 90),
+ sample(2_500, 100),
+ sample(4_000, 110),
+ sample(5_000, 120),
+ ];
+
+ test('returns the closest sample within tolerance', () => {
+ expect(pickSampleAtTimestamp(samples, 2_400, 500)).toEqual(samples[2]);
+ expect(pickSampleAtTimestamp(samples, 600, 500)).toEqual(samples[1]);
+ });
+
+ test('returns null when no sample is within tolerance', () => {
+ expect(pickSampleAtTimestamp(samples, 3_500, 100)).toBeNull();
+ });
+
+ test('tolerates unsorted samples by considering the global closest', () => {
+ const shuffled = [
+ samples[3],
+ samples[0],
+ samples[2],
+ samples[4],
+ samples[1],
+ ];
+ expect(pickSampleAtTimestamp(shuffled, 2_400, 500)).toEqual(samples[2]);
+ });
+
+ test('returns null for empty input', () => {
+ expect(pickSampleAtTimestamp([], 0, 1_000)).toBeNull();
+ });
+});
+
+describe('xFromTimestampMs and timestampMsFromX', () => {
+ const vp = viewport(0, 100_000, false);
+ const chartWidth = 320;
+ const padX = 14;
+
+ test('xFromTimestampMs maps edge timestamps to padX and chartWidth-padX', () => {
+ expect(xFromTimestampMs(0, vp, chartWidth, padX)).toBeCloseTo(padX, 5);
+ expect(xFromTimestampMs(100_000, vp, chartWidth, padX)).toBeCloseTo(
+ chartWidth - padX,
+ 5,
+ );
+ expect(xFromTimestampMs(50_000, vp, chartWidth, padX)).toBeCloseTo(
+ chartWidth / 2,
+ 5,
+ );
+ });
+
+ test('timestampMsFromX is the inverse of xFromTimestampMs', () => {
+ const ts = 73_421;
+ const x = xFromTimestampMs(ts, vp, chartWidth, padX);
+ const back = timestampMsFromX(x, vp, chartWidth, padX);
+ expect(back).toBeCloseTo(ts, 0);
+ });
+
+ test('timestampMsFromX clamps to viewport bounds', () => {
+ const before = timestampMsFromX(-100, vp, chartWidth, padX);
+ expect(before).toBeLessThanOrEqual(vp.startMs);
+ const after = timestampMsFromX(500, vp, chartWidth, padX);
+ expect(after).toBeGreaterThanOrEqual(vp.endMs);
+ });
+});
diff --git a/__tests__/continuousUsage.test.ts b/__tests__/continuousUsage.test.ts
new file mode 100644
index 0000000..fd1622e
--- /dev/null
+++ b/__tests__/continuousUsage.test.ts
@@ -0,0 +1,121 @@
+import {
+ buildContinuousUsageHistory,
+ filterSamplesByRange,
+} from '../src/training/continuousUsage';
+import { type HeartRateSample } from '../src/training/trainingTypes';
+
+function sample(
+ timestamp: number,
+ bpm: number,
+ deviceName = 'Magene H003',
+): HeartRateSample {
+ return {
+ timestamp,
+ bpm,
+ contactDetected: null,
+ rrIntervalsMs: [],
+ deviceName,
+ };
+}
+
+describe('buildContinuousUsageHistory', () => {
+ it('groups continuous samples into usage blocks separated by long gaps', () => {
+ const blocks = buildContinuousUsageHistory(
+ [
+ sample(1_000, 120),
+ sample(2_000, 130),
+ sample(10 * 60_000, 140),
+ sample(10 * 60_000 + 1_000, 150),
+ ],
+ { gapMs: 5 * 60_000 },
+ );
+
+ expect(blocks).toHaveLength(2);
+ expect(blocks[0]).toMatchObject({
+ startedAt: 10 * 60_000,
+ endedAt: 10 * 60_000 + 1_000,
+ sampleCount: 2,
+ averageBpm: 145,
+ peakBpm: 150,
+ minBpm: 140,
+ });
+ expect(blocks[1]).toMatchObject({
+ startedAt: 1_000,
+ endedAt: 2_000,
+ sampleCount: 2,
+ averageBpm: 125,
+ peakBpm: 130,
+ minBpm: 120,
+ });
+ });
+
+ it('sorts samples and ignores invalid BPM values', () => {
+ const blocks = buildContinuousUsageHistory(
+ [sample(5_000, 150), sample(1_000, 0), sample(3_000, 120)],
+ { gapMs: 10_000 },
+ );
+
+ expect(blocks).toHaveLength(1);
+ expect(blocks[0].startedAt).toBe(3_000);
+ expect(blocks[0].endedAt).toBe(5_000);
+ expect(blocks[0].sampleCount).toBe(2);
+ expect(blocks[0].samples.map(item => item.bpm)).toEqual([120, 150]);
+ });
+
+ it('uses the most recent block first for history display', () => {
+ const blocks = buildContinuousUsageHistory(
+ [sample(1_000, 110), sample(30 * 60_000, 160)],
+ { gapMs: 5 * 60_000 },
+ );
+
+ expect(blocks.map(block => block.startedAt)).toEqual([30 * 60_000, 1_000]);
+ });
+
+ it('keeps a stable block id while live samples extend the same usage block', () => {
+ const initial = buildContinuousUsageHistory(
+ [sample(1_000, 120), sample(2_000, 130)],
+ { gapMs: 5 * 60_000 },
+ );
+ const updated = buildContinuousUsageHistory(
+ [sample(1_000, 120), sample(2_000, 130), sample(3_000, 140)],
+ { gapMs: 5 * 60_000 },
+ );
+
+ expect(updated[0].id).toBe(initial[0].id);
+ expect(updated[0].sampleCount).toBe(3);
+ });
+});
+
+describe('filterSamplesByRange', () => {
+ it('returns samples inside an inclusive custom date/time range', () => {
+ const samples = [
+ sample(Date.UTC(2026, 5, 10, 8, 59), 110),
+ sample(Date.UTC(2026, 5, 10, 9, 0), 120),
+ sample(Date.UTC(2026, 5, 10, 9, 30), 130),
+ sample(Date.UTC(2026, 5, 10, 10, 0), 140),
+ sample(Date.UTC(2026, 5, 10, 10, 1), 150),
+ ];
+
+ const filtered = filterSamplesByRange(samples, {
+ start: Date.UTC(2026, 5, 10, 9, 0),
+ end: Date.UTC(2026, 5, 10, 10, 0),
+ });
+
+ expect(filtered.map(item => item.bpm)).toEqual([120, 130, 140]);
+ });
+
+ it('supports ranges that cross multiple days', () => {
+ const samples = [
+ sample(Date.UTC(2026, 5, 10, 23, 50), 120),
+ sample(Date.UTC(2026, 5, 11, 0, 10), 130),
+ sample(Date.UTC(2026, 5, 11, 1, 0), 140),
+ ];
+
+ const filtered = filterSamplesByRange(samples, {
+ start: Date.UTC(2026, 5, 10, 23, 45),
+ end: Date.UTC(2026, 5, 11, 0, 15),
+ });
+
+ expect(filtered.map(item => item.bpm)).toEqual([120, 130]);
+ });
+});
diff --git a/__tests__/heartRateParser.test.ts b/__tests__/heartRateParser.test.ts
new file mode 100644
index 0000000..3056b1e
--- /dev/null
+++ b/__tests__/heartRateParser.test.ts
@@ -0,0 +1,29 @@
+import { parseHeartRateMeasurement } from '../src/ble/heartRateParser';
+
+describe('parseHeartRateMeasurement', () => {
+ it('parses 8-bit BPM payloads', () => {
+ expect(parseHeartRateMeasurement([0x00, 154])).toMatchObject({
+ bpm: 154,
+ contactSupported: false,
+ contactDetected: null,
+ energyExpended: null,
+ rrIntervalsMs: [],
+ });
+ });
+
+ it('parses 16-bit BPM payloads', () => {
+ expect(parseHeartRateMeasurement([0x01, 0x2c, 0x01]).bpm).toBe(300);
+ });
+
+ it('parses contact, energy and RR interval data', () => {
+ const parsed = parseHeartRateMeasurement([
+ 0x1e, 160, 0x10, 0x00, 0x00, 0x04,
+ ]);
+
+ expect(parsed.bpm).toBe(160);
+ expect(parsed.contactSupported).toBe(true);
+ expect(parsed.contactDetected).toBe(true);
+ expect(parsed.energyExpended).toBe(16);
+ expect(parsed.rrIntervalsMs).toEqual([1000]);
+ });
+});
diff --git a/__tests__/rssi.test.ts b/__tests__/rssi.test.ts
new file mode 100644
index 0000000..3d6eb74
--- /dev/null
+++ b/__tests__/rssi.test.ts
@@ -0,0 +1,14 @@
+import { formatRssi, normalizeRssi } from '../src/ble/rssi';
+
+describe('RSSI dBm helpers', () => {
+ it('keeps valid BLE RSSI values as dBm', () => {
+ expect(normalizeRssi(-63)).toBe(-63);
+ expect(formatRssi(-63)).toBe('-63 dBm');
+ });
+
+ it('hides Android sentinel and impossible RSSI values', () => {
+ expect(normalizeRssi(-2147483648)).toBeNull();
+ expect(normalizeRssi(127)).toBeNull();
+ expect(formatRssi(-2147483648)).toBe('sem leitura');
+ });
+});
diff --git a/__tests__/trainingMetrics.test.ts b/__tests__/trainingMetrics.test.ts
new file mode 100644
index 0000000..6b06428
--- /dev/null
+++ b/__tests__/trainingMetrics.test.ts
@@ -0,0 +1,191 @@
+import {
+ buildInsightDashboard,
+ calculateBpmVariability,
+ calculateHeartRateDrift,
+ calculateLiveMetrics,
+ calculatePostSessionRecovery,
+ filterReliableHeartRateSamples,
+ isReliableHeartRateSample,
+ summarizeRrAvailability,
+} from '../src/training/trainingMetrics';
+import {
+ type CompletedTrainingSession,
+ type HeartRateSample,
+} from '../src/training/trainingTypes';
+
+function sample(
+ timestamp: number,
+ bpm: number,
+ rrIntervalsMs: readonly number[] = [],
+): HeartRateSample {
+ return {
+ timestamp,
+ bpm,
+ contactDetected: null,
+ rrIntervalsMs,
+ deviceName: 'Magene H003',
+ };
+}
+
+function session(
+ startedAt: number,
+ endedAt: number,
+ peakBpm = 180,
+): CompletedTrainingSession {
+ return {
+ id: `session-${startedAt}-${endedAt}`,
+ startedAt,
+ endedAt,
+ durationMs: endedAt - startedAt,
+ sampleCount: 12,
+ averageBpm: 150,
+ peakBpm,
+ minBpm: 112,
+ deviceName: 'Magene H003',
+ };
+}
+
+describe('sample reliability guards', () => {
+ it('rejects zero BPM and explicit no-contact packets', () => {
+ expect(isReliableHeartRateSample(sample(1_000, 0))).toBe(false);
+ expect(
+ isReliableHeartRateSample({
+ ...sample(2_000, 128),
+ contactDetected: false,
+ }),
+ ).toBe(false);
+ expect(isReliableHeartRateSample(sample(3_000, 128))).toBe(true);
+ });
+
+ it('removes invalid readings before metrics are calculated', () => {
+ const reliable = filterReliableHeartRateSamples([
+ sample(1_000, 0),
+ { ...sample(2_000, 125), contactDetected: false },
+ sample(3_000, 130),
+ ]);
+
+ expect(reliable.map(item => item.bpm)).toEqual([130]);
+ });
+
+ it('marks live BPM as unavailable when the latest valid reading is stale', () => {
+ const metrics = calculateLiveMetrics([sample(1_000, 140)], {
+ now: 20_000,
+ staleAfterMs: 12_000,
+ });
+
+ expect(metrics.currentBpm).toBeNull();
+ expect(metrics.averageBpm).toBe(140);
+ });
+});
+
+describe('calculateBpmVariability', () => {
+ it('summarizes BPM range, standard deviation and coefficient of variation', () => {
+ const variability = calculateBpmVariability([
+ sample(1_000, 100),
+ sample(2_000, 120),
+ sample(3_000, 140),
+ ]);
+
+ expect(variability).toMatchObject({
+ averageBpm: 120,
+ minBpm: 100,
+ maxBpm: 140,
+ rangeBpm: 40,
+ standardDeviationBpm: 16.3,
+ coefficientOfVariation: 0.136,
+ classification: 'oscilante',
+ });
+ });
+});
+
+describe('calculateHeartRateDrift', () => {
+ it('compares the first and last fifth of a long enough sample window', () => {
+ const start = Date.UTC(2026, 5, 10, 9, 0);
+ const samples = Array.from({ length: 12 }, (_, index) =>
+ sample(start + index * 60_000, index < 6 ? 130 + index : 150 + index),
+ );
+
+ expect(calculateHeartRateDrift(samples)).toMatchObject({
+ status: 'ok',
+ driftBpm: 30,
+ driftPercent: 23,
+ initialAverageBpm: 131,
+ finalAverageBpm: 161,
+ });
+ });
+
+ it('does not over-interpret short windows as cardiovascular drift', () => {
+ expect(
+ calculateHeartRateDrift([sample(1_000, 130), sample(60_000, 140)]),
+ ).toMatchObject({ status: 'short-window' });
+ });
+});
+
+describe('calculatePostSessionRecovery', () => {
+ it('calculates one and two minute recovery from post-window continuous samples', () => {
+ const startedAt = Date.UTC(2026, 5, 10, 10, 0);
+ const endedAt = startedAt + 10 * 60_000;
+
+ expect(
+ calculatePostSessionRecovery(session(startedAt, endedAt, 182), [
+ sample(endedAt + 5_000, 176),
+ sample(endedAt + 61_000, 160),
+ sample(endedAt + 119_000, 148),
+ ]),
+ ).toMatchObject({
+ status: 'ok',
+ oneMinuteDropBpm: 22,
+ twoMinuteDropBpm: 34,
+ });
+ });
+});
+
+describe('summarizeRrAvailability', () => {
+ it('reports RR availability without calculating fake HRV', () => {
+ expect(
+ summarizeRrAvailability([
+ sample(1_000, 120, [1000, 980]),
+ sample(2_000, 121, [990]),
+ ]),
+ ).toMatchObject({ status: 'ok', intervalCount: 3, sampleCount: 2 });
+
+ expect(summarizeRrAvailability([sample(1_000, 120)])).toMatchObject({
+ status: 'needs-rr',
+ intervalCount: 0,
+ });
+ });
+});
+
+describe('buildInsightDashboard', () => {
+ it('builds compact scientific rows using persisted samples before live fallback', () => {
+ const start = Date.UTC(2026, 5, 10, 9, 0);
+ const persisted = [
+ sample(start, 120),
+ sample(start + 60_000, 140),
+ sample(start + 11 * 60_000, 160),
+ ];
+
+ const dashboard = buildInsightDashboard({
+ liveSamples: [sample(start + 12 * 60_000, 170)],
+ continuousSamples: persisted,
+ sessions: [],
+ });
+
+ expect(dashboard.sampleCount).toBe(3);
+ expect(dashboard.rows.map(row => row.key)).toEqual([
+ 'current-bpm',
+ 'average-bpm',
+ 'bpm-range',
+ 'bpm-variability',
+ 'zones',
+ 'load',
+ 'drift',
+ 'recovery',
+ 'profile',
+ 'hrv',
+ ]);
+ expect(dashboard.rows.find(row => row.key === 'hrv')).toMatchObject({
+ status: 'needs-rr',
+ });
+ });
+});
diff --git a/babel.config.js b/babel.config.js
index f7b3da3..8ba8eb6 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,3 +1,4 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
+ plugins: ['react-native-worklets/plugin'],
};
diff --git a/index.js b/index.js
index 9b73932..07e209c 100644
--- a/index.js
+++ b/index.js
@@ -2,6 +2,9 @@
* @format
*/
+// Gesture Handler must be imported before any other React Native code so the
+// native gesture system is initialized before our screens mount.
+import 'react-native-gesture-handler';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
diff --git a/jest.config.js b/jest.config.js
index 294be30..c1e419f 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,7 @@
module.exports = {
preset: '@react-native/jest-preset',
+ setupFiles: ['/jest.setup.js'],
+ transformIgnorePatterns: [
+ 'node_modules/(?!(jest-)?@?react-native|@react-native-community|react-native-gesture-handler|react-native-reanimated|react-native-worklets|react-native-svg|react-native-safe-area-context)',
+ ],
};
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 0000000..bc6a7a4
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,55 @@
+/* eslint-env jest */
+
+// react-native-gesture-handler and react-native-reanimated both depend on
+// native modules that are not present in the Jest environment. Mock the
+// TurboModuleRegistry calls so importing our screens does not throw.
+
+jest.mock('react-native-gesture-handler', () => {
+ const View = require('react-native').View;
+ const React = require('react');
+ const passthrough = component =>
+ React.forwardRef((props, ref) => {
+ const Comp = component;
+ return React.createElement(Comp, { ...props, ref });
+ });
+ return {
+ __esModule: true,
+ GestureHandlerRootView: passthrough(View),
+ GestureDetector: passthrough(View),
+ Gesture: {
+ Pan: () => ({
+ onBegin: () => ({
+ onUpdate: () => ({
+ onEnd: () => ({
+ onFinalize: () => ({ minDistance: () => ({}) }),
+ }),
+ }),
+ }),
+ }),
+ Pinch: () => ({
+ onBegin: () => ({
+ onUpdate: () => ({
+ onEnd: () => ({ onFinalize: () => ({}) }),
+ }),
+ }),
+ }),
+ Tap: () => ({ maxDuration: () => ({ onEnd: () => ({}) }) }),
+ Race: (...args) => ({ _args: args }),
+ Simultaneous: (...args) => ({ _args: args }),
+ },
+ };
+});
+
+jest.mock('react-native-reanimated', () => {
+ const View = require('react-native').View;
+ return {
+ __esModule: true,
+ default: { View, createAnimatedComponent: c => c, Value: function () {} },
+ runOnJS: fn => fn,
+ useSharedValue: initial => ({ value: initial }),
+ useAnimatedStyle: () => ({}),
+ withTiming: v => v,
+ withSpring: v => v,
+ Easing: { inOut: () => ({}), linear: () => ({}) },
+ };
+});
diff --git a/package-lock.json b/package-lock.json
index c4cfbb8..2a1f804 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,20 +8,25 @@
"name": "FightRateRN",
"version": "0.0.1",
"dependencies": {
- "@react-native/new-app-screen": "0.86.0",
+ "lucide-react-native": "^1.17.0",
"react": "19.2.3",
"react-native": "0.86.0",
- "react-native-safe-area-context": "^5.5.2"
+ "react-native-gesture-handler": "^3.0.1",
+ "react-native-reanimated": "^4.4.1",
+ "react-native-safe-area-context": "^5.5.2",
+ "react-native-svg": "^15.15.5",
+ "react-native-worklets": "^0.9.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.1.0",
- "@react-native-community/cli-platform-android": "20.1.0",
- "@react-native-community/cli-platform-ios": "20.1.0",
+ "@react-native-community/cli": "^20.1.3",
+ "@react-native-community/cli-platform-android": "^20.1.3",
+ "@react-native-community/cli-platform-ios": "^20.1.3",
"@react-native/babel-preset": "0.86.0",
"@react-native/eslint-config": "0.86.0",
+ "@react-native/jest-preset": "^0.86.0",
"@react-native/metro-config": "0.86.0",
"@react-native/typescript-config": "0.86.0",
"@types/jest": "^29.5.13",
@@ -129,7 +134,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
@@ -158,7 +162,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -180,7 +183,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -198,7 +200,6 @@
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
"integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.28.6",
@@ -224,7 +225,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.7",
@@ -268,7 +268,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
@@ -281,7 +280,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -291,7 +289,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -309,7 +306,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
"integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-member-expression-to-functions": "^7.29.7",
@@ -327,7 +323,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.7",
@@ -368,7 +363,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.29.7",
@@ -512,7 +506,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -541,7 +534,7 @@
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
"integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -554,7 +547,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
"integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -567,7 +560,7 @@
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
"integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.12.13"
@@ -580,7 +573,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
"integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -596,7 +589,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
"integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -609,7 +601,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -625,7 +616,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -657,7 +647,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz",
"integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -673,7 +663,7 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -686,7 +676,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
"integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -699,7 +689,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -715,7 +704,7 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
"integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -728,7 +717,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
"integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -741,7 +729,7 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
"integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -754,7 +742,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
"integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -767,7 +755,7 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
"integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -780,7 +768,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
"integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -793,7 +780,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
"integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -809,7 +796,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
"integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -825,7 +812,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -858,7 +844,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
"integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -874,7 +859,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -892,7 +876,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.29.7",
@@ -926,7 +909,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -942,7 +924,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.29.7",
@@ -976,7 +957,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -1014,7 +994,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -1146,7 +1125,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -1163,7 +1141,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -1279,7 +1256,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.29.7",
@@ -1332,7 +1308,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.29.7",
@@ -1365,7 +1340,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1434,7 +1408,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1450,7 +1423,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -1483,7 +1455,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.29.7",
@@ -1500,7 +1471,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -1534,7 +1504,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1550,7 +1519,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -1570,7 +1538,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1586,7 +1553,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1602,7 +1568,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
"integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1651,7 +1616,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.29.7",
@@ -1672,7 +1636,6 @@
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
"integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.5",
@@ -1686,7 +1649,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
"integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1735,7 +1697,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
"integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1767,7 +1728,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
"integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
@@ -1820,7 +1780,6 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz",
"integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.29.7",
@@ -1951,6 +1910,25 @@
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/@babel/preset-typescript": {
+ "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.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"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
@@ -2228,7 +2206,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
"integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"camelcase": "^5.3.1",
@@ -2245,7 +2223,7 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
@@ -2255,7 +2233,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
@@ -2269,7 +2247,7 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -2283,7 +2261,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
@@ -2296,7 +2274,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
@@ -2312,7 +2290,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
@@ -2325,7 +2303,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2335,7 +2313,7 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2407,11 +2385,24 @@
}
}
},
+ "node_modules/@jest/create-cache-key-function": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
+ "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
"node_modules/@jest/environment": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
"integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/fake-timers": "^29.7.0",
@@ -2454,7 +2445,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
"integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -2591,7 +2582,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.11.6",
@@ -2696,6 +2687,19 @@
"eslint-scope": "5.1.1"
}
},
+ "node_modules/@nodable/entities": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
+ "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2735,18 +2739,18 @@
}
},
"node_modules/@react-native-community/cli": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz",
- "integrity": "sha512-441WsVtRe4nGJ9OzA+QMU1+22lA6Q2hRWqqIMKD0wjEMLqcSfOZyu2UL9a/yRpL/dRpyUsU4n7AxqKfTKO/Csg==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.3.tgz",
+ "integrity": "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-clean": "20.1.0",
- "@react-native-community/cli-config": "20.1.0",
- "@react-native-community/cli-doctor": "20.1.0",
- "@react-native-community/cli-server-api": "20.1.0",
- "@react-native-community/cli-tools": "20.1.0",
- "@react-native-community/cli-types": "20.1.0",
+ "@react-native-community/cli-clean": "20.1.3",
+ "@react-native-community/cli-config": "20.1.3",
+ "@react-native-community/cli-doctor": "20.1.3",
+ "@react-native-community/cli-server-api": "20.1.3",
+ "@react-native-community/cli-tools": "20.1.3",
+ "@react-native-community/cli-types": "20.1.3",
"commander": "^9.4.1",
"deepmerge": "^4.3.0",
"execa": "^5.0.0",
@@ -2765,26 +2769,26 @@
}
},
"node_modules/@react-native-community/cli-clean": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-20.1.0.tgz",
- "integrity": "sha512-77L4DifWfxAT8ByHnkypge7GBMYpbJAjBGV+toowt5FQSGaTBDcBHCX+FFqFRukD5fH6i8sZ41Gtw+nbfCTTIA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-20.1.3.tgz",
+ "integrity": "sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-tools": "20.1.3",
"execa": "^5.0.0",
"fast-glob": "^3.3.2",
"picocolors": "^1.1.1"
}
},
"node_modules/@react-native-community/cli-config": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-20.1.0.tgz",
- "integrity": "sha512-1x9rhLLR/dKKb92Lb5O0l0EmUG08FHf+ZVyVEf9M+tX+p5QIm52MRiy43R0UAZ2jJnFApxRk+N3sxoYK4Dtnag==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-20.1.3.tgz",
+ "integrity": "sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-tools": "20.1.3",
"cosmiconfig": "^9.0.0",
"deepmerge": "^4.3.0",
"fast-glob": "^3.3.2",
@@ -2793,43 +2797,43 @@
}
},
"node_modules/@react-native-community/cli-config-android": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-20.1.0.tgz",
- "integrity": "sha512-3A01ZDyFeCALzzPcwP/fleHoP3sGNq1UX7FzxkTrOFX8RRL9ntXNXQd27E56VU4BBxGAjAJT4Utw8pcOjJceIA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-20.1.3.tgz",
+ "integrity": "sha512-DNHDP+OWLyhKShGciBqPcxhxfp1Z/7GQcb4F+TGyCeKQAr+JdnUjRXN3X+YCU/v+g2kbYYyRJKlGabzkVvdrAw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-tools": "20.1.3",
"fast-glob": "^3.3.2",
- "fast-xml-parser": "^4.4.1",
+ "fast-xml-parser": "^5.3.6",
"picocolors": "^1.1.1"
}
},
"node_modules/@react-native-community/cli-config-apple": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.0.tgz",
- "integrity": "sha512-n6JVs8Q3yxRbtZQOy05ofeb1kGtspGN3SgwPmuaqvURF9fsuS7c4/9up2Kp9C+1D2J1remPJXiZLNGOcJvfpOA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.3.tgz",
+ "integrity": "sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-tools": "20.1.3",
"execa": "^5.0.0",
"fast-glob": "^3.3.2",
"picocolors": "^1.1.1"
}
},
"node_modules/@react-native-community/cli-doctor": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-20.1.0.tgz",
- "integrity": "sha512-QfJF1GVjA4PBrIT3SJ0vFFIu0km1vwOmLDlOYVqfojajZJ+Dnvl0f94GN1il/jT7fITAxom///XH3/URvi7YTQ==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-20.1.3.tgz",
+ "integrity": "sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config": "20.1.0",
- "@react-native-community/cli-platform-android": "20.1.0",
- "@react-native-community/cli-platform-apple": "20.1.0",
- "@react-native-community/cli-platform-ios": "20.1.0",
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-config": "20.1.3",
+ "@react-native-community/cli-platform-android": "20.1.3",
+ "@react-native-community/cli-platform-apple": "20.1.3",
+ "@react-native-community/cli-platform-ios": "20.1.3",
+ "@react-native-community/cli-tools": "20.1.3",
"command-exists": "^1.2.8",
"deepmerge": "^4.3.0",
"envinfo": "^7.13.0",
@@ -2843,9 +2847,9 @@
}
},
"node_modules/@react-native-community/cli-doctor/node_modules/semver": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
- "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"devOptional": true,
"license": "ISC",
"bin": {
@@ -2856,52 +2860,52 @@
}
},
"node_modules/@react-native-community/cli-platform-android": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.0.tgz",
- "integrity": "sha512-TeHPDThOwDppQRpndm9kCdRCBI8AMy3HSIQ+iy7VYQXL5BtZ5LfmGdusoj7nVN/ZGn0Lc6Gwts5qowyupXdeKg==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.3.tgz",
+ "integrity": "sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config-android": "20.1.0",
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-config-android": "20.1.3",
+ "@react-native-community/cli-tools": "20.1.3",
"execa": "^5.0.0",
"logkitty": "^0.7.1",
"picocolors": "^1.1.1"
}
},
"node_modules/@react-native-community/cli-platform-apple": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.0.tgz",
- "integrity": "sha512-0ih1hrYezSM2cuOlVnwBEFtMwtd8YgpTLmZauDJCv50rIumtkI1cQoOgLoS4tbPCj9U/Vn2a9BFH0DLFOOIacg==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.3.tgz",
+ "integrity": "sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config-apple": "20.1.0",
- "@react-native-community/cli-tools": "20.1.0",
+ "@react-native-community/cli-config-apple": "20.1.3",
+ "@react-native-community/cli-tools": "20.1.3",
"execa": "^5.0.0",
- "fast-xml-parser": "^4.4.1",
+ "fast-xml-parser": "^5.3.6",
"picocolors": "^1.1.1"
}
},
"node_modules/@react-native-community/cli-platform-ios": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.0.tgz",
- "integrity": "sha512-XN7Da9z4WsJxtqVtEzY8q2bv22OsvzaFP5zy5+phMWNoJlU4lf7IvBSxqGYMpQ9XhYP7arDw5vmW4W34s06rnA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.3.tgz",
+ "integrity": "sha512-2qL48SINotuHbZO73cgqSwqd/OWNx0xTbFSdujhpogV4p8BNwYYypfjh4vJY5qJEB5PxuoVkMXT+aCADpg9nBg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-platform-apple": "20.1.0"
+ "@react-native-community/cli-platform-apple": "20.1.3"
}
},
"node_modules/@react-native-community/cli-server-api": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-20.1.0.tgz",
- "integrity": "sha512-Tb415Oh8syXNT2zOzLzFkBXznzGaqKCiaichxKzGCDKg6JGHp3jSuCmcTcaPeYC7oc32n/S3Psw7798r4Q/7lA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-20.1.3.tgz",
+ "integrity": "sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.1.0",
- "body-parser": "^1.20.3",
+ "@react-native-community/cli-tools": "20.1.3",
+ "body-parser": "^2.2.2",
"compression": "^1.7.1",
"connect": "^3.6.5",
"errorhandler": "^1.5.1",
@@ -2909,13 +2913,14 @@
"open": "^6.2.0",
"pretty-format": "^29.7.0",
"serve-static": "^1.13.1",
+ "strict-url-sanitise": "0.0.1",
"ws": "^6.2.3"
}
},
"node_modules/@react-native-community/cli-tools": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-20.1.0.tgz",
- "integrity": "sha512-/YmzHGOkY6Bgrv4OaA1L8rFqsBlQd1EB2/ipAoKPiieV0EcB5PUamUSuNeFU3sBZZTYQCUENwX4wgOHgFUlDnQ==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-20.1.3.tgz",
+ "integrity": "sha512-EAn0vPCMxtHhfWk2UwLmSUfPfLUnFgC7NjiVJVTKJyVk5qGnkPfoT8te/1IUXFTysUB0F0RIi+NgDB4usFOLeA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -2932,9 +2937,9 @@
}
},
"node_modules/@react-native-community/cli-tools/node_modules/semver": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
- "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"devOptional": true,
"license": "ISC",
"bin": {
@@ -2945,9 +2950,9 @@
}
},
"node_modules/@react-native-community/cli-types": {
- "version": "20.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-20.1.0.tgz",
- "integrity": "sha512-D0kDspcwgbVXyNjwicT7Bb1JgXjijTw1JJd+qxyF/a9+sHv7TU4IchV+gN38QegeXqVyM4Ym7YZIvXMFBmyJqA==",
+ "version": "20.1.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-20.1.3.tgz",
+ "integrity": "sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -2980,7 +2985,6 @@
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
"integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.0",
@@ -2994,7 +2998,6 @@
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.0.tgz",
"integrity": "sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -3243,6 +3246,26 @@
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
+ "node_modules/@react-native/jest-preset": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/jest-preset/-/jest-preset-0.86.0.tgz",
+ "integrity": "sha512-KA+xpIP3DvJy7PQJ9c6ZdEKkOPChl+Rk/rV2MhQACEAzfhWU84407KZQv4ccyO3B4caD0gPrFjE96a4P993nsQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/create-cache-key-function": "^29.7.0",
+ "@react-native/js-polyfills": "0.86.0",
+ "babel-jest": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "regenerator-runtime": "^0.13.2"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
"node_modules/@react-native/js-polyfills": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.0.tgz",
@@ -3256,7 +3279,6 @@
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz",
"integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -3275,7 +3297,6 @@
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz",
"integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native/js-polyfills": "0.86.0",
@@ -3287,25 +3308,6 @@
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@react-native/new-app-screen": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/new-app-screen/-/new-app-screen-0.86.0.tgz",
- "integrity": "sha512-hgZYg2I6gk+U0PWpV/JWg02KaS+FYYQ7OpFPVyG5RbFRAKNJw38mp8up3JqWFCMHeUH/kjWLEbJsCA0O5Qx2MA==",
- "license": "MIT",
- "engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
- },
- "peerDependencies": {
- "@types/react": "^19.1.0",
- "react": "*",
- "react-native": "0.86.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@react-native/normalize-colors": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz",
@@ -3376,7 +3378,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
@@ -3386,7 +3388,7 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
@@ -3396,7 +3398,7 @@
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
@@ -3410,7 +3412,7 @@
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
@@ -3420,7 +3422,7 @@
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
@@ -3431,7 +3433,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
@@ -3441,7 +3443,7 @@
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
"integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -3495,7 +3497,6 @@
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3505,7 +3506,6 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
"integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
@@ -3515,7 +3515,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/yargs": {
@@ -3975,7 +3975,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -3985,6 +3985,19 @@
"node": ">= 8"
}
},
+ "node_modules/anynum": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz",
+ "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/appdirsjs": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz",
@@ -4190,7 +4203,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/transform": "^29.7.0",
@@ -4212,7 +4225,7 @@
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
"integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -4229,7 +4242,7 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
"integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.12.3",
@@ -4246,7 +4259,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
"integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.3.3",
@@ -4262,7 +4275,6 @@
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
"integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
@@ -4291,7 +4303,6 @@
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
"integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.8"
@@ -4313,7 +4324,6 @@
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-syntax-flow": "^7.12.1"
@@ -4323,7 +4333,7 @@
"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==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-syntax-async-generators": "^7.8.4",
@@ -4350,7 +4360,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
"integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"babel-plugin-jest-hoist": "^29.6.3",
@@ -4418,46 +4428,35 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.5",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
- "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "bytes": "~3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "~1.2.0",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "on-finished": "~2.4.1",
- "qs": "~6.15.1",
- "raw-body": "~2.5.3",
- "type-is": "~1.6.18",
- "unpipe": "~1.0.0"
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
},
"engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/body-parser/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/body-parser/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "devOptional": true,
- "license": "MIT"
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
},
"node_modules/brace-expansion": {
"version": "5.0.6",
@@ -4923,7 +4922,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/connect": {
@@ -4976,7 +4975,6 @@
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
"integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1"
@@ -5049,11 +5047,51 @@
"node": ">= 8"
}
},
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/data-view-buffer": {
@@ -5277,6 +5315,61 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5332,6 +5425,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -5475,7 +5580,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6052,7 +6156,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@@ -6223,7 +6327,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@@ -6233,10 +6337,10 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-xml-parser": {
- "version": "4.5.6",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz",
- "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==",
+ "node_modules/fast-xml-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
"devOptional": true,
"funding": [
{
@@ -6246,7 +6350,28 @@
],
"license": "MIT",
"dependencies": {
- "strnum": "^1.0.5"
+ "path-expression-matcher": "^1.5.0",
+ "xml-naming": "^0.1.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz",
+ "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.2.0",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.3.0",
+ "xml-naming": "^0.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -6442,7 +6567,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
"node_modules/fsevents": {
@@ -6464,7 +6589,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6558,7 +6682,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0"
@@ -6614,7 +6738,7 @@
"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",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -6648,14 +6772,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -6666,7 +6790,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -6818,7 +6942,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -6908,16 +7031,20 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
@@ -7007,7 +7134,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
@@ -7018,7 +7145,7 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@@ -7150,7 +7277,6 @@
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
@@ -7561,7 +7687,7 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
@@ -7863,7 +7989,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
"integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "^29.7.0",
@@ -7890,7 +8016,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
"integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -7946,7 +8072,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.12.13",
@@ -7967,7 +8093,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
"integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -8000,7 +8126,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
"integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -8489,7 +8615,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -8685,6 +8810,17 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react-native": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-1.17.0.tgz",
+ "integrity": "sha512-H5eM7dZkXJbYcrsjczlDC6Sq1/siWcM5O5BLLx6ljT0XDIGorZFjul+AdThuMs0I614nwowv5qbDivuQ+349Xw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-native": "*",
+ "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
+ }
+ },
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -8739,14 +8875,20 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
"node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"devOptional": true,
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.8"
}
},
"node_modules/memoize-one": {
@@ -9342,7 +9484,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9361,6 +9503,18 @@
"node": ">=8"
}
},
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -9513,7 +9667,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -9701,11 +9855,27 @@
"node": ">=8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9724,7 +9894,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/picocolors": {
@@ -9749,7 +9918,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -10011,19 +10180,19 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
+ "iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.10"
}
},
"node_modules/react": {
@@ -10131,6 +10300,57 @@
}
}
},
+ "node_modules/react-native-gesture-handler": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-3.0.1.tgz",
+ "integrity": "sha512-fu9X6vLiDy197CUcOphmq6PnV5dDPpDltFjJDnq2mAkSxB24veqmYxHyEAfS7IsG8v8jYkvfgihp2UAojgYVNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-test-renderer": "^19.1.0",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-is-edge-to-edge": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz",
+ "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-reanimated": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.4.1.tgz",
+ "integrity": "sha512-WCVBfhLE+AYI2l4inL6PC1vcfNOfmVYRSVSBkPiD12N3jvzByipnygwVpmunyhaNqbiSEDrFYcl7cOJnbHKykw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-is-edge-to-edge": "^1.3.1",
+ "semver": "^7.7.3"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "0.83 - 0.86",
+ "react-native-worklets": "0.9.x"
+ }
+ },
+ "node_modules/react-native-reanimated/node_modules/semver": {
+ "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"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.8.0.tgz",
@@ -10141,6 +10361,58 @@
"react-native": "*"
}
},
+ "node_modules/react-native-svg": {
+ "version": "15.15.5",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.5.tgz",
+ "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==",
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "css-tree": "^1.1.3"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-worklets": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.9.2.tgz",
+ "integrity": "sha512-bCBV4dwcoLnqpk0bbL92nNuv6km37DRpamCX/nqNxsRs6LmLeFoFe/a7OKL1pVDi3PY97C0BEhmq8Qq/iNvpVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-class-properties": "^7.28.6",
+ "@babel/plugin-transform-classes": "^7.28.6",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
+ "@babel/plugin-transform-optional-chaining": "^7.28.6",
+ "@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.28.5",
+ "@babel/types": "^7.27.1",
+ "convert-source-map": "^2.0.0",
+ "semver": "^7.7.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "*",
+ "@react-native/metro-config": "*",
+ "react": "*",
+ "react-native": "0.83 - 0.86"
+ }
+ },
+ "node_modules/react-native-worklets/node_modules/semver": {
+ "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"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -10255,14 +10527,12 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
"integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -10302,7 +10572,6 @@
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
"integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
@@ -10320,14 +10589,12 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz",
"integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==",
- "devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.1.0"
@@ -10356,7 +10623,6 @@
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -10872,7 +11138,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -10947,14 +11213,14 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
@@ -10967,7 +11233,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11023,6 +11289,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/strict-url-sanitise": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz",
+ "integrity": "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -11222,9 +11495,9 @@
}
},
"node_modules/strnum": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
- "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz",
+ "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==",
"devOptional": true,
"funding": [
{
@@ -11232,7 +11505,10 @@
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.0"
+ }
},
"node_modules/supports-color": {
"version": "7.2.0",
@@ -11250,7 +11526,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11297,7 +11572,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
@@ -11312,14 +11587,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -11330,7 +11605,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -11454,7 +11729,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -11474,17 +11749,53 @@
}
},
"node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/typed-array-buffer": {
@@ -11608,7 +11919,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
"integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -11618,7 +11928,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
"integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^2.0.0",
@@ -11632,7 +11941,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
"integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -11642,7 +11950,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
"integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -11921,14 +12228,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
"node_modules/write-file-atomic": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -11948,6 +12255,22 @@
"async-limiter": "~1.0.0"
}
},
+ "node_modules/xml-naming": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 1deb104..0dc0f91 100644
--- a/package.json
+++ b/package.json
@@ -7,23 +7,29 @@
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
- "test": "jest"
+ "test": "jest",
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
+ "lucide-react-native": "^1.17.0",
"react": "19.2.3",
"react-native": "0.86.0",
- "@react-native/new-app-screen": "0.86.0",
- "react-native-safe-area-context": "^5.5.2"
+ "react-native-gesture-handler": "^3.0.1",
+ "react-native-reanimated": "^4.4.1",
+ "react-native-safe-area-context": "^5.5.2",
+ "react-native-svg": "^15.15.5",
+ "react-native-worklets": "^0.9.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.1.0",
- "@react-native-community/cli-platform-android": "20.1.0",
- "@react-native-community/cli-platform-ios": "20.1.0",
+ "@react-native-community/cli": "^20.1.3",
+ "@react-native-community/cli-platform-android": "^20.1.3",
+ "@react-native-community/cli-platform-ios": "^20.1.3",
"@react-native/babel-preset": "0.86.0",
"@react-native/eslint-config": "0.86.0",
+ "@react-native/jest-preset": "^0.86.0",
"@react-native/metro-config": "0.86.0",
"@react-native/typescript-config": "0.86.0",
"@types/jest": "^29.5.13",
@@ -38,4 +44,4 @@
"engines": {
"node": ">= 22.11.0"
}
-}
\ No newline at end of file
+}
diff --git a/src/components/TrainingChart.tsx b/src/components/TrainingChart.tsx
new file mode 100644
index 0000000..6ce20b9
--- /dev/null
+++ b/src/components/TrainingChart.tsx
@@ -0,0 +1,811 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import {
+ type LayoutChangeEvent,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+import { runOnJS } from 'react-native-reanimated';
+import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg';
+import { Minus, Plus, RotateCcw } from 'lucide-react-native';
+import { type HeartRateSample } from '../training/trainingTypes';
+import {
+ applyPan,
+ applyZoomIn,
+ applyZoomOut,
+ buildChartViewport,
+ formatTickLabel,
+ granularityForSpan,
+ isLiveTailSnap,
+ pickSampleAtTimestamp,
+ timestampMsFromX,
+ xFromTimestampMs,
+ type Granularity,
+ type Viewport,
+} from '../training/chartViewport';
+import { colors, radius, spacing, typography } from '../theme';
+
+type Props = {
+ readonly samples: readonly HeartRateSample[];
+ readonly defaultSpanMs?: number;
+ readonly liveTail?: boolean;
+};
+
+type ChartPoint = {
+ readonly x: number;
+ readonly y: number;
+ readonly sample: HeartRateSample;
+};
+
+const CHART_WIDTH = 320;
+const CHART_HEIGHT = 148;
+const PAD_X = 14;
+const PAD_Y = 18;
+const DEFAULT_SPAN_MS = 5 * 60_000;
+const MIN_SPAN_MS = 1_000;
+const MAX_SPAN_MS = 24 * 60 * 60 * 1_000;
+const TICK_COUNT_TARGET = 4;
+
+export function TrainingChart({
+ samples,
+ defaultSpanMs = DEFAULT_SPAN_MS,
+ liveTail = true,
+}: Props) {
+ const [layoutWidth, setLayoutWidth] = useState(CHART_WIDTH);
+ const [viewport, setViewport] = useState(() =>
+ buildChartViewport(samples, { defaultSpanMs, liveTail }),
+ );
+ const [pinTimestampMs, setPinTimestampMs] = useState(null);
+
+ // Recompute viewport when samples shrink or when initial dataset changes
+ // shape. We do not recompute on every new sample in live mode: the auto-follow
+ // effect below handles that without resetting the user's pan/zoom state.
+ useEffect(() => {
+ setViewport(current => {
+ if (current === null) {
+ return buildChartViewport(samples, { defaultSpanMs, liveTail });
+ }
+ return current;
+ });
+ }, [samples, defaultSpanMs, liveTail]);
+
+ const lastSampleTimestamp = useMemo(() => {
+ if (samples.length === 0) return null;
+ return samples[samples.length - 1].timestamp;
+ }, [samples]);
+
+ // Auto-follow: when liveTail is on and the dataset extends past the current
+ // viewport end, slide endMs forward. Pin is untouched.
+ useEffect(() => {
+ if (!liveTail) return;
+ if (lastSampleTimestamp === null) return;
+ setViewport(current => {
+ if (current === null) return current;
+ if (!current.liveTail) return current;
+ if (lastSampleTimestamp <= current.endMs) return current;
+ const span = current.endMs - current.startMs;
+ return {
+ startMs: lastSampleTimestamp - span,
+ endMs: lastSampleTimestamp,
+ liveTail: true,
+ };
+ });
+ }, [lastSampleTimestamp, liveTail]);
+
+ // Re-evaluate the liveTail flag whenever the user finishes a pan: if the
+ // viewport's end is back inside the snap zone, re-attach to live.
+ const tryResumeLiveTail = useCallback(
+ (candidate: Viewport) => {
+ if (lastSampleTimestamp === null) {
+ setViewport(candidate);
+ return;
+ }
+ const resume = isLiveTailSnap(candidate, lastSampleTimestamp);
+ setViewport({ ...candidate, liveTail: resume });
+ },
+ [lastSampleTimestamp],
+ );
+
+ const onLayout = useCallback((event: LayoutChangeEvent) => {
+ setLayoutWidth(Math.max(1, event.nativeEvent.layout.width));
+ }, []);
+
+ // -------- Gestures --------
+
+ // Track the viewport snapshot captured at gesture start so incremental deltas
+ // compose against a stable baseline (avoids drift across multi-frame moves).
+ const viewportAtGestureStart = useRef(null);
+
+ const onPanBegin = useCallback(() => {
+ viewportAtGestureStart.current = viewport;
+ }, [viewport]);
+
+ const onPanUpdate = useCallback(
+ (dxPixels: number) => {
+ const baseline = viewportAtGestureStart.current;
+ if (baseline === null) return;
+ const usableWidth = Math.max(1, layoutWidth - PAD_X * 2);
+ const pixelsPerMs =
+ usableWidth / Math.max(1, baseline.endMs - baseline.startMs);
+ setViewport(applyPan(baseline, dxPixels, pixelsPerMs));
+ },
+ [layoutWidth],
+ );
+
+ const onPanEnd = useCallback(() => {
+ setViewport(current => {
+ if (current === null) return current;
+ tryResumeLiveTail(current);
+ return current;
+ });
+ }, [tryResumeLiveTail]);
+
+ const onPinchBegin = useCallback(() => {
+ viewportAtGestureStart.current = viewport;
+ }, [viewport]);
+
+ const onPinchUpdate = useCallback(
+ (scale: number, focusX: number) => {
+ const baseline = viewportAtGestureStart.current;
+ if (baseline === null) return;
+ const usableWidth = Math.max(1, layoutWidth - PAD_X * 2);
+ const focusTimestamp = timestampMsFromX(
+ focusX,
+ baseline,
+ CHART_WIDTH,
+ PAD_X,
+ );
+ // Map focusX from layout space to viewBox space.
+ const focusXInViewBox = (focusX / layoutWidth) * CHART_WIDTH;
+ const focusTimestampFromLayout = timestampMsFromX(
+ focusXInViewBox,
+ baseline,
+ CHART_WIDTH,
+ PAD_X,
+ );
+ const target = (baseline.endMs - baseline.startMs) / Math.max(0.1, scale);
+ const next = applyZoom(baseline, focusTimestamp, target, {
+ minSpanMs: MIN_SPAN_MS,
+ maxSpanMs: MAX_SPAN_MS,
+ });
+ // Keep the focus point visually anchored by re-aiming the result so the
+ // timestamp under the pinch focus lands at the same x as before.
+ const afterX = xFromTimestampMs(
+ focusTimestampFromLayout,
+ next,
+ CHART_WIDTH,
+ PAD_X,
+ );
+ const pixelDelta =
+ (afterX - focusXInViewBox) * (usableWidth / (CHART_WIDTH - PAD_X * 2));
+ setViewport(
+ applyPan(
+ next,
+ -pixelDelta,
+ usableWidth / Math.max(1, next.endMs - next.startMs),
+ ),
+ );
+ },
+ [layoutWidth],
+ );
+
+ const onPinchEnd = useCallback(() => {
+ viewportAtGestureStart.current = null;
+ onPanEnd();
+ }, [onPanEnd]);
+
+ const onZoomInPress = useCallback(() => {
+ setViewport(current => {
+ if (current === null) return current;
+ const focusMs = (current.startMs + current.endMs) / 2;
+ return applyZoomIn(current, focusMs, {
+ minSpanMs: MIN_SPAN_MS,
+ maxSpanMs: MAX_SPAN_MS,
+ });
+ });
+ }, []);
+
+ const onZoomOutPress = useCallback(() => {
+ setViewport(current => {
+ if (current === null) return current;
+ const focusMs = (current.startMs + current.endMs) / 2;
+ return applyZoomOut(current, focusMs, {
+ minSpanMs: MIN_SPAN_MS,
+ maxSpanMs: MAX_SPAN_MS,
+ });
+ });
+ }, []);
+
+ const onResetPress = useCallback(() => {
+ const fresh = buildChartViewport(samples, { defaultSpanMs, liveTail });
+ if (fresh !== null) {
+ setViewport(fresh);
+ setPinTimestampMs(null);
+ }
+ }, [samples, defaultSpanMs, liveTail]);
+
+ const onTapAtX = useCallback(
+ (xInViewBox: number) => {
+ if (viewport === null) return;
+ const tapTimestamp = timestampMsFromX(
+ xInViewBox,
+ viewport,
+ CHART_WIDTH,
+ PAD_X,
+ );
+ const tolerance = (viewport.endMs - viewport.startMs) * 0.1;
+ const sample = pickSampleAtTimestamp(samples, tapTimestamp, tolerance);
+ if (sample === null) {
+ setPinTimestampMs(null);
+ return;
+ }
+ setPinTimestampMs(previous => {
+ if (previous === sample.timestamp) return null;
+ return sample.timestamp;
+ });
+ },
+ [samples, viewport],
+ );
+
+ // Pan uses absolute deltas via the JS bridge: each frame, runOnJS forwards
+ // the cumulative translationX to applyPan over the captured baseline.
+ const panGesture = useMemo(
+ () =>
+ Gesture.Pan()
+ .onBegin(() => {
+ runOnJS(onPanBegin)();
+ })
+ .onUpdate(event => {
+ runOnJS(onPanUpdate)(event.translationX);
+ })
+ .onEnd(() => {
+ runOnJS(onPanEnd)();
+ })
+ .onFinalize(() => {
+ runOnJS(onPanEnd)();
+ })
+ .minDistance(4),
+ [onPanBegin, onPanUpdate, onPanEnd],
+ );
+
+ const pinchGesture = useMemo(
+ () =>
+ Gesture.Pinch()
+ .onBegin(() => {
+ runOnJS(onPinchBegin)();
+ })
+ .onUpdate(event => {
+ runOnJS(onPinchUpdate)(event.scale, event.focalX);
+ })
+ .onEnd(() => {
+ runOnJS(onPinchEnd)();
+ })
+ .onFinalize(() => {
+ runOnJS(onPinchEnd)();
+ }),
+ [onPinchBegin, onPinchUpdate, onPinchEnd],
+ );
+
+ const tapGesture = useMemo(
+ () =>
+ Gesture.Tap()
+ .maxDuration(220)
+ .onEnd(event => {
+ // Convert focalX (layout space) to viewBox space before hit-testing.
+ const xInViewBox = (event.x / Math.max(1, layoutWidth)) * CHART_WIDTH;
+ runOnJS(onTapAtX)(xInViewBox);
+ }),
+ [layoutWidth, onTapAtX],
+ );
+
+ const composed = useMemo(
+ () =>
+ Gesture.Race(tapGesture, Gesture.Simultaneous(panGesture, pinchGesture)),
+ [tapGesture, panGesture, pinchGesture],
+ );
+
+ // -------- Render data --------
+
+ const renderable = useMemo(() => {
+ if (viewport === null) return null;
+ if (samples.length < 2) return null;
+
+ const visible = samples.filter(
+ sample =>
+ sample.timestamp >= viewport.startMs &&
+ sample.timestamp <= viewport.endMs,
+ );
+ if (visible.length < 1) return null;
+
+ const bpms = visible.map(sample => sample.bpm);
+ const minBpm = Math.min(...bpms);
+ const maxBpm = Math.max(...bpms);
+ const bpmRange = Math.max(1, maxBpm - minBpm);
+ const span = Math.max(1, viewport.endMs - viewport.startMs);
+
+ const points: ChartPoint[] = visible.map(sample => {
+ const x = xFromTimestampMs(
+ sample.timestamp,
+ viewport,
+ CHART_WIDTH,
+ PAD_X,
+ );
+ const y =
+ CHART_HEIGHT -
+ PAD_Y -
+ ((sample.bpm - minBpm) / bpmRange) * (CHART_HEIGHT - PAD_Y * 2);
+ return { x, y, sample };
+ });
+
+ const path = points
+ .map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
+ .join(' ');
+
+ return {
+ visible,
+ minBpm,
+ maxBpm,
+ lastPoint: points[points.length - 1],
+ points,
+ path,
+ span,
+ };
+ }, [samples, viewport]);
+
+ const granularity: Granularity = useMemo(
+ () =>
+ granularityForSpan(
+ viewport ? viewport.endMs - viewport.startMs : DEFAULT_SPAN_MS,
+ ),
+ [viewport],
+ );
+
+ const ticks = useMemo(() => {
+ if (viewport === null) return [];
+ return buildTicks(viewport, granularity, TICK_COUNT_TARGET);
+ }, [viewport, granularity]);
+
+ const pinPoint = useMemo(() => {
+ if (renderable === null || pinTimestampMs === null) return null;
+ const sample = samples.find(s => s.timestamp === pinTimestampMs);
+ if (sample === undefined) return null;
+ if (
+ sample.timestamp < viewport!.startMs ||
+ sample.timestamp > viewport!.endMs
+ ) {
+ return { sample, offViewport: true };
+ }
+ return { sample, offViewport: false };
+ }, [renderable, pinTimestampMs, samples, viewport]);
+
+ // -------- JSX --------
+
+ if (viewport === null) {
+ return (
+
+ Aguardando amostras reais
+
+ O gráfico aparece quando o sensor enviar BPM pelo characteristic 2A37.
+
+
+ );
+ }
+
+ if (renderable === null) {
+ return (
+
+ Sem amostras na janela
+
+ Toque em [↺] para voltar ao fit ou [+] [−] para ajustar a janela.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ }
+ onPress={onZoomInPress}
+ />
+ }
+ onPress={onZoomOutPress}
+ />
+ }
+ onPress={onResetPress}
+ />
+
+
+
+
+ {pinPoint
+ ? 'AMOSTRA PINADA'
+ : viewport.liveTail
+ ? 'AO VIVO'
+ : 'JANELA'}
+
+
+ {pinPoint
+ ? `${formatSampleTime(pinPoint.sample.timestamp)} · ${
+ pinPoint.sample.bpm
+ } BPM`
+ : `${formatSampleTime(renderable.lastPoint.sample.timestamp)} · ${
+ renderable.lastPoint.sample.bpm
+ } BPM`}
+
+
+ {!viewport.liveTail &&
+ liveTail &&
+ renderable.lastPoint.sample.timestamp < (lastSampleTimestamp ?? 0) ? (
+
+ Dados mais novos disponíveis. Arraste para a direita ou toque em ↺.
+
+ ) : null}
+ {pinPoint && pinPoint.offViewport ? (
+
+ PIN FORA DA JANELA. Toque em ↺ para retornar.
+
+ ) : null}
+
+ );
+}
+
+// -------- Helpers --------
+
+function PinMarker({
+ timestampMs,
+ bpm,
+ minBpm,
+ maxBpm,
+ viewport,
+}: {
+ readonly timestampMs: number;
+ readonly bpm: number;
+ readonly minBpm: number;
+ readonly maxBpm: number;
+ readonly viewport: Viewport;
+}) {
+ const x = xFromTimestampMs(timestampMs, viewport, CHART_WIDTH, PAD_X);
+ const bpmRange = Math.max(1, maxBpm - minBpm);
+ const y =
+ CHART_HEIGHT -
+ PAD_Y -
+ ((bpm - minBpm) / bpmRange) * (CHART_HEIGHT - PAD_Y * 2);
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function ControlButton({
+ accessibilityLabel,
+ icon,
+ onPress,
+}: {
+ readonly accessibilityLabel: string;
+ readonly icon: React.ReactNode;
+ readonly onPress: () => void;
+}) {
+ return (
+ [
+ styles.controlButton,
+ pressed ? styles.controlButtonPressed : null,
+ ]}
+ >
+ {icon}
+
+ );
+}
+
+type Tick = {
+ readonly timestampMs: number;
+ readonly x: number;
+ readonly label: string;
+};
+
+function buildTicks(
+ viewport: Viewport,
+ granularity: Granularity,
+ target: number,
+): readonly Tick[] {
+ const ticks: Tick[] = [];
+ const step = granularity.stepMs;
+ // Snap start to the next multiple of step so labels look aligned.
+ const firstTick = Math.ceil(viewport.startMs / step) * step;
+ for (let t = firstTick; t <= viewport.endMs; t += step) {
+ ticks.push({
+ timestampMs: t,
+ x: xFromTimestampMs(t, viewport, CHART_WIDTH, PAD_X),
+ label: formatTickLabel(t, granularity),
+ });
+ if (ticks.length >= target + 2) break;
+ }
+ return ticks;
+}
+
+function applyZoom(
+ viewport: Viewport,
+ focusMs: number,
+ targetSpan: number,
+ options: { minSpanMs: number; maxSpanMs: number },
+): Viewport {
+ const span = Math.max(
+ options.minSpanMs,
+ Math.min(options.maxSpanMs, targetSpan),
+ );
+ const currentSpan = Math.max(1, viewport.endMs - viewport.startMs);
+ const focusRatio = (focusMs - viewport.startMs) / currentSpan;
+ const newStart = focusMs - focusRatio * span;
+ return {
+ startMs: newStart,
+ endMs: newStart + span,
+ liveTail: false,
+ };
+}
+
+function formatSampleTime(timestamp: number) {
+ return new Date(timestamp).toLocaleTimeString('pt-BR', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ borderWidth: 1,
+ borderColor: colors.line,
+ borderRadius: radius.md,
+ backgroundColor: colors.surfaceHot,
+ overflow: 'hidden',
+ },
+ chartRow: {
+ flexDirection: 'row',
+ alignItems: 'stretch',
+ },
+ chartSurface: {
+ flex: 1,
+ minHeight: CHART_HEIGHT,
+ },
+ controls: {
+ borderLeftWidth: 1,
+ borderLeftColor: colors.line,
+ paddingVertical: spacing.xs,
+ paddingHorizontal: spacing.xxs,
+ gap: spacing.xs,
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ },
+ controlButton: {
+ width: 32,
+ height: 32,
+ borderRadius: radius.xs,
+ borderWidth: 1,
+ borderColor: colors.line,
+ backgroundColor: colors.surfaceRaised,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ controlButtonPressed: {
+ opacity: 0.6,
+ },
+ readout: {
+ borderTopWidth: 1,
+ borderTopColor: colors.line,
+ minHeight: 42,
+ paddingHorizontal: spacing.sm,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: spacing.sm,
+ },
+ readoutLabel: {
+ ...typography.label,
+ color: colors.warning,
+ },
+ readoutValue: {
+ ...typography.label,
+ color: colors.text,
+ flexShrink: 1,
+ textAlign: 'right',
+ },
+ tailNotice: {
+ ...typography.label,
+ color: colors.textDim,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderTopWidth: 1,
+ borderTopColor: colors.line,
+ },
+ pinOffNotice: {
+ ...typography.label,
+ color: colors.warning,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderTopWidth: 1,
+ borderTopColor: colors.line,
+ },
+ emptyWrap: {
+ minHeight: CHART_HEIGHT,
+ borderWidth: 1,
+ borderColor: colors.line,
+ borderRadius: radius.md,
+ backgroundColor: colors.surfaceHot,
+ padding: spacing.md,
+ justifyContent: 'center',
+ },
+ emptyTitle: {
+ ...typography.label,
+ color: colors.warning,
+ },
+ emptyCopy: {
+ ...typography.body,
+ color: colors.textMuted,
+ marginTop: spacing.xs,
+ },
+});
diff --git a/src/training/chartViewport.ts b/src/training/chartViewport.ts
new file mode 100644
index 0000000..14fa13c
--- /dev/null
+++ b/src/training/chartViewport.ts
@@ -0,0 +1,253 @@
+import { type HeartRateSample } from './trainingTypes';
+
+export type GranularityKind =
+ | 'minute'
+ | 'halfMinute'
+ | 'quarterMinute'
+ | 'tenSeconds'
+ | 'second'
+ | 'halfSecond';
+
+export type Granularity = {
+ readonly kind: GranularityKind;
+ readonly stepMs: number;
+};
+
+const HALF_SECOND_MS = 500;
+const SECOND_MS = 1_000;
+const TEN_SECONDS_MS = 10_000;
+const QUARTER_MINUTE_MS = 15_000;
+const HALF_MINUTE_MS = 30_000;
+const MINUTE_MS = 60_000;
+const FIVE_MINUTES_MS = 5 * MINUTE_MS;
+
+export function granularityForSpan(spanMs: number): Granularity {
+ if (spanMs < 2 * SECOND_MS) {
+ return { kind: 'halfSecond', stepMs: HALF_SECOND_MS };
+ }
+ if (spanMs < TEN_SECONDS_MS) {
+ return { kind: 'second', stepMs: SECOND_MS };
+ }
+ if (spanMs < HALF_MINUTE_MS) {
+ return { kind: 'tenSeconds', stepMs: TEN_SECONDS_MS };
+ }
+ if (spanMs < MINUTE_MS) {
+ return { kind: 'quarterMinute', stepMs: QUARTER_MINUTE_MS };
+ }
+ if (spanMs < FIVE_MINUTES_MS) {
+ return { kind: 'halfMinute', stepMs: HALF_MINUTE_MS };
+ }
+ return { kind: 'minute', stepMs: MINUTE_MS };
+}
+
+function pad2(value: number): string {
+ return value < 10 ? `0${value}` : `${value}`;
+}
+
+export function formatTickLabel(
+ timestampMs: number,
+ granularity: Granularity,
+): string {
+ const date = new Date(timestampMs);
+ const hours = pad2(date.getHours());
+ const minutes = pad2(date.getMinutes());
+ const seconds = pad2(date.getSeconds());
+
+ if (granularity.kind === 'minute') {
+ return `${hours}:${minutes}`;
+ }
+
+ if (granularity.kind === 'halfSecond') {
+ const tenths = Math.floor(date.getMilliseconds() / 100);
+ return `${hours}:${minutes}:${seconds}.${tenths}`;
+ }
+
+ return `${hours}:${minutes}:${seconds}`;
+}
+
+export type Viewport = {
+ readonly startMs: number;
+ readonly endMs: number;
+ readonly liveTail: boolean;
+};
+
+export type BuildViewportOptions = {
+ readonly defaultSpanMs: number;
+ readonly liveTail?: boolean;
+};
+
+export function buildChartViewport(
+ samples: readonly HeartRateSample[],
+ options: BuildViewportOptions,
+): Viewport | null {
+ if (samples.length < 2) {
+ return null;
+ }
+
+ const sortedSamples = samples
+ .slice()
+ .sort((left, right) => left.timestamp - right.timestamp);
+ const firstTimestamp = sortedSamples[0].timestamp;
+ const lastTimestamp = sortedSamples[sortedSamples.length - 1].timestamp;
+ const totalSpan = lastTimestamp - firstTimestamp;
+ const liveTail = options.liveTail ?? true;
+
+ if (totalSpan <= 0) {
+ return {
+ startMs: firstTimestamp,
+ endMs: firstTimestamp + options.defaultSpanMs,
+ liveTail,
+ };
+ }
+
+ if (totalSpan <= options.defaultSpanMs) {
+ return {
+ startMs: firstTimestamp,
+ endMs: lastTimestamp,
+ liveTail,
+ };
+ }
+
+ return {
+ startMs: lastTimestamp - options.defaultSpanMs,
+ endMs: lastTimestamp,
+ liveTail,
+ };
+}
+
+const DEFAULT_MIN_SPAN_MS = 1_000;
+const DEFAULT_MAX_SPAN_MS = 24 * 60 * 60 * 1_000;
+const DEFAULT_SNAP_FRACTION = 0.05;
+
+export type ZoomOptions = {
+ readonly minSpanMs?: number;
+ readonly maxSpanMs?: number;
+};
+
+export function applyZoomIn(
+ viewport: Viewport,
+ focusMs: number,
+ options: ZoomOptions = {},
+): Viewport {
+ const minSpan = options.minSpanMs ?? DEFAULT_MIN_SPAN_MS;
+ const maxSpan = options.maxSpanMs ?? DEFAULT_MAX_SPAN_MS;
+ const currentSpan = viewport.endMs - viewport.startMs;
+ const targetSpan = clampSpan(currentSpan / 2, minSpan, maxSpan);
+ return zoomAroundFocus(viewport, focusMs, targetSpan);
+}
+
+export function applyZoomOut(
+ viewport: Viewport,
+ focusMs: number,
+ options: ZoomOptions = {},
+): Viewport {
+ const minSpan = options.minSpanMs ?? DEFAULT_MIN_SPAN_MS;
+ const maxSpan = options.maxSpanMs ?? DEFAULT_MAX_SPAN_MS;
+ const currentSpan = viewport.endMs - viewport.startMs;
+ const targetSpan = clampSpan(currentSpan * 2, minSpan, maxSpan);
+ return zoomAroundFocus(viewport, focusMs, targetSpan);
+}
+
+function clampSpan(span: number, min: number, max: number): number {
+ if (span < min) return min;
+ if (span > max) return max;
+ return span;
+}
+
+function zoomAroundFocus(
+ viewport: Viewport,
+ focusMs: number,
+ newSpan: number,
+): Viewport {
+ const currentSpan = Math.max(1, viewport.endMs - viewport.startMs);
+ const focusRatio = (focusMs - viewport.startMs) / currentSpan;
+ const newStart = focusMs - focusRatio * newSpan;
+ return {
+ startMs: newStart,
+ endMs: newStart + newSpan,
+ liveTail: false,
+ };
+}
+
+export function applyPan(
+ viewport: Viewport,
+ dxPixels: number,
+ pixelsPerMs: number,
+): Viewport {
+ if (pixelsPerMs === 0) {
+ return {
+ startMs: viewport.startMs,
+ endMs: viewport.endMs,
+ liveTail: false,
+ };
+ }
+ const dxMs = dxPixels / pixelsPerMs;
+ return {
+ startMs: viewport.startMs + dxMs,
+ endMs: viewport.endMs + dxMs,
+ liveTail: false,
+ };
+}
+
+export function isLiveTailSnap(
+ viewport: Viewport,
+ lastSampleTimestampMs: number,
+ snapFraction: number = DEFAULT_SNAP_FRACTION,
+): boolean {
+ const visibleSpan = viewport.endMs - viewport.startMs;
+ if (visibleSpan <= 0) return false;
+ const distance = lastSampleTimestampMs - viewport.endMs;
+ if (distance < 0) return false;
+ return distance <= visibleSpan * snapFraction;
+}
+
+export function pickSampleAtTimestamp(
+ samples: readonly HeartRateSample[],
+ timestampMs: number,
+ toleranceMs: number,
+): HeartRateSample | null {
+ if (samples.length === 0) {
+ return null;
+ }
+
+ let closest: HeartRateSample | null = null;
+ let closestDistance = Number.POSITIVE_INFINITY;
+
+ for (const candidate of samples) {
+ const distance = Math.abs(candidate.timestamp - timestampMs);
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closest = candidate;
+ }
+ }
+
+ return closestDistance <= toleranceMs ? closest : null;
+}
+
+export function xFromTimestampMs(
+ timestampMs: number,
+ viewport: Viewport,
+ chartWidth: number,
+ padX: number,
+): number {
+ const span = Math.max(1, viewport.endMs - viewport.startMs);
+ const ratio = (timestampMs - viewport.startMs) / span;
+ return padX + ratio * (chartWidth - 2 * padX);
+}
+
+export function timestampMsFromX(
+ x: number,
+ viewport: Viewport,
+ chartWidth: number,
+ padX: number,
+): number {
+ const usableWidth = chartWidth - 2 * padX;
+ if (usableWidth <= 0) {
+ return viewport.startMs;
+ }
+ const ratio = (x - padX) / usableWidth;
+ const ts = viewport.startMs + ratio * (viewport.endMs - viewport.startMs);
+ if (ts < viewport.startMs) return viewport.startMs;
+ if (ts > viewport.endMs) return viewport.endMs;
+ return ts;
+}