diff --git a/frontend/.env.example b/frontend/.env.example
index 877160b..58135df 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -1,2 +1,14 @@
-# Android emulator: 10.0.2.2 reaches the host machine.
-EXPO_PUBLIC_API_URL=http://10.0.2.2:8000/api/v1
+# Baidu Maps browser-side AK with JavaScript API enabled.
+# Allow localhost for web development and https://timeflow.local/* for the native WebView.
+EXPO_PUBLIC_BAIDU_MAP_AK=replace-with-your-baidu-map-ak
+
+# Real backend WebSocket URL (required for release / production builds).
+# The client appends its persisted device_id query parameter automatically.
+# Local device example: ws://192.168.1.10:8000/ws
+# Production example: wss://api.example.com/ws
+EXPO_PUBLIC_WS_URL=
+
+# Use in-process FakeWsServer when EXPO_PUBLIC_WS_URL is empty.
+# In __DEV__, Fake is allowed by default if this is unset.
+# Release builds always ignore this flag and fail fast without a URL.
+# EXPO_PUBLIC_USE_FAKE_WS=true
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 91d1261..650b43f 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -1,12 +1,15 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
# Expo
.expo/
+dist/
web-build/
expo-env.d.ts
-# Metro
-.metro-health-check*
-
-# Native signing
+# Native
.kotlin/
*.orig.*
*.jks
@@ -15,8 +18,46 @@ expo-env.d.ts
*.key
*.mobileprovision
-# generated native folders
-# Leading slash keeps these to this directory only — a bare `android/` would
-# match a directory of that name at any depth.
+# Metro
+.metro-health-check*
+
+# debug
+*.log
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# test coverage
+coverage/
+
+# macOS / Windows
+.DS_Store
+Thumbs.db
+*.pem
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# local env files
+.env*.local
+.env
+!.env.example
+
+# typescript
+*.tsbuildinfo
+
+# generated native folders — the repo root does not cover these
+# Leading slash is required: a bare `android/` matches a directory of that
+# name at any depth, not just the prebuild output sitting here.
/ios
/android
+
+# local build artifacts & previews
+*.apk
+alarm-ring-preview.html
+
+# local source backups
+_backup*
diff --git a/frontend/App.tsx b/frontend/App.tsx
index ac3548e..d491a66 100644
--- a/frontend/App.tsx
+++ b/frontend/App.tsx
@@ -1,5 +1,16 @@
-import { HomeScreen } from './src/screens/HomeScreen';
+import { AppRoot } from '@/app/AppRoot';
+import { AppProviders } from '@/app/providers';
+import { useAlarmPermissionsOnLaunch } from '@/features/reminder';
+
+function Root() {
+ useAlarmPermissionsOnLaunch();
+ return ;
+}
export default function App() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/frontend/__tests__/app/AppRoot.test.tsx b/frontend/__tests__/app/AppRoot.test.tsx
new file mode 100644
index 0000000..94e6840
--- /dev/null
+++ b/frontend/__tests__/app/AppRoot.test.tsx
@@ -0,0 +1,254 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { BackHandler } from 'react-native';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { makeSchedule } from '@test/fixtures';
+import { AppDialogProvider } from '@/shared/components/AppDialogProvider';
+
+const mockSaveDraft = jest.fn(async () => makeSchedule());
+const mockToggle = jest.fn(async () => undefined);
+const mockDelete = jest.fn(async () => undefined);
+const mockItems = [makeSchedule({ id: 'nav_1', title: '导航日程' })];
+
+jest.mock('@/features/schedule', () => ({
+ useScheduleCommands: () => ({
+ items: mockItems,
+ ready: true,
+ mutation: { status: 'idle', error: null, pendingId: null },
+ saveDraft: mockSaveDraft,
+ toggleScheduleDone: mockToggle,
+ deleteSchedule: mockDelete,
+ service: {},
+ }),
+ ScheduleProvider: ({ children }: { children: unknown }) => children,
+ upsertDraftForSchedule: (item: {
+ id: string;
+ title: string;
+ source_mode: string;
+ schedule_type: string;
+ }) => ({
+ schedule_id: item.id,
+ source_mode: item.source_mode,
+ schedule_type: item.schedule_type,
+ title: item.title,
+ }),
+ scheduleDraftFromVoiceParse: (draft: { title: string; schedule_type: string }) => ({
+ source_mode: 'voice',
+ schedule_type: draft.schedule_type,
+ title: draft.title,
+ }),
+ useSessionSavedLocations: () => ({ locations: [], upsert: jest.fn() }),
+ ScheduleScreen: ({
+ onCreate,
+ onEditSchedule,
+ scheduleItems,
+ }: {
+ onCreate: () => void;
+ onEditSchedule: (item: (typeof mockItems)[number]) => void;
+ scheduleItems: typeof mockItems;
+ }) => {
+ const { Pressable, Text } = require('react-native');
+ return (
+ <>
+ {scheduleItems[0]?.title}
+
+ create
+
+ onEditSchedule(scheduleItems[0]!)}>
+ edit
+
+ >
+ );
+ },
+ StandardCreateModal: ({
+ onClose,
+ onSave,
+ onUpsertLocation,
+ initialDraft,
+ visible,
+ }: {
+ onClose: () => void;
+ onSave: (draft: unknown) => void | Promise;
+ onUpsertLocation: (location: {
+ id: string;
+ address: string;
+ latitude: number;
+ longitude: number;
+ }) => void;
+ initialDraft?: { schedule_id?: string | null; title?: string } | null;
+ visible: boolean;
+ }) => {
+ const React = require('react') as typeof import('react');
+ const { Pressable, Text } = require('react-native');
+ const [saveError, setSaveError] = React.useState('');
+ if (!visible) return null;
+ return (
+ <>
+ {initialDraft ? `editing:${initialDraft.title}` : 'creating'}
+ {saveError ? {saveError} : null}
+ {
+ setSaveError('');
+ void Promise.resolve(
+ onSave({
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '保存的',
+ start_time: new Date(Date.now() + 60_000).toISOString(),
+ }),
+ ).catch((error: unknown) => {
+ setSaveError(error instanceof Error ? error.message : '保存失败');
+ });
+ }}
+ >
+ save
+
+
+ onUpsertLocation({
+ id: 'loc_x',
+ address: 'A',
+ latitude: 1,
+ longitude: 2,
+ })
+ }
+ >
+ loc
+
+
+ close
+
+ >
+ );
+ },
+}));
+
+jest.mock('@/features/assistant', () => ({
+ AssistantChatSheet: () => null,
+ AssistantDock: () => null,
+ useAssistantSession: () => ({
+ messages: [],
+ handleVoiceStart: jest.fn(async () => undefined),
+ handleVoiceEnd: jest.fn(async () => undefined),
+ handleVoiceCancel: jest.fn(),
+ handleAction: jest.fn(async () => undefined),
+ }),
+}));
+
+jest.mock('@/app/overlay/OverlayProvider', () => {
+ let sequence = 0;
+ return {
+ OverlayProvider: ({ children }: { children: unknown }) => children,
+ useOverlay: () => {
+ const React = require('react') as typeof import('react');
+ const [stack, setStack] = React.useState<
+ { id: string; kind: string; onClose?: () => void }[]
+ >([]);
+ const push = React.useCallback((entry: { kind: string; onClose?: () => void }) => {
+ const id = `overlay_test_${++sequence}`;
+ setStack((current) => [...current, { ...entry, id }]);
+ return id;
+ }, []);
+ const popKind = React.useCallback((kind: string) => {
+ setStack((current) => {
+ const index = current.map((entry) => entry.kind).lastIndexOf(kind);
+ if (index < 0) return current;
+ current[index]?.onClose?.();
+ return current.filter((_, currentIndex) => currentIndex !== index);
+ });
+ }, []);
+ const pop = React.useCallback(() => {
+ setStack((current) => current.slice(0, -1));
+ }, []);
+ return {
+ stack,
+ push,
+ pop,
+ popKind,
+ isOpen: (kind: string) => stack.some((entry) => entry.kind === kind),
+ top: stack.at(-1) ?? null,
+ };
+ },
+ };
+});
+
+jest.mock('@/app/session/SessionProvider', () => ({
+ useSession: () => ({
+ deviceId: 'device_test',
+ userId: 'user_test',
+ connectionStatus: 'ready',
+ transportMode: 'fake',
+ sessionEpoch: 1,
+ client: {
+ connect: async () => undefined,
+ close: () => undefined,
+ onStatus: () => () => undefined,
+ onMessage: () => () => undefined,
+ sendJson: () => undefined,
+ sendBinary: () => undefined,
+ request: async () => ({ ok: true }),
+ },
+ fakeServer: null,
+ connectionError: null,
+ }),
+}));
+
+import { AppRoot } from '@/app/AppRoot';
+
+function renderAppRoot() {
+ return render(
+
+
+ ,
+ );
+}
+
+describe('AppRoot', () => {
+ it('opens create and edit sheets and saves drafts', async () => {
+ renderAppRoot();
+ expect(screen.getByText('导航日程')).toBeTruthy();
+
+ fireEvent.press(screen.getByLabelText('mock-create'));
+ expect(screen.getByText('creating')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('mock-save-draft'));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ expect(mockSaveDraft).toHaveBeenCalled();
+
+ fireEvent.press(screen.getByLabelText('mock-create'));
+ fireEvent.press(screen.getByLabelText('mock-upsert-loc'));
+ fireEvent.press(screen.getByLabelText('mock-close-create'));
+
+ fireEvent.press(screen.getByLabelText('mock-edit'));
+ expect(screen.getByText(/editing:/)).toBeTruthy();
+ });
+
+ it('closes the create sheet on hardware back', () => {
+ let handler: (() => boolean) | null = null;
+ jest.spyOn(BackHandler, 'addEventListener').mockImplementation((_event, cb) => {
+ handler = cb as () => boolean;
+ return { remove: jest.fn() } as never;
+ });
+
+ renderAppRoot();
+ fireEvent.press(screen.getByLabelText('mock-create'));
+ expect(screen.getByText('creating')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('mock-close-create'));
+ expect(screen.queryByText('creating')).toBeNull();
+ void handler;
+ });
+
+ it('returns save failures to the create form', async () => {
+ mockSaveDraft.mockRejectedValueOnce(new Error('日程不存在'));
+ renderAppRoot();
+
+ fireEvent.press(screen.getByLabelText('mock-create'));
+ fireEvent.press(screen.getByLabelText('mock-save-draft'));
+
+ expect(await screen.findByText('日程不存在')).toBeTruthy();
+ expect(screen.getByText('creating')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/app/overlay/OverlayProvider.test.tsx b/frontend/__tests__/app/overlay/OverlayProvider.test.tsx
new file mode 100644
index 0000000..b65fedc
--- /dev/null
+++ b/frontend/__tests__/app/overlay/OverlayProvider.test.tsx
@@ -0,0 +1,61 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import { StrictMode } from 'react';
+import { Pressable, Text, View } from 'react-native';
+
+import { OverlayProvider, useOverlay, type OverlayKind } from '@/app/overlay/OverlayProvider';
+
+function Harness({ onClose }: { onClose: () => void }) {
+ const { pop, popKind, push, stack } = useOverlay();
+
+ const add = (kind: OverlayKind) => {
+ push({ kind, onClose });
+ };
+
+ return (
+ <>
+ {stack.map((entry) => entry.kind).join(',')}
+
+ add('standardCreate')} />
+ add('assistant')} />
+
+ popKind('standardCreate')} />
+
+ >
+ );
+}
+
+function renderHarness(onClose: () => void) {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+describe('OverlayProvider', () => {
+ it('invokes onClose once when the top overlay is popped', () => {
+ const onClose = jest.fn();
+ renderHarness(onClose);
+
+ fireEvent.press(screen.getByLabelText('push-standard'));
+ fireEvent.press(screen.getByLabelText('pop'));
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('removes and closes only the latest matching overlay', () => {
+ const onClose = jest.fn();
+ renderHarness(onClose);
+
+ fireEvent.press(screen.getByLabelText('push-standard'));
+ fireEvent.press(screen.getByLabelText('push-assistant'));
+ fireEvent.press(screen.getByLabelText('push-standard'));
+ fireEvent.press(screen.getByLabelText('pop-standard'));
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ expect(screen.getByText('standardCreate,assistant')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/app/providers.test.tsx b/frontend/__tests__/app/providers.test.tsx
new file mode 100644
index 0000000..19587db
--- /dev/null
+++ b/frontend/__tests__/app/providers.test.tsx
@@ -0,0 +1,73 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { Platform, Text } from 'react-native';
+import { render, screen } from '@testing-library/react-native';
+
+jest.mock('@/app/session/SessionProvider', () => ({
+ SessionProvider: ({ children }: { children: unknown }) => children,
+ useSession: () => ({
+ deviceId: 'device_test',
+ userId: 'user_test',
+ connectionStatus: 'ready',
+ transportMode: 'fake',
+ sessionEpoch: 1,
+ client: {
+ connect: async () => undefined,
+ close: () => undefined,
+ onStatus: () => () => undefined,
+ onMessage: () => () => undefined,
+ sendJson: () => undefined,
+ request: async () => ({ ok: true }),
+ },
+ fakeServer: null,
+ connectionError: null,
+ }),
+}));
+
+jest.mock('@/features/schedule', () => ({
+ ScheduleProvider: ({ children }: { children: unknown }) => children,
+ useScheduleCommands: () => ({
+ items: [],
+ ready: true,
+ mutation: { status: 'idle', error: null, pendingId: null },
+ saveDraft: jest.fn(),
+ toggleScheduleDone: jest.fn(),
+ deleteSchedule: jest.fn(),
+ service: {},
+ }),
+}));
+
+jest.mock('@/app/overlay/OverlayProvider', () => ({
+ OverlayProvider: ({ children }: { children: unknown }) => children,
+ useOverlay: () => ({
+ stack: [],
+ push: jest.fn(),
+ pop: jest.fn(),
+ popKind: jest.fn(),
+ isOpen: jest.fn(() => false),
+ top: null,
+ }),
+}));
+
+import { AppProviders } from '@/app/providers';
+
+describe('AppProviders', () => {
+ it('wraps children for native platforms', () => {
+ const { getByText } = render(
+
+ child
+ ,
+ );
+ expect(getByText('child')).toBeTruthy();
+ });
+
+ it('wraps web children in the desktop frame', () => {
+ (Platform as { OS: string }).OS = 'web';
+ render(
+
+ web-child
+ ,
+ );
+ expect(screen.getByText('web-child')).toBeTruthy();
+ (Platform as { OS: string }).OS = 'ios';
+ });
+});
diff --git a/frontend/__tests__/app/session/sessionEndpoint.test.ts b/frontend/__tests__/app/session/sessionEndpoint.test.ts
new file mode 100644
index 0000000..457c936
--- /dev/null
+++ b/frontend/__tests__/app/session/sessionEndpoint.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { buildSessionWebSocketUrl, resolveSessionUserId } from '@/app/session/sessionEndpoint';
+
+describe('session endpoint compatibility', () => {
+ it('adds the persisted device id to the backend WebSocket URL', () => {
+ expect(buildSessionWebSocketUrl('ws://127.0.0.1:8000/ws', 'device 1')).toBe(
+ 'ws://127.0.0.1:8000/ws?device_id=device+1',
+ );
+ });
+
+ it('replaces a stale device id while preserving other query parameters', () => {
+ expect(
+ buildSessionWebSocketUrl('wss://api.example.com/ws?token=test&device_id=stale', 'current'),
+ ).toBe('wss://api.example.com/ws?token=test&device_id=current');
+ });
+
+ it('rejects non-WebSocket URLs', () => {
+ expect(() => buildSessionWebSocketUrl('http://127.0.0.1:8000/ws', 'device_1')).toThrow(
+ '必须使用 ws:// 或 wss://',
+ );
+ });
+
+ it('uses the MVP backend user when session.ready omits user_id', () => {
+ expect(resolveSessionUserId(undefined)).toBe('default_user');
+ expect(resolveSessionUserId(' user_1 ')).toBe('user_1');
+ });
+});
diff --git a/frontend/__tests__/dev/fakes/schedule/scheduleConflicts.test.ts b/frontend/__tests__/dev/fakes/schedule/scheduleConflicts.test.ts
new file mode 100644
index 0000000..1a66b56
--- /dev/null
+++ b/frontend/__tests__/dev/fakes/schedule/scheduleConflicts.test.ts
@@ -0,0 +1,135 @@
+import { describe, expect, it } from '@jest/globals';
+
+import type { Schedule, ScheduleUpsertCommand } from '@/contracts';
+
+import { upsertSchedule } from '@/dev/fakes/schedule/scheduleConflicts';
+
+function makeSchedule(overrides: Partial = {}): Schedule {
+ return {
+ id: 'schedule_existing',
+ user_id: 'default_user',
+ source_mode: 'manual',
+ schedule_type: 'time',
+ status: 'scheduled',
+ title: '已有日程',
+ notes: null,
+ start_time: new Date(2026, 6, 29, 9, 0).toISOString(),
+ end_time: new Date(2026, 6, 29, 10, 0).toISOString(),
+ timezone: 'Asia/Shanghai',
+ location_name: null,
+ location_address: null,
+ latitude: null,
+ longitude: null,
+ geofence_radius_meters: 100,
+ geofence_armed: false,
+ time_remind_offset_minutes: 15,
+ time_triggered_at: null,
+ geo_triggered_at: null,
+ system_schedule_ref_id: null,
+ system_alarm_ref_id: null,
+ created_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ updated_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ ...overrides,
+ };
+}
+
+function makeCommand(startHour: number, endHour: number | null): ScheduleUpsertCommand {
+ return {
+ type: 'schedule.upsert.command',
+ request_id: 'req_test',
+ payload: {
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '新日程',
+ start_time: new Date(2026, 6, 29, startHour, 0).toISOString(),
+ end_time: endHour === null ? null : new Date(2026, 6, 29, endHour, 0).toISOString(),
+ },
+ };
+}
+
+describe('upsertSchedule conflict detection', () => {
+ const existing = [makeSchedule()];
+
+ it('flags a schedule that overlaps an existing one', () => {
+ const result = upsertSchedule(makeCommand(9, 10), existing, 'schedule_new');
+ expect(result.payload.conflicts.map((conflict) => conflict.schedule_id)).toEqual([
+ 'schedule_existing',
+ ]);
+ });
+
+ it('flags a partial overlap', () => {
+ const result = upsertSchedule(makeCommand(9, 11), existing, 'schedule_new');
+ expect(result.payload.conflicts).toHaveLength(1);
+ });
+
+ it('treats touching boundaries as a conflict', () => {
+ const result = upsertSchedule(makeCommand(10, 11), existing, 'schedule_new');
+ expect(result.payload.conflicts).toHaveLength(1);
+ });
+
+ it('reports nothing for a non-overlapping slot', () => {
+ const result = upsertSchedule(makeCommand(11, 12), existing, 'schedule_new');
+ expect(result.payload.conflicts).toHaveLength(0);
+ });
+
+ it('does not conflict a schedule with itself while editing', () => {
+ const result = upsertSchedule(makeCommand(9, 10), existing, 'schedule_existing');
+ expect(result.payload.conflicts).toHaveLength(0);
+ });
+
+ it('ignores deleted schedules', () => {
+ const deleted = [makeSchedule({ status: 'deleted' })];
+ const result = upsertSchedule(makeCommand(9, 10), deleted, 'schedule_new');
+ expect(result.payload.conflicts).toHaveLength(0);
+ });
+
+ it('reports nothing when the new schedule has no start time', () => {
+ const command: ScheduleUpsertCommand = {
+ type: 'schedule.upsert.command',
+ request_id: 'req_test',
+ payload: {
+ source_mode: 'manual',
+ schedule_type: 'location',
+ title: '地点日程',
+ start_time: null,
+ },
+ };
+ const result = upsertSchedule(command, existing, 'schedule_new');
+ expect(result.payload.conflicts).toHaveLength(0);
+ });
+
+ it('echoes the request id and schedule id back', () => {
+ const result = upsertSchedule(makeCommand(14, 15), existing, 'schedule_new');
+ expect(result.request_id).toBe('req_test');
+ expect(result.payload.schedule_id).toBe('schedule_new');
+ });
+
+ it('ignores unparseable start times on the new command', () => {
+ const command: ScheduleUpsertCommand = {
+ type: 'schedule.upsert.command',
+ request_id: 'req_test',
+ payload: {
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '坏时间',
+ start_time: 'not-a-date',
+ end_time: null,
+ },
+ };
+ expect(upsertSchedule(command, existing, 'schedule_new').payload.conflicts).toHaveLength(0);
+ });
+
+ it('ignores existing items whose times do not parse', () => {
+ const broken = [makeSchedule({ id: 'broken', start_time: 'bad', end_time: 'also-bad' })];
+ expect(
+ upsertSchedule(makeCommand(9, 10), broken, 'schedule_new').payload.conflicts,
+ ).toHaveLength(0);
+ });
+
+ it('inherits geofence_armed from the existing schedule when omitted', () => {
+ const armed = [makeSchedule({ id: 'schedule_existing', geofence_armed: true })];
+ const command = makeCommand(14, 15);
+ delete (command.payload as { geofence_armed?: boolean }).geofence_armed;
+ expect(upsertSchedule(command, armed, 'schedule_existing').payload.geofence_armed).toBe(true);
+ });
+});
diff --git a/frontend/__tests__/features/assistant/components/AssistantChatSheet.test.tsx b/frontend/__tests__/features/assistant/components/AssistantChatSheet.test.tsx
new file mode 100644
index 0000000..50ae6d7
--- /dev/null
+++ b/frontend/__tests__/features/assistant/components/AssistantChatSheet.test.tsx
@@ -0,0 +1,77 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { AssistantChatSheet } from '@/features/assistant/components/AssistantChatSheet';
+
+describe('AssistantChatSheet', () => {
+ it('shows empty state and closes', () => {
+ const onClose = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('等你说第一句话')).toBeTruthy();
+ fireEvent.press(screen.getAllByLabelText('关闭语音助手')[0]!);
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('renders user, draft and assistant messages', () => {
+ const onAction = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('明天下午开会')).toBeTruthy();
+ expect(screen.getByText('开会')).toBeTruthy();
+ expect(screen.getByText('已记下')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('加入'));
+ expect(onAction).toHaveBeenCalledWith('d1', { id: 'ok', kind: 'confirm', label: '加入' });
+ });
+
+ it('hides when not visible', () => {
+ render(
+ ,
+ );
+ expect(screen.queryByText('语音助手')).toBeNull();
+ });
+
+ it('shows a processing state after recording is released', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('正在整理录音…')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/assistant/components/AssistantDock.test.tsx b/frontend/__tests__/features/assistant/components/AssistantDock.test.tsx
new file mode 100644
index 0000000..553fe6c
--- /dev/null
+++ b/frontend/__tests__/features/assistant/components/AssistantDock.test.tsx
@@ -0,0 +1,31 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { AssistantDock } from '@/features/assistant/components/AssistantDock';
+
+jest.mock('@/features/assistant/components/VoiceHoldButton', () => ({
+ VoiceHoldButton: ({ onPress }: { onPress?: () => void }) => {
+ const { Pressable, Text } = require('react-native');
+ return (
+
+ voice-hold
+
+ );
+ },
+}));
+
+describe('AssistantDock', () => {
+ it('hides when requested', () => {
+ const { queryByText } = render(
+ ,
+ );
+ expect(queryByText('voice-hold')).toBeNull();
+ });
+
+ it('shows the voice control when visible', () => {
+ const onOpen = jest.fn();
+ render();
+ fireEvent.press(screen.getByLabelText('mock-open-assistant'));
+ expect(onOpen).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/frontend/__tests__/features/assistant/components/AssistantDraftCard.test.tsx b/frontend/__tests__/features/assistant/components/AssistantDraftCard.test.tsx
new file mode 100644
index 0000000..cac1725
--- /dev/null
+++ b/frontend/__tests__/features/assistant/components/AssistantDraftCard.test.tsx
@@ -0,0 +1,49 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import type { AssistantDraft } from '@/features/assistant/types';
+import { AssistantDraftCard } from '@/features/assistant/components/AssistantDraftCard';
+
+const draft = (overrides: Partial = {}): AssistantDraft => ({
+ title: '下午评审',
+ whenLabel: '今天 15:00',
+ state: 'pending',
+ ...overrides,
+});
+
+describe('AssistantDraftCard', () => {
+ it('shows actions while pending', () => {
+ const onAction = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('待确认')).toBeTruthy();
+ expect(screen.getByText('会议室 A')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('加入'));
+ expect(onAction).toHaveBeenCalledWith({ id: 'ok', kind: 'confirm', label: '加入' });
+ });
+
+ it('hides actions after the draft is resolved', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('已加入日程')).toBeTruthy();
+ expect(screen.queryByLabelText('加入')).toBeNull();
+ });
+
+ it('renders dismissed chip', () => {
+ render();
+ expect(screen.getByText('已忽略')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/assistant/components/TempoAssistantIcon.test.tsx b/frontend/__tests__/features/assistant/components/TempoAssistantIcon.test.tsx
new file mode 100644
index 0000000..09d2059
--- /dev/null
+++ b/frontend/__tests__/features/assistant/components/TempoAssistantIcon.test.tsx
@@ -0,0 +1,13 @@
+import { describe, expect, it } from '@jest/globals';
+import { render } from '@testing-library/react-native';
+
+import { TempoAssistantIcon } from '@/features/assistant/components/TempoAssistantIcon';
+
+describe('TempoAssistantIcon', () => {
+ it('renders with default and custom props', () => {
+ const { rerender, toJSON } = render();
+ expect(toJSON()).toBeTruthy();
+ rerender();
+ expect(toJSON()).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/assistant/components/VoiceHoldButton.test.tsx b/frontend/__tests__/features/assistant/components/VoiceHoldButton.test.tsx
new file mode 100644
index 0000000..2da8f4f
--- /dev/null
+++ b/frontend/__tests__/features/assistant/components/VoiceHoldButton.test.tsx
@@ -0,0 +1,71 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import { Vibration } from 'react-native';
+
+import { VoiceHoldButton } from '@/features/assistant/components/VoiceHoldButton';
+
+describe('VoiceHoldButton', () => {
+ it('renders the hold-to-talk control', () => {
+ render();
+ });
+
+ it('opens the assistant on a tap without starting or ending audio', () => {
+ const onPress = jest.fn();
+ const onVoiceStart = jest.fn();
+ const onVoiceEnd = jest.fn();
+ render(
+ ,
+ );
+
+ fireEvent.press(screen.getByLabelText('轻点打开语音助手,按住说话,上滑取消'));
+
+ expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onVoiceStart).not.toHaveBeenCalled();
+ expect(onVoiceEnd).not.toHaveBeenCalled();
+ });
+
+ it('starts on long press and ends when released', () => {
+ const vibrate = jest.spyOn(Vibration, 'vibrate').mockImplementation(() => undefined);
+ const onPress = jest.fn();
+ const onVoiceStart = jest.fn();
+ const onVoiceEnd = jest.fn();
+ render(
+ ,
+ );
+ const button = screen.getByLabelText('轻点打开语音助手,按住说话,上滑取消');
+ const event = { nativeEvent: { pageY: 200 } };
+
+ fireEvent(button, 'pressIn', event);
+ fireEvent(button, 'longPress', event);
+ fireEvent(button, 'pressOut', event);
+
+ expect(onVoiceStart).toHaveBeenCalledTimes(1);
+ expect(onVoiceEnd).toHaveBeenCalledTimes(1);
+ expect(onPress).not.toHaveBeenCalled();
+ expect(vibrate).toHaveBeenCalledTimes(1);
+ vibrate.mockRestore();
+ });
+
+ it('cancels a long press moved upward before release', () => {
+ const vibrate = jest.spyOn(Vibration, 'vibrate').mockImplementation(() => undefined);
+ const onVoiceCancel = jest.fn();
+ render(
+ ,
+ );
+ const button = screen.getByLabelText('按住说话,松开发送,上滑取消');
+
+ fireEvent(button, 'pressIn', { nativeEvent: { pageY: 200 } });
+ fireEvent(button, 'longPress', { nativeEvent: { pageY: 200 } });
+ fireEvent(button, 'touchMove', { nativeEvent: { pageY: 100 } });
+ fireEvent(button, 'touchMove', { nativeEvent: { pageY: 90 } });
+ fireEvent(button, 'pressOut', { nativeEvent: { pageY: 100 } });
+
+ expect(onVoiceCancel).toHaveBeenCalledTimes(1);
+ expect(vibrate).toHaveBeenCalledTimes(2);
+ vibrate.mockRestore();
+ });
+});
diff --git a/frontend/__tests__/features/assistant/hooks/useAssistantSession.test.tsx b/frontend/__tests__/features/assistant/hooks/useAssistantSession.test.tsx
new file mode 100644
index 0000000..361c4dd
--- /dev/null
+++ b/frontend/__tests__/features/assistant/hooks/useAssistantSession.test.tsx
@@ -0,0 +1,250 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { act, renderHook } from '@testing-library/react-native';
+
+import type { WsJsonMessage } from '@/contracts';
+import type { VoiceRecorder, VoiceTransport } from '@/features/assistant/data/VoiceStreamPort';
+import { useAssistantSession } from '@/features/assistant/hooks/useAssistantSession';
+
+function createVoiceTransport(options: {
+ missingFields?: string[];
+ ambiguousFields?: string[];
+ startGate?: Promise;
+}): VoiceTransport {
+ const listeners = new Set<(message: WsJsonMessage | ArrayBuffer) => void>();
+ let resultRequestId = '';
+
+ return {
+ onMessage(listener) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+ },
+ async request(message: WsJsonMessage & { request_id: string }) {
+ if (message.type === 'voice.stream.start') {
+ await options.startGate;
+ resultRequestId = message.request_id;
+ return {
+ type: 'voice.stream.started',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: 'stream_1', job_id: 'job_1' },
+ } as unknown as T;
+ }
+ if (message.type === 'voice.stream.end') {
+ const parseResult: WsJsonMessage = {
+ type: 'voice.parse.result',
+ request_id: resultRequestId,
+ job_id: 'job_1',
+ status: 'ready_for_confirmation',
+ draft: {
+ schedule_type: 'time',
+ title: '语音日程',
+ start_time: null,
+ },
+ missing_fields: options.missingFields ?? [],
+ ambiguous_fields: options.ambiguousFields ?? [],
+ needs_confirmation: true,
+ };
+ for (const listener of listeners) listener(parseResult);
+ return {
+ type: 'voice.stream.ended',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: 'stream_1', job_id: 'job_1', status: 'processing' },
+ } as unknown as T;
+ }
+ return {
+ type: 'voice.stream.cancelled',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: 'stream_1' },
+ } as unknown as T;
+ },
+ sendBinary() {},
+ };
+}
+
+const recorder: VoiceRecorder = {
+ async start(onChunk) {
+ onChunk(new ArrayBuffer(2));
+ },
+ async stop() {},
+ async cancel() {},
+};
+
+describe('useAssistantSession', () => {
+ it('keeps incomplete parse metadata and does not offer direct confirmation', async () => {
+ const onConfirmDraft = jest.fn(async () => undefined);
+ const { result } = renderHook(() =>
+ useAssistantSession({
+ client: createVoiceTransport({
+ missingFields: ['start_time'],
+ ambiguousFields: ['title'],
+ }),
+ onConfirmDraft,
+ recorder,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleVoiceStart();
+ });
+ await act(async () => {
+ await result.current.handleVoiceEnd();
+ });
+
+ const message = result.current.messages[0];
+ expect(message?.draft?.clarificationLabel).toBe('需要补充:开始时间;需要确认:标题');
+ expect(message?.actions?.map((action) => action.kind)).toEqual(['dismiss']);
+ expect(onConfirmDraft).not.toHaveBeenCalled();
+ });
+
+ it('offers confirmation when the parsed draft is complete', async () => {
+ const onConfirmDraft = jest.fn(async () => undefined);
+ const { result } = renderHook(() =>
+ useAssistantSession({
+ client: createVoiceTransport({}),
+ onConfirmDraft,
+ recorder,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleVoiceStart();
+ });
+ await act(async () => {
+ await result.current.handleVoiceEnd();
+ });
+
+ const message = result.current.messages[0];
+ const confirm = message?.actions?.find((action) => action.kind === 'confirm');
+ expect(confirm).toBeDefined();
+
+ await act(async () => {
+ await result.current.handleAction(message!.id, confirm!);
+ });
+ expect(onConfirmDraft).toHaveBeenCalledTimes(1);
+ expect(result.current.messages[0]?.draft?.state).toBe('added');
+ });
+
+ it('waits for an in-flight start when the user releases immediately', async () => {
+ let releaseStart: () => void = () => undefined;
+ const startGate = new Promise((resolve) => {
+ releaseStart = resolve;
+ });
+ const { result } = renderHook(() =>
+ useAssistantSession({
+ client: createVoiceTransport({ startGate }),
+ onConfirmDraft: async () => undefined,
+ recorder,
+ }),
+ );
+
+ let starting!: Promise;
+ let ending!: Promise;
+ act(() => {
+ starting = result.current.handleVoiceStart();
+ ending = result.current.handleVoiceEnd();
+ });
+
+ await act(async () => {
+ releaseStart();
+ await Promise.all([starting, ending]);
+ });
+
+ expect(result.current.messages).toHaveLength(1);
+ expect(result.current.messages[0]?.draft?.title).toBe('语音日程');
+ });
+
+ it('reports processing while waiting for the final parse result', async () => {
+ let releaseEnd: () => void = () => undefined;
+ const listeners = new Set<(message: WsJsonMessage | ArrayBuffer) => void>();
+ let startRequestId = '';
+ const client: VoiceTransport = {
+ onMessage(listener) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+ },
+ async request(message: WsJsonMessage & { request_id: string }) {
+ if (message.type === 'voice.stream.start') {
+ startRequestId = message.request_id;
+ return {
+ type: 'voice.stream.started',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: 'stream_1', job_id: 'job_1' },
+ } as unknown as T;
+ }
+ await new Promise((resolve) => {
+ releaseEnd = resolve;
+ });
+ for (const listener of listeners) {
+ listener({
+ type: 'voice.parse.result',
+ request_id: startRequestId,
+ job_id: 'job_1',
+ status: 'ready_for_confirmation',
+ draft: { schedule_type: 'time', title: '语音日程', start_time: null },
+ missing_fields: [],
+ ambiguous_fields: [],
+ needs_confirmation: true,
+ });
+ }
+ return {
+ type: 'voice.stream.ended',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: 'stream_1', job_id: 'job_1', status: 'processing' },
+ } as unknown as T;
+ },
+ sendBinary() {},
+ };
+ const { result } = renderHook(() =>
+ useAssistantSession({ client, onConfirmDraft: async () => undefined, recorder }),
+ );
+ await act(async () => {
+ await result.current.handleVoiceStart();
+ });
+
+ let ending!: Promise;
+ act(() => {
+ ending = result.current.handleVoiceEnd();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ });
+ expect(result.current.isProcessing).toBe(true);
+
+ await act(async () => {
+ releaseEnd();
+ await ending;
+ });
+ expect(result.current.isProcessing).toBe(false);
+ });
+
+ it('cancels without parsing when the recorder produced no audio', async () => {
+ const client = createVoiceTransport({});
+ const request = jest.spyOn(client, 'request');
+ const silentRecorder: VoiceRecorder = {
+ async start() {},
+ async stop() {},
+ async cancel() {},
+ };
+ const { result } = renderHook(() =>
+ useAssistantSession({
+ client,
+ onConfirmDraft: async () => undefined,
+ recorder: silentRecorder,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleVoiceStart();
+ await result.current.handleVoiceEnd();
+ });
+
+ const requestTypes = request.mock.calls.map(([message]) => message.type);
+ expect(requestTypes).toEqual(['voice.stream.start', 'voice.stream.cancel']);
+ expect(result.current.messages).toHaveLength(0);
+ expect(result.current.isProcessing).toBe(false);
+ });
+});
diff --git a/frontend/__tests__/features/reminder/hooks/useAlarmPermissionsOnLaunch.test.ts b/frontend/__tests__/features/reminder/hooks/useAlarmPermissionsOnLaunch.test.ts
new file mode 100644
index 0000000..09eb1d7
--- /dev/null
+++ b/frontend/__tests__/features/reminder/hooks/useAlarmPermissionsOnLaunch.test.ts
@@ -0,0 +1,85 @@
+import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
+import { act, renderHook } from '@testing-library/react-native';
+import { AppState, Platform } from 'react-native';
+
+const mockGetStatus = jest.fn();
+const mockOpenSettings = jest.fn(async () => undefined);
+const mockRequestNotifications = jest.fn(async () => true);
+const mockIsSupported = jest.fn(() => true);
+const mockConfirm = jest.fn(async () => false);
+
+jest.mock('@/shared/components/AppDialogProvider', () => ({
+ useAppDialog: () => ({ confirm: mockConfirm, showNotice: jest.fn() }),
+}));
+
+jest.mock('@/features/reminder/native/alarmScheduler', () => ({
+ getAndroidAlarmPermissionStatus: () => mockGetStatus(),
+ openAndroidAlarmPermissionSettings: (...args: unknown[]) =>
+ (mockOpenSettings as (...a: unknown[]) => unknown)(...args),
+ requestAndroidNotificationPermission: () => mockRequestNotifications(),
+ isAndroidAlarmSupported: () => mockIsSupported(),
+}));
+
+import { useAlarmPermissionsOnLaunch } from '@/features/reminder';
+
+describe('useAlarmPermissionsOnLaunch', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ jest.clearAllMocks();
+ (Platform as { OS: string }).OS = 'android';
+ mockIsSupported.mockReturnValue(true);
+ mockConfirm.mockResolvedValue(false);
+ mockGetStatus.mockResolvedValue({
+ exactAlarm: false,
+ overlay: true,
+ fullScreen: true,
+ notifications: true,
+ battery: true,
+ } as never);
+ jest.spyOn(AppState, 'addEventListener').mockReturnValue({ remove: jest.fn() } as never);
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ jest.restoreAllMocks();
+ });
+
+ it('does nothing on non-android platforms', () => {
+ (Platform as { OS: string }).OS = 'ios';
+ renderHook(() => useAlarmPermissionsOnLaunch());
+ act(() => {
+ jest.runOnlyPendingTimers();
+ });
+ expect(mockGetStatus).not.toHaveBeenCalled();
+ });
+
+ it('prompts for the next missing permission and can skip', async () => {
+ renderHook(() => useAlarmPermissionsOnLaunch());
+ await act(async () => {
+ jest.advanceTimersByTime(600);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(mockConfirm).toHaveBeenCalledWith(
+ expect.objectContaining({ title: '需要精确闹钟权限' }),
+ );
+ expect(mockGetStatus).toHaveBeenCalled();
+ });
+
+ it('requests notification permission first when missing', async () => {
+ mockGetStatus.mockResolvedValue({
+ exactAlarm: true,
+ overlay: true,
+ fullScreen: true,
+ notifications: false,
+ battery: true,
+ } as never);
+ renderHook(() => useAlarmPermissionsOnLaunch());
+ await act(async () => {
+ jest.advanceTimersByTime(600);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(mockRequestNotifications).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/features/reminder/native/alarmScheduler.test.ts b/frontend/__tests__/features/reminder/native/alarmScheduler.test.ts
new file mode 100644
index 0000000..f357e29
--- /dev/null
+++ b/frontend/__tests__/features/reminder/native/alarmScheduler.test.ts
@@ -0,0 +1,135 @@
+import { beforeEach, describe, expect, it, jest } from '@jest/globals';
+
+const mockSchedule = jest.fn(async () => ({ alarmId: 'alarm_1' }));
+const mockCancel = jest.fn(async () => true);
+const mockGetPermissionStatus = jest.fn(async () => ({
+ exactAlarm: true,
+ overlay: true,
+ fullScreen: true,
+ notifications: true,
+ battery: true,
+}));
+const mockOpenPermissionSettings = jest.fn(async () => true);
+const mockRequestNotificationPermission = jest.fn(async () => true);
+
+jest.mock('react-native', () => ({
+ Platform: {
+ OS: 'android',
+ select: (specs: Record) => specs.android,
+ },
+ NativeModules: {
+ TimeflowAlarm: {
+ schedule: (...args: unknown[]) => (mockSchedule as (...a: unknown[]) => unknown)(...args),
+ cancel: (...args: unknown[]) => (mockCancel as (...a: unknown[]) => unknown)(...args),
+ getPermissionStatus: (...args: unknown[]) =>
+ (mockGetPermissionStatus as (...a: unknown[]) => unknown)(...args),
+ openPermissionSettings: (...args: unknown[]) =>
+ (mockOpenPermissionSettings as (...a: unknown[]) => unknown)(...args),
+ requestNotificationPermission: (...args: unknown[]) =>
+ (mockRequestNotificationPermission as (...a: unknown[]) => unknown)(...args),
+ },
+ },
+}));
+
+import { Platform } from 'react-native';
+
+import {
+ areAndroidAlarmPermissionsGranted,
+ cancelAndroidAlarm,
+ computeScheduleAlarmTriggerMillis,
+ getAndroidAlarmPermissionStatus,
+ isAndroidAlarmSupported,
+ openAndroidAlarmPermissionSettings,
+ requestAndroidNotificationPermission,
+ scheduleAndroidAlarm,
+} from '@/features/reminder/native/alarmScheduler';
+
+describe('alarmScheduler', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (Platform as { OS: string }).OS = 'android';
+ mockSchedule.mockResolvedValue({ alarmId: 'alarm_1' });
+ mockCancel.mockResolvedValue(true);
+ mockGetPermissionStatus.mockResolvedValue({
+ exactAlarm: true,
+ overlay: true,
+ fullScreen: true,
+ notifications: true,
+ battery: true,
+ });
+ });
+
+ describe('computeScheduleAlarmTriggerMillis', () => {
+ it('returns null without a start time or with an invalid one', () => {
+ expect(computeScheduleAlarmTriggerMillis(null, 0)).toBeNull();
+ expect(computeScheduleAlarmTriggerMillis('not-a-date', 0)).toBeNull();
+ });
+
+ it('returns null when the trigger is not in the future', () => {
+ const past = new Date(Date.now() - 60_000).toISOString();
+ expect(computeScheduleAlarmTriggerMillis(past, 0)).toBeNull();
+ });
+
+ it('subtracts the offset from start_time', () => {
+ const start = Date.now() + 30 * 60_000;
+ const trigger = computeScheduleAlarmTriggerMillis(new Date(start).toISOString(), 10);
+ expect(trigger).toBe(start - 10 * 60_000);
+ });
+
+ it('defaults a missing offset to zero', () => {
+ const start = Date.now() + 60_000;
+ expect(computeScheduleAlarmTriggerMillis(new Date(start).toISOString(), null)).toBe(start);
+ });
+ });
+
+ describe('native wrappers on android', () => {
+ it('reports support when the native module exists', () => {
+ expect(isAndroidAlarmSupported()).toBe(true);
+ });
+
+ it('schedules and returns the alarm id', async () => {
+ await expect(scheduleAndroidAlarm(Date.now() + 60_000, '会议')).resolves.toBe('alarm_1');
+ expect(mockSchedule).toHaveBeenCalled();
+ });
+
+ it('cancels by id and swallows cancel errors', async () => {
+ await cancelAndroidAlarm('alarm_1');
+ expect(mockCancel).toHaveBeenCalledWith('alarm_1');
+ mockCancel.mockRejectedValueOnce(new Error('gone'));
+ await expect(cancelAndroidAlarm('alarm_1')).resolves.toBeUndefined();
+ await expect(cancelAndroidAlarm(null)).resolves.toBeUndefined();
+ });
+
+ it('reads permission status and opens settings', async () => {
+ await expect(getAndroidAlarmPermissionStatus()).resolves.toMatchObject({ exactAlarm: true });
+ await openAndroidAlarmPermissionSettings('exactAlarm');
+ expect(mockOpenPermissionSettings).toHaveBeenCalledWith('exactAlarm');
+ await expect(requestAndroidNotificationPermission()).resolves.toBe(true);
+ await expect(areAndroidAlarmPermissionsGranted()).resolves.toBe(true);
+ });
+
+ it('rejects areAndroidAlarmPermissionsGranted when any flag is false', async () => {
+ mockGetPermissionStatus.mockResolvedValueOnce({
+ exactAlarm: true,
+ overlay: true,
+ fullScreen: true,
+ notifications: false,
+ battery: true,
+ });
+ await expect(areAndroidAlarmPermissionsGranted()).resolves.toBe(false);
+ });
+ });
+
+ describe('unsupported platforms', () => {
+ it('short-circuits when not android', async () => {
+ (Platform as { OS: string }).OS = 'ios';
+ expect(isAndroidAlarmSupported()).toBe(false);
+ await expect(scheduleAndroidAlarm(1, 't')).resolves.toBeNull();
+ await expect(getAndroidAlarmPermissionStatus()).resolves.toBeNull();
+ await expect(requestAndroidNotificationPermission()).resolves.toBe(false);
+ await expect(areAndroidAlarmPermissionsGranted()).resolves.toBe(false);
+ await openAndroidAlarmPermissionSettings('app');
+ await cancelAndroidAlarm('x');
+ });
+ });
+});
diff --git a/frontend/__tests__/features/schedule/application/ScheduleService.test.ts b/frontend/__tests__/features/schedule/application/ScheduleService.test.ts
new file mode 100644
index 0000000..aea4b9c
--- /dev/null
+++ b/frontend/__tests__/features/schedule/application/ScheduleService.test.ts
@@ -0,0 +1,211 @@
+import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
+
+import type {
+ Schedule,
+ ScheduleUpsertCommand,
+ ScheduleUpsertPayload as ScheduleDraft,
+ ScheduleUpsertResponse,
+} from '@/contracts';
+import { ScheduleService } from '@/features/schedule/application/ScheduleService';
+import { ScheduleCache } from '@/features/schedule/data/ScheduleCache';
+import type { ScheduleRepositoryPort } from '@/features/schedule/data/ScheduleRepositoryPort';
+import { makeSchedule } from '@test/fixtures';
+
+function makeDraft(overrides: Partial = {}): ScheduleDraft {
+ return {
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '新会议',
+ start_time: new Date(Date.now() + 3_600_000).toISOString(),
+ end_time: null,
+ time_remind_offset_minutes: 5,
+ ...overrides,
+ };
+}
+
+function upsertOk(command: ScheduleUpsertCommand, id: string): ScheduleUpsertResponse {
+ return {
+ type: 'schedule.upsert.result',
+ request_id: command.request_id,
+ ok: true,
+ payload: {
+ schedule_id: id,
+ schedule_type: command.payload.schedule_type,
+ status: 'scheduled',
+ conflicts: [],
+ geofence_armed: true,
+ },
+ };
+}
+
+describe('ScheduleService', () => {
+ let cache: ScheduleCache;
+ let repository: jest.Mocked;
+ let syncForSchedule: jest.MockedFunction<
+ NonNullable[0]['alarmAdapter']>['syncForSchedule']
+ >;
+ let cancel: jest.MockedFunction<
+ NonNullable[0]['alarmAdapter']>['cancel']
+ >;
+ let notifyConflicts: jest.MockedFunction<
+ NonNullable[0]['notifyConflicts']>
+ >;
+ let service: ScheduleService;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ cache = new ScheduleCache();
+ repository = {
+ list: jest.fn(async () => [] as Schedule[]),
+ upsert: jest.fn(async (command: ScheduleUpsertCommand) =>
+ upsertOk(command, command.payload.schedule_id ?? 'schedule_auto'),
+ ),
+ updateStatus: jest.fn(async (id: string, status: 'scheduled' | 'done') => ({
+ type: 'schedule.status.result' as const,
+ request_id: `req_status_${id}`,
+ ok: true as const,
+ payload: { schedule_id: id, status },
+ })),
+ notifyDeleted: jest.fn(async (id: string) => ({
+ type: 'schedule.deleted.ack' as const,
+ request_id: `req_deleted_${id}`,
+ schedule_id: id,
+ ok: true as const,
+ })),
+ subscribe: jest.fn(() => () => undefined),
+ };
+ syncForSchedule = jest.fn(async () => null);
+ cancel = jest.fn(async () => null);
+ notifyConflicts = jest.fn();
+ service = new ScheduleService({
+ repository,
+ cache,
+ getUserId: () => 'default_user',
+ alarmAdapter: { syncForSchedule, cancel },
+ notifyConflicts,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('bootstraps from repository.list into cache', async () => {
+ const seed = [makeSchedule({ id: 'seed' })];
+ repository.list.mockResolvedValueOnce(seed);
+ await service.bootstrap();
+ expect(service.getItems()).toEqual(seed);
+ });
+
+ it('creates a schedule via upsert and caches it', async () => {
+ const saved = await service.saveDraft(makeDraft({ title: 'A' }));
+ expect(saved.title).toBe('A');
+ expect(saved.id).toBe('schedule_auto');
+ expect(service.getItems()).toHaveLength(1);
+ expect(repository.upsert).toHaveBeenCalled();
+ expect(repository.upsert.mock.calls[0]![0].payload.schedule_id).toBeUndefined();
+ });
+
+ it('alerts when upsert reports conflicts', async () => {
+ repository.upsert.mockImplementation(async (command) => ({
+ type: 'schedule.upsert.result',
+ request_id: command.request_id,
+ ok: true,
+ payload: {
+ schedule_id: command.payload.schedule_id ?? 'x',
+ schedule_type: command.payload.schedule_type,
+ status: 'scheduled',
+ conflicts: [
+ {
+ schedule_id: 'other',
+ title: '已有会议',
+ start_time: new Date().toISOString(),
+ end_time: null,
+ },
+ ],
+ geofence_armed: true,
+ },
+ }));
+
+ await service.saveDraft(makeDraft());
+ expect(notifyConflicts).toHaveBeenCalledWith([expect.objectContaining({ title: '已有会议' })]);
+ });
+
+ it('updates an existing schedule when schedule_id is set', async () => {
+ await service.saveDraft(makeDraft({ schedule_id: 'schedule_edit', title: '旧标题' }));
+ await service.saveDraft(makeDraft({ schedule_id: 'schedule_edit', title: '新标题' }));
+ expect(service.getItems()).toHaveLength(1);
+ expect(service.getItems()[0]?.title).toBe('新标题');
+ });
+
+ it('syncs android alarms when adapter returns an id', async () => {
+ syncForSchedule.mockResolvedValue('alarm_99');
+ const saved = await service.saveDraft(makeDraft({ title: '安卓会议' }));
+ expect(syncForSchedule).toHaveBeenCalled();
+ expect(saved.system_schedule_ref_id).toBe('alarm_99');
+ });
+
+ it('toggles done through updateStatus and re-arms on undo', async () => {
+ syncForSchedule.mockResolvedValue('alarm_old');
+ await service.saveDraft(
+ makeDraft({
+ schedule_id: 'toggle_1',
+ start_time: new Date(Date.now() + 3_600_000).toISOString(),
+ }),
+ );
+ const item = service.getItems()[0]!;
+ await service.toggleDone(item);
+ expect(repository.updateStatus).toHaveBeenCalledWith('toggle_1', 'done');
+ expect(repository.notifyDeleted).not.toHaveBeenCalled();
+ expect(service.getItems()[0]?.status).toBe('done');
+
+ syncForSchedule.mockResolvedValue('alarm_rearm');
+ await service.toggleDone(service.getItems()[0]!);
+ expect(repository.updateStatus).toHaveBeenCalledWith('toggle_1', 'scheduled');
+ expect(service.getItems()[0]?.status).toBe('scheduled');
+ expect(syncForSchedule).toHaveBeenCalled();
+ });
+
+ it('marks a schedule deleted and cancels its alarm', async () => {
+ syncForSchedule.mockResolvedValue('alarm_del');
+ await service.saveDraft(makeDraft({ schedule_id: 'del_1' }));
+ await service.deleteSchedule(service.getItems()[0]!);
+ expect(repository.notifyDeleted).toHaveBeenCalledWith('del_1');
+ expect(cancel).toHaveBeenCalledWith('alarm_del');
+ expect(service.getItems()[0]?.status).toBe('deleted');
+ expect(service.getItems()[0]?.system_schedule_ref_id).toBeNull();
+ });
+
+ it('keeps the alarm reference returned by the platform adapter', async () => {
+ cancel.mockResolvedValue('remote_alarm');
+ cache.replaceAll([makeSchedule({ id: 'remote', system_schedule_ref_id: 'local_alarm' })]);
+
+ await service.deleteSchedule(service.getItems()[0]!);
+
+ expect(service.getItems()[0]?.system_schedule_ref_id).toBe('remote_alarm');
+ });
+
+ it('does not mutate cache or alarms when delete is rejected', async () => {
+ repository.notifyDeleted.mockResolvedValueOnce({
+ type: 'schedule.deleted.ack',
+ request_id: 'req_delete_failed',
+ schedule_id: 'reject',
+ ok: false,
+ error: { code: 'denied', message: '删除被拒绝', details: null },
+ });
+ const schedule = makeSchedule({ id: 'reject', system_schedule_ref_id: 'alarm_reject' });
+ cache.replaceAll([schedule]);
+
+ await expect(service.deleteSchedule(schedule)).rejects.toThrow('删除被拒绝');
+ expect(cancel).not.toHaveBeenCalled();
+ expect(service.getItems()[0]).toEqual(schedule);
+ });
+
+ it('ignores toggle and delete for already deleted items', async () => {
+ cache.replaceAll([makeSchedule({ id: 'gone', status: 'deleted' })]);
+ await service.toggleDone(service.getItems()[0]!);
+ await service.deleteSchedule(service.getItems()[0]!);
+ expect(repository.updateStatus).not.toHaveBeenCalled();
+ expect(repository.notifyDeleted).not.toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/calendar/MonthView.test.tsx b/frontend/__tests__/features/schedule/calendar/MonthView.test.tsx
new file mode 100644
index 0000000..8f8f2e2
--- /dev/null
+++ b/frontend/__tests__/features/schedule/calendar/MonthView.test.tsx
@@ -0,0 +1,62 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { makeSchedule } from '@test/fixtures';
+
+import { MonthView } from '@/features/schedule/calendar/MonthView';
+import { buildScheduleIndex } from '@/features/schedule/calendar/scheduleIndex';
+
+describe('MonthView', () => {
+ const now = new Date(2026, 6, 31);
+ const month = new Date(2026, 6, 1);
+
+ it('navigates months and selects a day with events', () => {
+ const onMonthChange = jest.fn();
+ const onSelectDate = jest.fn();
+ const onOpenSchedule = jest.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText('7月')).toBeTruthy();
+ expect(screen.getByText('月底会议')).toBeTruthy();
+
+ fireEvent.press(screen.getByLabelText('上个月'));
+ expect(onMonthChange).toHaveBeenCalled();
+ fireEvent.press(screen.getByLabelText('下个月'));
+ expect(onMonthChange).toHaveBeenCalledTimes(2);
+
+ fireEvent.press(screen.getByText('月底会议'));
+ expect(onOpenSchedule).toHaveBeenCalledWith('m1');
+ });
+
+ it('shows empty agenda copy when the selected day has no events', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('这一天暂无详细安排')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/calendar/ScheduleRow.test.tsx b/frontend/__tests__/features/schedule/calendar/ScheduleRow.test.tsx
new file mode 100644
index 0000000..5569c86
--- /dev/null
+++ b/frontend/__tests__/features/schedule/calendar/ScheduleRow.test.tsx
@@ -0,0 +1,40 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { makeSchedule } from '@test/fixtures';
+
+import { ScheduleRow } from '@/features/schedule/calendar/ScheduleRow';
+
+describe('ScheduleRow', () => {
+ it('renders title, time and optional meta', () => {
+ const onPress = jest.fn();
+ const onToggle = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('晨会')).toBeTruthy();
+ expect(screen.getByText('会议室')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('09:05 晨会'));
+ expect(onPress).toHaveBeenCalled();
+ fireEvent.press(screen.getByLabelText('完成 晨会'));
+ expect(onToggle).toHaveBeenCalled();
+ });
+
+ it('shows restore affordance for done items in compact mode', () => {
+ const onToggle = jest.fn();
+ render(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('恢复 已做完'));
+ expect(onToggle).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/data/WsScheduleRepository.test.ts b/frontend/__tests__/features/schedule/data/WsScheduleRepository.test.ts
new file mode 100644
index 0000000..6813609
--- /dev/null
+++ b/frontend/__tests__/features/schedule/data/WsScheduleRepository.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it, jest } from '@jest/globals';
+
+import type { WsJsonMessage } from '@/contracts';
+import type { ScheduleTransport } from '@/features/schedule/data/ScheduleTransport';
+import { WsScheduleRepository } from '@/features/schedule/data/WsScheduleRepository';
+
+describe('WsScheduleRepository backend compatibility', () => {
+ it('accepts the MVP delete acknowledgement without request_id', async () => {
+ const request = jest.fn(
+ async (
+ _message: WsJsonMessage & { request_id: string },
+ isMatch?: (response: WsJsonMessage) => boolean,
+ ) => {
+ const response = {
+ type: 'schedule.deleted.ack',
+ schedule_id: 'schedule_1',
+ ok: true,
+ };
+ expect(isMatch?.(response)).toBe(true);
+ return response;
+ },
+ );
+ const transport = {
+ onMessage: () => () => undefined,
+ request,
+ sendJson: () => undefined,
+ } as unknown as ScheduleTransport;
+ const repository = new WsScheduleRepository(transport);
+
+ await expect(repository.notifyDeleted('schedule_1')).resolves.toMatchObject({ ok: true });
+ repository.dispose();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/data/adapters.test.ts b/frontend/__tests__/features/schedule/data/adapters.test.ts
new file mode 100644
index 0000000..e7fbcfe
--- /dev/null
+++ b/frontend/__tests__/features/schedule/data/adapters.test.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { upsertDraftForSchedule } from '@/features/schedule/data/adapters';
+import { makeSchedule } from '@test/fixtures';
+
+describe('upsertDraftForSchedule', () => {
+ it('maps schedule fields into a domain draft', () => {
+ const schedule = makeSchedule({
+ id: 'schedule_42',
+ notes: '备注',
+ location_name: '办公室',
+ location_address: '南京东路1号',
+ latitude: 31.2,
+ longitude: 121.4,
+ geofence_radius_meters: 200,
+ geofence_armed: true,
+ time_remind_offset_minutes: 10,
+ });
+
+ expect(upsertDraftForSchedule(schedule)).toEqual({
+ schedule_id: 'schedule_42',
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '测试日程',
+ notes: '备注',
+ start_time: schedule.start_time,
+ end_time: null,
+ timezone: 'Asia/Shanghai',
+ location_name: '办公室',
+ location_address: '南京东路1号',
+ latitude: 31.2,
+ longitude: 121.4,
+ geofence_radius_meters: 200,
+ geofence_armed: true,
+ time_remind_offset_minutes: 10,
+ });
+ });
+});
diff --git a/frontend/__tests__/features/schedule/detail/ScheduleDetailSheet.test.tsx b/frontend/__tests__/features/schedule/detail/ScheduleDetailSheet.test.tsx
new file mode 100644
index 0000000..3fd7f92
--- /dev/null
+++ b/frontend/__tests__/features/schedule/detail/ScheduleDetailSheet.test.tsx
@@ -0,0 +1,84 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
+import type { ReactElement } from 'react';
+
+import { makeSchedule } from '@test/fixtures';
+import { ScheduleDetailSheet } from '@/features/schedule/detail/ScheduleDetailSheet';
+import { AppDialogProvider } from '@/shared/components/AppDialogProvider';
+
+function renderWithDialog(element: ReactElement) {
+ return render({element});
+}
+
+describe('ScheduleDetailSheet', () => {
+ it('is hidden when schedule is null', () => {
+ renderWithDialog(
+ ,
+ );
+ expect(screen.queryByText('安排详情')).toBeNull();
+ });
+
+ it('shows schedule content and opens the day view', () => {
+ const onClose = jest.fn();
+ const onOpenDay = jest.fn();
+ renderWithDialog(
+ ,
+ );
+ expect(screen.getByText('评审会')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('查看当天日程'));
+ expect(onClose).toHaveBeenCalled();
+ expect(onOpenDay).toHaveBeenCalled();
+ });
+
+ it('offers edit when editable and confirms delete', async () => {
+ const onEdit = jest.fn();
+ const onDelete = jest.fn();
+ const onClose = jest.fn();
+ renderWithDialog(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('编辑日程'));
+ expect(onEdit).toHaveBeenCalled();
+
+ fireEvent.press(screen.getByLabelText('删除日程'));
+ expect(screen.getByText('确定删除这个日程吗?相关提醒也会一并取消。')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('删除'));
+ await waitFor(() => expect(onDelete).toHaveBeenCalled());
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('renders completed status styling', () => {
+ renderWithDialog(
+ ,
+ );
+ expect(screen.getAllByText('已完成').length).toBeGreaterThan(0);
+ expect(screen.getByText('已完成 · 可回顾这次安排')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/domain/scheduleOrdering.test.ts b/frontend/__tests__/features/schedule/domain/scheduleOrdering.test.ts
new file mode 100644
index 0000000..a7c14d6
--- /dev/null
+++ b/frontend/__tests__/features/schedule/domain/scheduleOrdering.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { makeSchedule } from '@test/fixtures';
+
+import { compareSchedules } from '@/features/schedule/domain/scheduleOrdering';
+
+describe('compareSchedules', () => {
+ it('orders by start_time when both have one', () => {
+ const earlier = makeSchedule({
+ id: 'a',
+ start_time: new Date(2026, 6, 29, 8, 0).toISOString(),
+ });
+ const later = makeSchedule({
+ id: 'b',
+ start_time: new Date(2026, 6, 29, 10, 0).toISOString(),
+ });
+ expect(compareSchedules(earlier, later)).toBeLessThan(0);
+ expect(compareSchedules(later, earlier)).toBeGreaterThan(0);
+ });
+
+ it('puts timed schedules before location-only ones', () => {
+ const timed = makeSchedule({ id: 't', start_time: new Date(2026, 6, 29, 9, 0).toISOString() });
+ const locationOnly = makeSchedule({ id: 'l', start_time: null });
+ expect(compareSchedules(timed, locationOnly)).toBe(-1);
+ expect(compareSchedules(locationOnly, timed)).toBe(1);
+ });
+
+ it('falls back to created_at when neither has start_time', () => {
+ const older = makeSchedule({
+ id: 'old',
+ start_time: null,
+ created_at: new Date(2026, 6, 1).toISOString(),
+ });
+ const newer = makeSchedule({
+ id: 'new',
+ start_time: null,
+ created_at: new Date(2026, 6, 20).toISOString(),
+ });
+ expect(compareSchedules(older, newer)).toBeGreaterThan(0);
+ });
+});
diff --git a/frontend/__tests__/features/schedule/editor/DateTimeField.test.tsx b/frontend/__tests__/features/schedule/editor/DateTimeField.test.tsx
new file mode 100644
index 0000000..b730369
--- /dev/null
+++ b/frontend/__tests__/features/schedule/editor/DateTimeField.test.tsx
@@ -0,0 +1,91 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+jest.mock('@/shared/components/DatePickerSheet', () => ({
+ DatePickerSheet: ({
+ visible,
+ onSelect,
+ onClose,
+ }: {
+ visible: boolean;
+ onSelect: (date: Date) => void;
+ onClose: () => void;
+ }) => {
+ const { Pressable, Text } = require('react-native');
+ if (!visible) return null;
+ return (
+ {
+ onSelect(new Date(2026, 7, 1));
+ onClose();
+ }}
+ >
+ mock-date
+
+ );
+ },
+}));
+
+jest.mock('@/shared/components/TimePickerSheet', () => ({
+ TimePickerSheet: ({
+ visible,
+ onSelect,
+ onClose,
+ }: {
+ visible: boolean;
+ onSelect: (value: string) => void;
+ onClose: () => void;
+ }) => {
+ const { Pressable, Text } = require('react-native');
+ if (!visible) return null;
+ return (
+ {
+ onSelect('15:30');
+ onClose();
+ }}
+ >
+ mock-time
+
+ );
+ },
+}));
+
+import { DateTimeField } from '@/features/schedule/editor/DateTimeField';
+
+describe('DateTimeField', () => {
+ it('opens the date sheet and formats the selection', () => {
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('日期'));
+ fireEvent.press(screen.getByLabelText('mock-date-select'));
+ expect(onChange).toHaveBeenCalledWith('2026 / 08 / 01');
+ });
+
+ it('opens the time sheet and returns HH:mm', () => {
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('09:00')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('时间'));
+ fireEvent.press(screen.getByLabelText('mock-time-select'));
+ expect(onChange).toHaveBeenCalledWith('15:30');
+ });
+});
diff --git a/frontend/__tests__/features/schedule/editor/StandardCreateSheet.test.tsx b/frontend/__tests__/features/schedule/editor/StandardCreateSheet.test.tsx
new file mode 100644
index 0000000..d369d72
--- /dev/null
+++ b/frontend/__tests__/features/schedule/editor/StandardCreateSheet.test.tsx
@@ -0,0 +1,190 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
+
+import { StandardCreateSheet } from '@/features/schedule/editor/StandardCreateSheet';
+import type { ScheduleUpsertPayload as ScheduleDraft } from '@/contracts';
+
+function pressPrimaryAction(label: string) {
+ const matches = screen.getAllByText(label);
+ fireEvent.press(matches[matches.length - 1]!);
+}
+
+describe('StandardCreateSheet', () => {
+ const baseProps = {
+ onClose: jest.fn(),
+ onSave: jest.fn(async (_draft: ScheduleDraft) => undefined),
+ onUpsertLocation: jest.fn(),
+ savedLocations: [] as [],
+ };
+
+ it('rejects an empty title', async () => {
+ render();
+ pressPrimaryAction('添加日程');
+ expect(await screen.findByText('请填写日程标题。')).toBeTruthy();
+ expect(baseProps.onSave).not.toHaveBeenCalled();
+ });
+
+ it('creates a time schedule with a future start', async () => {
+ const onSave = jest.fn(async (_draft: ScheduleDraft) => undefined);
+
+ render();
+ fireEvent.changeText(screen.getByPlaceholderText('请输入日程标题'), '项目评审');
+ pressPrimaryAction('添加日程');
+
+ await waitFor(() => expect(onSave).toHaveBeenCalled());
+ const draft = onSave.mock.calls[0]![0];
+ expect(draft.title).toBe('项目评审');
+ expect(draft.schedule_type).toBe('time');
+ expect(draft.start_time).toBeTruthy();
+ });
+
+ it('rejects a start time that is not in the future', async () => {
+ const past = new Date(Date.now() - 120_000);
+ render(
+ ,
+ );
+ pressPrimaryAction('保存修改');
+ expect(await screen.findByText('开始时间需晚于当前分钟,请选择下一分钟及以后。')).toBeTruthy();
+ });
+
+ it('surfaces save errors from onSave', async () => {
+ const onSave = jest.fn(async () => {
+ throw new Error('网络异常');
+ });
+ render();
+ fireEvent.changeText(screen.getByPlaceholderText('请输入日程标题'), '会失败');
+ pressPrimaryAction('添加日程');
+ expect(await screen.findByText('网络异常')).toBeTruthy();
+ });
+
+ it('surfaces non-Error save failures', async () => {
+ const onSave = jest.fn(async () => {
+ throw 'boom';
+ });
+ render();
+ fireEvent.changeText(screen.getByPlaceholderText('请输入日程标题'), '会失败');
+ pressPrimaryAction('添加日程');
+ expect(await screen.findByText('保存失败,请稍后重试。')).toBeTruthy();
+ });
+
+ it('saves a location-only schedule', async () => {
+ const onSave = jest.fn(async (_draft: ScheduleDraft) => undefined);
+ const location = {
+ id: 'loc_1',
+ address: '南京东路1号',
+ latitude: 31.2,
+ longitude: 121.5,
+ name: '办公室',
+ };
+ render(
+ ,
+ );
+ // Clearing the date also clears start/end, so the schedule becomes location-only.
+ fireEvent.press(screen.getByLabelText('清除日期'));
+ pressPrimaryAction('添加日程');
+ await waitFor(() => expect(onSave).toHaveBeenCalled());
+ expect(onSave.mock.calls[0]![0].schedule_type).toBe('location');
+ });
+
+ it('rejects end time earlier than start', async () => {
+ const future = new Date(Date.now() + 3_600_000);
+ const later = new Date(Date.now() + 7_200_000);
+ render(
+ ,
+ );
+ pressPrimaryAction('保存修改');
+ expect(await screen.findByText('结束时间不能早于开始时间。')).toBeTruthy();
+ });
+
+ it('rejects a negative remind offset', async () => {
+ const future = new Date(Date.now() + 3_600_000);
+ render(
+ ,
+ );
+ fireEvent.changeText(screen.getByLabelText('提前提醒分钟数'), '-1');
+ pressPrimaryAction('保存修改');
+ expect(await screen.findByText('提前提醒分钟数必须是非负整数。')).toBeTruthy();
+ });
+
+ it('rejects invalid geofence radius for location schedules', async () => {
+ render(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('清除日期'));
+ fireEvent.changeText(screen.getByLabelText('地理围栏半径'), '0');
+ pressPrimaryAction('添加日程');
+ expect(await screen.findByText('地理围栏半径必须是大于 0 的整数。')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/editor/datetime.test.ts b/frontend/__tests__/features/schedule/editor/datetime.test.ts
new file mode 100644
index 0000000..10bfce8
--- /dev/null
+++ b/frontend/__tests__/features/schedule/editor/datetime.test.ts
@@ -0,0 +1,126 @@
+import { describe, expect, it } from '@jest/globals';
+
+import {
+ currentTimezone,
+ dateAndTimeFromIso,
+ defaultCreateDateAndTime,
+ formatDateValue,
+ isoFromDateAndTime,
+ optionalNumber,
+ parseDateValue,
+ parsePickerValue,
+ parseTimeValue,
+} from '@/features/schedule/editor/datetime';
+import { formatTimeValue } from '@/shared/utils/date';
+
+describe('parseDateValue', () => {
+ it('round-trips the format the field renders', () => {
+ expect(formatDateValue(parseDateValue('2026 / 07 / 29'))).toBe('2026 / 07 / 29');
+ });
+
+ it('accepts any non-digit separator', () => {
+ expect(formatDateValue(parseDateValue('2026-7-29'))).toBe('2026 / 07 / 29');
+ });
+
+ it('falls back to today when the input is incomplete', () => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ expect(parseDateValue('2026 / 07').getTime()).toBe(today.getTime());
+ });
+});
+
+describe('parseTimeValue', () => {
+ it('reads hours and minutes', () => {
+ expect(formatTimeValue(parseTimeValue('09:05'))).toBe('09:05');
+ });
+
+ it('accepts a single-digit hour', () => {
+ expect(formatTimeValue(parseTimeValue('9:05'))).toBe('09:05');
+ });
+});
+
+describe('isoFromDateAndTime', () => {
+ it('combines the two fields into one instant', () => {
+ expect(isoFromDateAndTime('2026 / 07 / 29', '09:05')).toBe(
+ new Date(2026, 6, 29, 9, 5).toISOString(),
+ );
+ });
+
+ it('returns null when the time does not parse', () => {
+ expect(isoFromDateAndTime('2026 / 07 / 29', '9am')).toBeNull();
+ });
+
+ it('returns null when the date is incomplete', () => {
+ expect(isoFromDateAndTime('2026 / 07', '09:05')).toBeNull();
+ });
+});
+
+describe('defaultCreateDateAndTime', () => {
+ it('defaults to the next whole minute', () => {
+ expect(defaultCreateDateAndTime(new Date(2026, 6, 30, 13, 58, 40, 123))).toEqual({
+ date: '2026 / 07 / 30',
+ time: '13:59',
+ });
+ });
+
+ it('rolls to the next day near midnight', () => {
+ expect(defaultCreateDateAndTime(new Date(2026, 6, 30, 23, 59, 10))).toEqual({
+ date: '2026 / 07 / 31',
+ time: '00:00',
+ });
+ });
+});
+
+describe('dateAndTimeFromIso', () => {
+ it('returns empty strings for a missing value', () => {
+ expect(dateAndTimeFromIso(null)).toEqual({ date: '', time: '' });
+ });
+
+ it('returns empty strings for an unparseable value', () => {
+ expect(dateAndTimeFromIso('not-a-date')).toEqual({ date: '', time: '' });
+ });
+
+ it('splits an ISO string back into the two fields', () => {
+ expect(dateAndTimeFromIso(new Date(2026, 6, 29, 9, 5).toISOString())).toEqual({
+ date: '2026 / 07 / 29',
+ time: '09:05',
+ });
+ });
+});
+
+describe('optionalNumber', () => {
+ it('treats blank input as absent rather than zero', () => {
+ expect(optionalNumber('')).toBeNull();
+ expect(optionalNumber(' ')).toBeNull();
+ });
+
+ it('rejects non-numeric input', () => {
+ expect(optionalNumber('abc')).toBeNull();
+ });
+
+ it('keeps negative and decimal values', () => {
+ expect(optionalNumber('-31.2451')).toBe(-31.2451);
+ expect(optionalNumber('0')).toBe(0);
+ });
+});
+
+describe('parsePickerValue', () => {
+ it('delegates to date or time parsers by mode', () => {
+ expect(formatDateValue(parsePickerValue('2026 / 07 / 29', 'date'))).toBe('2026 / 07 / 29');
+ expect(formatTimeValue(parsePickerValue('09:05', 'time'))).toBe('09:05');
+ });
+});
+
+describe('parseTimeValue fallback', () => {
+ it('keeps the current clock when the string does not match', () => {
+ const parsed = parseTimeValue('bad');
+ expect(parsed.getSeconds()).toBe(0);
+ expect(parsed.getMilliseconds()).toBe(0);
+ });
+});
+
+describe('currentTimezone', () => {
+ it('returns the runtime timezone when Intl is available', () => {
+ expect(typeof currentTimezone()).toBe('string');
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/AddressEditorSheet.test.tsx b/frontend/__tests__/features/schedule/location/AddressEditorSheet.test.tsx
new file mode 100644
index 0000000..5a19bb7
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/AddressEditorSheet.test.tsx
@@ -0,0 +1,39 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+jest.mock('@/features/schedule/location/MapPicker', () => ({
+ MapPicker: () => null,
+}));
+
+import { AddressEditorSheet } from '@/features/schedule/location/AddressEditorSheet';
+
+describe('AddressEditorSheet', () => {
+ it('requires a map location before save', () => {
+ const onSave = jest.fn();
+ render();
+ fireEvent.press(screen.getByLabelText('保存地点'));
+ expect(screen.getByText('请选择一个地图位置')).toBeTruthy();
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it('saves with an optional name', () => {
+ const onSave = jest.fn();
+ render(
+ ,
+ );
+ fireEvent.changeText(screen.getByLabelText('地点名称'), '办公室');
+ fireEvent.press(screen.getByLabelText('保存地点'));
+ expect(onSave).toHaveBeenCalledWith(
+ expect.objectContaining({
+ address: '南京东路1号',
+ name: '办公室',
+ }),
+ );
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/LocationPickerSheet.test.tsx b/frontend/__tests__/features/schedule/location/LocationPickerSheet.test.tsx
new file mode 100644
index 0000000..71a22cf
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/LocationPickerSheet.test.tsx
@@ -0,0 +1,100 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+jest.mock('@/features/schedule/location/AddressEditorSheet', () => ({
+ AddressEditorSheet: ({
+ visible,
+ onSave,
+ onClose,
+ }: {
+ visible: boolean;
+ onSave: (location: {
+ address: string;
+ latitude: number;
+ longitude: number;
+ name?: string;
+ }) => void;
+ onClose: () => void;
+ }) => {
+ const { Pressable, Text } = require('react-native');
+ if (!visible) return null;
+ return (
+ <>
+
+ onSave({ address: '新地址', latitude: 31.1, longitude: 121.1, name: '新地点' })
+ }
+ >
+ mock-save
+
+
+ mock-close
+
+ >
+ );
+ },
+}));
+
+import { LocationPickerSheet } from '@/features/schedule/location/LocationPickerSheet';
+
+const office = {
+ id: 'loc_1',
+ address: '南京东路1号',
+ latitude: 31.2,
+ longitude: 121.5,
+ name: '办公室',
+};
+
+describe('LocationPickerSheet', () => {
+ it('shows empty state', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('还没有常用地点')).toBeTruthy();
+ });
+
+ it('selects a location and closes', () => {
+ const onSelect = jest.fn();
+ const onClose = jest.fn();
+ render(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('选择地点 办公室'));
+ expect(onSelect).toHaveBeenCalledWith(office);
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('opens the editor and upserts a new location', () => {
+ const onUpsert = jest.fn();
+ const onSelect = jest.fn();
+ const onClose = jest.fn();
+ render(
+ ,
+ );
+ fireEvent.press(screen.getByLabelText('添加地点'));
+ fireEvent.press(screen.getByLabelText('mock-save-location'));
+ expect(onUpsert).toHaveBeenCalled();
+ expect(onSelect).toHaveBeenCalled();
+ expect(onClose).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/MapPicker/MapPicker.native.test.tsx b/frontend/__tests__/features/schedule/location/MapPicker/MapPicker.native.test.tsx
new file mode 100644
index 0000000..113de65
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/MapPicker/MapPicker.native.test.tsx
@@ -0,0 +1,149 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+jest.mock('@/features/schedule/location/MapPicker/baidu', () => ({
+ BAIDU_MAP_AK: 'test-ak',
+ createCoordinateLocation: (latitude: number, longitude: number) => ({
+ address: `坐标 ${latitude},${longitude}`,
+ latitude,
+ longitude,
+ }),
+ buildBaiduMapDocument: () => '',
+}));
+
+jest.mock('@/features/schedule/location/MapPicker/Overlay', () => ({
+ MapPickerOverlay: ({
+ onCancel,
+ onConfirm,
+ onLocate,
+ onSearch,
+ mapReady,
+ selection,
+ }: {
+ onCancel: () => void;
+ onConfirm: () => void;
+ onLocate: () => void;
+ onSearch: (query: string) => Promise;
+ mapReady: boolean;
+ selection: { address: string } | null;
+ }) => {
+ const { Pressable, Text } = require('react-native');
+ return (
+ <>
+ {mapReady ? 'ready' : 'loading'}
+ {selection?.address ?? 'no-selection'}
+
+ cancel
+
+
+ confirm
+
+
+ locate
+
+ {
+ void onSearch('外滩');
+ }}
+ >
+ search
+
+ >
+ );
+ },
+}));
+
+import { MapPicker } from '@/features/schedule/location/MapPicker/MapPicker.native';
+
+describe('MapPicker.native', () => {
+ it('handles bridge messages and confirms the selection', async () => {
+ const onConfirm = jest.fn();
+ const onCancel = jest.fn();
+ const { UNSAFE_getByType } = render(
+ ,
+ );
+
+ const WebView = require('react-native-webview').WebView;
+ const webview = UNSAFE_getByType(WebView);
+
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: { data: JSON.stringify({ type: 'map-ready' }) },
+ });
+ });
+ expect(screen.getByText('ready')).toBeTruthy();
+
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: {
+ data: JSON.stringify({ type: 'selecting', latitude: 31.2, longitude: 121.5 }),
+ },
+ });
+ });
+ expect(screen.getByText('坐标 31.2,121.5')).toBeTruthy();
+
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: {
+ data: JSON.stringify({
+ type: 'selected',
+ location: { address: '外滩', latitude: 31.2, longitude: 121.5 },
+ }),
+ },
+ });
+ });
+ expect(screen.getByText('外滩')).toBeTruthy();
+
+ fireEvent.press(screen.getByLabelText('mock-confirm'));
+ expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ address: '外滩' }));
+ fireEvent.press(screen.getByLabelText('mock-cancel'));
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('surfaces map errors from the bridge', async () => {
+ const { UNSAFE_getByType } = render(
+ ,
+ );
+ const WebView = require('react-native-webview').WebView;
+ const webview = UNSAFE_getByType(WebView);
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: { data: JSON.stringify({ type: 'map-error', message: '坏了' }) },
+ });
+ });
+ expect(screen.getByText('loading')).toBeTruthy();
+ });
+
+ it('handles location errors from the bridge', async () => {
+ const { UNSAFE_getByType } = render(
+ ,
+ );
+ const WebView = require('react-native-webview').WebView;
+ const webview = UNSAFE_getByType(WebView);
+
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: { data: JSON.stringify({ type: 'map-ready' }) },
+ });
+ });
+
+ await act(async () => {
+ webview.props.onMessage({
+ nativeEvent: {
+ data: JSON.stringify({ type: 'location-error', message: '无定位' }),
+ },
+ });
+ });
+
+ fireEvent.press(screen.getByLabelText('mock-locate'));
+
+ await act(async () => {
+ webview.props.onMessage({ nativeEvent: { data: 'not-json' } });
+ });
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/MapPicker/Overlay.test.tsx b/frontend/__tests__/features/schedule/location/MapPicker/Overlay.test.tsx
new file mode 100644
index 0000000..6ab1054
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/MapPicker/Overlay.test.tsx
@@ -0,0 +1,92 @@
+import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
+import { act, fireEvent, render, screen } from '@testing-library/react-native';
+
+import { MapPickerOverlay } from '@/features/schedule/location/MapPicker/Overlay';
+
+async function flushDebouncedSearch() {
+ await act(async () => {
+ jest.advanceTimersByTime(320);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+}
+
+describe('MapPickerOverlay', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ const base = {
+ mapError: null as string | null,
+ mapReady: true,
+ locating: false,
+ locationError: null as string | null,
+ onCancel: jest.fn(),
+ onLocate: jest.fn(),
+ onConfirm: jest.fn(),
+ onSearch: jest.fn(async () => [
+ { address: '外滩 · 中山东一路', latitude: 31.24, longitude: 121.49 },
+ ]),
+ onSelectSearchResult: jest.fn(),
+ selection: {
+ address: '南京东路',
+ latitude: 31.23,
+ longitude: 121.48,
+ },
+ };
+
+ it('debounces search and lists results', async () => {
+ render();
+ fireEvent.changeText(screen.getByLabelText('搜索地点'), '外滩');
+ await flushDebouncedSearch();
+ expect(base.onSearch).toHaveBeenCalledWith('外滩');
+ expect(screen.getByText('外滩 · 中山东一路')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('选择 外滩 · 中山东一路'));
+ expect(base.onSelectSearchResult).toHaveBeenCalled();
+ });
+
+ it('confirms and cancels', () => {
+ render();
+ fireEvent.press(screen.getByLabelText('确认选中的地址'));
+ expect(base.onConfirm).toHaveBeenCalled();
+ fireEvent.press(screen.getByLabelText('退出地图选点'));
+ expect(base.onCancel).toHaveBeenCalled();
+ });
+
+ it('shows map errors and locating state', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText('地图加载失败')).toBeTruthy();
+ expect(screen.getByText('定位失败')).toBeTruthy();
+ expect(screen.getByText('正在获取当前位置...')).toBeTruthy();
+ });
+
+ it('surfaces search failures', async () => {
+ const onSearch = jest.fn(async () => {
+ throw new Error('qps');
+ });
+ render();
+ fireEvent.changeText(screen.getByLabelText('搜索地点'), '失败');
+ await flushDebouncedSearch();
+ expect(screen.getByText('搜索暂时不可用,请直接在地图上选点')).toBeTruthy();
+ });
+
+ it('shows empty search results', async () => {
+ const onSearch = jest.fn(async () => []);
+ render();
+ fireEvent.changeText(screen.getByLabelText('搜索地点'), '空');
+ await flushDebouncedSearch();
+ expect(screen.getByText('没有找到相关地点')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/MapPicker/baidu/baiduMapWebView.test.ts b/frontend/__tests__/features/schedule/location/MapPicker/baidu/baiduMapWebView.test.ts
new file mode 100644
index 0000000..e52aeda
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/MapPicker/baidu/baiduMapWebView.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { buildBaiduMapDocument } from '@/features/schedule/location/MapPicker/baidu/baiduMapWebView';
+
+describe('buildBaiduMapDocument', () => {
+ it('embeds the AK and default Shanghai center when no initial location', () => {
+ const html = buildBaiduMapDocument('test-ak', null);
+ expect(html).toContain(encodeURIComponent('test-ak'));
+ expect(html).toContain('31.236305');
+ expect(html).toContain('121.480237');
+ expect(html).toContain('null');
+ });
+
+ it('embeds the provided initial location', () => {
+ const html = buildBaiduMapDocument('ak', {
+ address: '办公室',
+ latitude: 31.1,
+ longitude: 121.2,
+ name: '办公室',
+ });
+ expect(html).toContain('31.1');
+ expect(html).toContain('121.2');
+ expect(html).toContain('办公室');
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.test.ts b/frontend/__tests__/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.test.ts
new file mode 100644
index 0000000..7dc492d
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.test.ts
@@ -0,0 +1,91 @@
+import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
+
+import {
+ createReverseGeocodeGate,
+ type ReverseGeocodeJob,
+ type ReverseGeocodeRunner,
+} from '@/features/schedule/location/MapPicker/baidu/reverseGeocodeGate';
+
+describe('createReverseGeocodeGate', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('debounces rapid schedules and only runs the latest job', async () => {
+ const gate = createReverseGeocodeGate({ debounceMs: 450, minIntervalMs: 0 });
+ const run = jest.fn();
+
+ gate.schedule({ latitude: 1, longitude: 1, requestId: 1 }, run);
+ gate.schedule({ latitude: 2, longitude: 2, requestId: 2 }, run);
+ gate.schedule({ latitude: 3, longitude: 3, requestId: 3 }, run);
+
+ expect(run).not.toHaveBeenCalled();
+ await jest.advanceTimersByTimeAsync(450);
+
+ expect(run).toHaveBeenCalledTimes(1);
+ const firstCall = run.mock.calls[0]?.[0] as ReverseGeocodeJob | undefined;
+ expect(firstCall).toEqual({
+ latitude: 3,
+ longitude: 3,
+ requestId: 3,
+ });
+ });
+
+ it('waits for the minimum interval before starting the next run', async () => {
+ const gate = createReverseGeocodeGate({ debounceMs: 0, minIntervalMs: 350 });
+ const run = jest.fn(async () => undefined);
+
+ gate.schedule({ latitude: 1, longitude: 1, requestId: 1 }, run);
+ await jest.advanceTimersByTimeAsync(0);
+ expect(run).toHaveBeenCalledTimes(1);
+
+ gate.schedule({ latitude: 2, longitude: 2, requestId: 2 }, run);
+ await jest.advanceTimersByTimeAsync(0);
+ expect(run).toHaveBeenCalledTimes(1);
+
+ await jest.advanceTimersByTimeAsync(350);
+ expect(run).toHaveBeenCalledTimes(2);
+ });
+
+ it('clear cancels pending timers and drops the queued job', async () => {
+ const gate = createReverseGeocodeGate({ debounceMs: 450, minIntervalMs: 0 });
+ const run = jest.fn();
+
+ gate.schedule({ latitude: 1, longitude: 1, requestId: 1 }, run);
+ gate.clear();
+ await jest.advanceTimersByTimeAsync(450);
+ expect(run).not.toHaveBeenCalled();
+ });
+
+ it('queues the next job until the in-flight request finishes', async () => {
+ const gate = createReverseGeocodeGate({ debounceMs: 0, minIntervalMs: 0 });
+ let resolveFirst: (() => void) | undefined;
+ const first = jest.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveFirst = resolve;
+ }),
+ );
+ const second = jest.fn(async () => undefined);
+
+ gate.schedule({ latitude: 1, longitude: 1, requestId: 1 }, first);
+ await jest.advanceTimersByTimeAsync(0);
+ expect(first).toHaveBeenCalledTimes(1);
+
+ gate.schedule({ latitude: 2, longitude: 2, requestId: 2 }, second);
+ await jest.advanceTimersByTimeAsync(0);
+ expect(second).not.toHaveBeenCalled();
+
+ resolveFirst?.();
+ await Promise.resolve();
+ await jest.advanceTimersByTimeAsync(0);
+
+ expect(second).toHaveBeenCalledTimes(1);
+ const secondCall = second.mock.calls[0]?.[0] as ReverseGeocodeJob | undefined;
+ expect(secondCall?.requestId).toBe(2);
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/MapPicker/baidu/services.test.ts b/frontend/__tests__/features/schedule/location/MapPicker/baidu/services.test.ts
new file mode 100644
index 0000000..697f643
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/MapPicker/baidu/services.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from '@jest/globals';
+
+import {
+ BAIDU_COORDINATE_SYSTEM,
+ SHANGHAI_CENTER,
+ coordinateAddress,
+ createCoordinateLocation,
+ readablePoiAddress,
+} from '@/features/schedule/location/MapPicker/baidu/services';
+
+describe('map picker services', () => {
+ it('exposes the Baidu coordinate system and Shanghai default', () => {
+ expect(BAIDU_COORDINATE_SYSTEM).toBe('bd09ll');
+ expect(SHANGHAI_CENTER.latitude).toBeCloseTo(31.236305);
+ expect(SHANGHAI_CENTER.longitude).toBeCloseTo(121.480237);
+ });
+
+ it('formats a coordinate fallback address', () => {
+ expect(coordinateAddress(31.2, 121.5)).toBe('百度地图选点 · 31.20000, 121.50000');
+ });
+
+ it('builds a MapLocation from coordinates', () => {
+ expect(createCoordinateLocation(31.2, 121.5)).toEqual({
+ address: '百度地图选点 · 31.20000, 121.50000',
+ latitude: 31.2,
+ longitude: 121.5,
+ });
+ });
+
+ it('joins POI title with address when present', () => {
+ expect(readablePoiAddress('外滩', '中山东一路')).toBe('外滩 · 中山东一路');
+ expect(readablePoiAddress('外滩', ' ')).toBe('外滩');
+ expect(readablePoiAddress('外滩')).toBe('外滩');
+ });
+});
diff --git a/frontend/__tests__/features/schedule/location/locationUtils.test.ts b/frontend/__tests__/features/schedule/location/locationUtils.test.ts
new file mode 100644
index 0000000..1f642fd
--- /dev/null
+++ b/frontend/__tests__/features/schedule/location/locationUtils.test.ts
@@ -0,0 +1,90 @@
+import { afterEach, describe, expect, it, jest } from '@jest/globals';
+
+import {
+ createSavedLocation,
+ matchSavedLocation,
+ upsertSavedLocation,
+} from '@/features/schedule/location/utils';
+import type { SavedLocation } from '@/features/schedule/location/types';
+
+const office: SavedLocation = {
+ id: 'loc_office',
+ address: '南京东路1号',
+ latitude: 31.23,
+ longitude: 121.48,
+ name: '办公室',
+};
+
+describe('createSavedLocation', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('keeps an explicit id', () => {
+ expect(
+ createSavedLocation({ address: 'A', latitude: 1, longitude: 2, name: 'N' }, 'loc_fixed'),
+ ).toEqual({
+ address: 'A',
+ latitude: 1,
+ longitude: 2,
+ name: 'N',
+ id: 'loc_fixed',
+ });
+ });
+
+ it('generates an id from Date.now when omitted', () => {
+ jest.spyOn(Date, 'now').mockReturnValue(42);
+ expect(createSavedLocation({ address: 'A', latitude: 1, longitude: 2 }).id).toBe('loc_42');
+ });
+});
+
+describe('upsertSavedLocation', () => {
+ it('appends a new location', () => {
+ expect(upsertSavedLocation([], office)).toEqual([office]);
+ });
+
+ it('replaces an existing location with the same id', () => {
+ const updated = { ...office, name: '总部' };
+ expect(upsertSavedLocation([office], updated)).toEqual([updated]);
+ });
+});
+
+describe('matchSavedLocation', () => {
+ const locations = [office];
+
+ it('matches by coordinates first', () => {
+ expect(
+ matchSavedLocation(locations, {
+ latitude: 31.23,
+ longitude: 121.48,
+ location_name: '别的名字',
+ }),
+ ).toBe(office);
+ });
+
+ it('matches by name and address together', () => {
+ expect(
+ matchSavedLocation(locations, {
+ location_name: '办公室',
+ location_address: '南京东路1号',
+ }),
+ ).toBe(office);
+ });
+
+ it('matches by name alone', () => {
+ expect(matchSavedLocation(locations, { location_name: '办公室' })).toBe(office);
+ });
+
+ it('matches by address alone', () => {
+ expect(matchSavedLocation(locations, { location_address: '南京东路1号' })).toBe(office);
+ });
+
+ it('returns null when there is nothing to match on', () => {
+ expect(matchSavedLocation(locations, {})).toBeNull();
+ expect(matchSavedLocation(locations, { location_name: ' ', location_address: '' })).toBeNull();
+ });
+
+ it('returns null when nothing matches', () => {
+ expect(matchSavedLocation(locations, { location_name: '咖啡馆' })).toBeNull();
+ });
+});
diff --git a/frontend/__tests__/features/schedule/presentation/scheduleFormat.test.ts b/frontend/__tests__/features/schedule/presentation/scheduleFormat.test.ts
new file mode 100644
index 0000000..b6ce272
--- /dev/null
+++ b/frontend/__tests__/features/schedule/presentation/scheduleFormat.test.ts
@@ -0,0 +1,139 @@
+import { describe, expect, it } from '@jest/globals';
+
+import type { Schedule } from '@/contracts';
+
+import {
+ scheduleColor,
+ scheduleDate,
+ scheduleDuration,
+ scheduleRange,
+ scheduleSourceLabel,
+ scheduleStatusLabel,
+ scheduleTime,
+ timeToMinutes,
+} from '@/features/schedule/presentation/scheduleFormat';
+
+// Built from local-time components on purpose: the formatters read getHours()
+// and friends, so a fixed offset string would make these tests timezone-bound.
+function makeSchedule(overrides: Partial = {}): Schedule {
+ return {
+ id: 'schedule_test',
+ user_id: 'default_user',
+ source_mode: 'manual',
+ schedule_type: 'time',
+ status: 'scheduled',
+ title: '测试日程',
+ notes: null,
+ start_time: new Date(2026, 6, 29, 9, 5).toISOString(),
+ end_time: null,
+ timezone: 'Asia/Shanghai',
+ location_name: null,
+ location_address: null,
+ latitude: null,
+ longitude: null,
+ geofence_radius_meters: 100,
+ geofence_armed: false,
+ time_remind_offset_minutes: 15,
+ time_triggered_at: null,
+ geo_triggered_at: null,
+ system_schedule_ref_id: null,
+ system_alarm_ref_id: null,
+ created_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ updated_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ ...overrides,
+ };
+}
+
+describe('scheduleDate', () => {
+ it('returns null when there is no start time', () => {
+ expect(scheduleDate(makeSchedule({ start_time: null }))).toBeNull();
+ });
+
+ it('returns null for an unparseable start time', () => {
+ expect(scheduleDate(makeSchedule({ start_time: 'not-a-date' }))).toBeNull();
+ });
+});
+
+describe('scheduleTime', () => {
+ it('pads hours and minutes to two digits', () => {
+ expect(scheduleTime(makeSchedule())).toBe('09:05');
+ });
+
+ it('falls back to 地点 for schedules without a start time', () => {
+ expect(scheduleTime(makeSchedule({ start_time: null }))).toBe('地点');
+ });
+});
+
+describe('scheduleRange', () => {
+ it('joins start and end times', () => {
+ const item = makeSchedule({ end_time: new Date(2026, 6, 29, 10, 40).toISOString() });
+ expect(scheduleRange(item)).toBe('09:05–10:40');
+ });
+
+ it('returns only the start when there is no end time', () => {
+ expect(scheduleRange(makeSchedule())).toBe('09:05');
+ });
+
+ it('ignores an unparseable end time', () => {
+ expect(scheduleRange(makeSchedule({ end_time: 'not-a-date' }))).toBe('09:05');
+ });
+
+ it('prefers the location name for location schedules', () => {
+ const item = makeSchedule({ start_time: null, location_name: '办公室' });
+ expect(scheduleRange(item)).toBe('办公室');
+ });
+
+ it('falls back to a generic label with neither time nor place', () => {
+ expect(scheduleRange(makeSchedule({ start_time: null }))).toBe('地点提醒');
+ });
+});
+
+describe('scheduleDuration', () => {
+ it('reports the gap in minutes', () => {
+ const item = makeSchedule({ end_time: new Date(2026, 6, 29, 9, 40).toISOString() });
+ expect(scheduleDuration(item)).toBe('35 分钟');
+ });
+
+ it('reports 未设置时长 when the end time is missing', () => {
+ expect(scheduleDuration(makeSchedule())).toBe('未设置时长');
+ });
+
+ it('reports 未设置时长 when the end is not after the start', () => {
+ const item = makeSchedule({ end_time: new Date(2026, 6, 29, 9, 5).toISOString() });
+ expect(scheduleDuration(item)).toBe('未设置时长');
+ });
+});
+
+describe('scheduleColor', () => {
+ it('uses the done colour whatever the type is', () => {
+ expect(scheduleColor(makeSchedule({ status: 'done', schedule_type: 'location' }))).toBe(
+ '#A8C7B5',
+ );
+ });
+
+ it('distinguishes location, voice and manual schedules', () => {
+ expect(scheduleColor(makeSchedule({ schedule_type: 'location' }))).toBe('#E79472');
+ expect(scheduleColor(makeSchedule({ source_mode: 'voice' }))).toBe('#AEC46B');
+ expect(scheduleColor(makeSchedule())).toBe('#7DA6B8');
+ });
+});
+
+describe('label helpers', () => {
+ it('names the creation source', () => {
+ expect(scheduleSourceLabel(makeSchedule({ source_mode: 'voice' }))).toBe('语音创建');
+ expect(scheduleSourceLabel(makeSchedule())).toBe('手动创建');
+ });
+
+ it('names all three statuses', () => {
+ expect(scheduleStatusLabel(makeSchedule({ status: 'done' }))).toBe('已完成');
+ expect(scheduleStatusLabel(makeSchedule({ status: 'deleted' }))).toBe('已删除');
+ expect(scheduleStatusLabel(makeSchedule())).toBe('待完成');
+ });
+});
+
+describe('timeToMinutes', () => {
+ it('counts minutes since midnight', () => {
+ expect(timeToMinutes('08:30')).toBe(510);
+ expect(timeToMinutes('00:00')).toBe(0);
+ });
+});
diff --git a/frontend/__tests__/features/schedule/screens/ScheduleScreen.test.tsx b/frontend/__tests__/features/schedule/screens/ScheduleScreen.test.tsx
new file mode 100644
index 0000000..eafeeb7
--- /dev/null
+++ b/frontend/__tests__/features/schedule/screens/ScheduleScreen.test.tsx
@@ -0,0 +1,74 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import type { ReactElement } from 'react';
+
+import { makeSchedule } from '@test/fixtures';
+
+jest.mock('@/shared/hooks/useCurrentDate', () => ({
+ useCurrentDate: () => new Date(2026, 6, 31, 12, 0, 0),
+}));
+
+jest.mock('@/shared/components/DatePickerSheet', () => ({
+ DatePickerSheet: () => null,
+}));
+
+import { ScheduleScreen } from '@/features/schedule/screens/ScheduleScreen';
+import { AppDialogProvider } from '@/shared/components/AppDialogProvider';
+
+function renderWithDialog(element: ReactElement) {
+ return render({element});
+}
+
+describe('ScheduleScreen', () => {
+ const props = {
+ onCreate: jest.fn(),
+ onDeleteSchedule: jest.fn(),
+ onEditSchedule: jest.fn(),
+ scheduleItems: [
+ makeSchedule({
+ id: 't1',
+ title: '今日评审',
+ start_time: new Date(2026, 6, 31, 10, 0).toISOString(),
+ }),
+ ],
+ };
+
+ it('shows the month view by default', () => {
+ renderWithDialog();
+ expect(screen.getAllByText('7月').length).toBeGreaterThan(0);
+ expect(screen.getByText('今日评审')).toBeTruthy();
+ });
+
+ it('opens create from the add button', () => {
+ renderWithDialog();
+ fireEvent.press(screen.getByLabelText('添加日程'));
+ expect(props.onCreate).toHaveBeenCalled();
+ });
+
+ it('routes completion from both the agenda row and detail sheet', () => {
+ const onToggleSchedule = jest.fn();
+ renderWithDialog();
+
+ fireEvent.press(screen.getByLabelText('完成 今日评审'));
+ expect(onToggleSchedule).toHaveBeenCalledWith(expect.objectContaining({ id: 't1' }));
+
+ fireEvent.press(screen.getByLabelText('10:00 今日评审'));
+ fireEvent.press(screen.getByLabelText('完成日程'));
+ expect(onToggleSchedule).toHaveBeenCalledTimes(2);
+ });
+
+ it('disables mutation affordances until the schedule service is ready', () => {
+ const onToggleSchedule = jest.fn();
+ renderWithDialog(
+ ,
+ );
+
+ expect(screen.getByLabelText('添加日程').props.accessibilityState).toEqual({ disabled: true });
+ expect(screen.getByLabelText('完成 今日评审').props.accessibilityState).toEqual({
+ checked: false,
+ disabled: true,
+ });
+ fireEvent.press(screen.getByLabelText('完成 今日评审'));
+ expect(onToggleSchedule).not.toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts
new file mode 100644
index 0000000..4195d90
--- /dev/null
+++ b/frontend/__tests__/fixtures.ts
@@ -0,0 +1,30 @@
+import type { Schedule } from '@/contracts';
+
+export function makeSchedule(overrides: Partial = {}): Schedule {
+ return {
+ id: 'schedule_test',
+ user_id: 'default_user',
+ source_mode: 'manual',
+ schedule_type: 'time',
+ status: 'scheduled',
+ title: '测试日程',
+ notes: null,
+ start_time: new Date(2026, 6, 29, 9, 5).toISOString(),
+ end_time: null,
+ timezone: 'Asia/Shanghai',
+ location_name: null,
+ location_address: null,
+ latitude: null,
+ longitude: null,
+ geofence_radius_meters: 100,
+ geofence_armed: false,
+ time_remind_offset_minutes: 15,
+ time_triggered_at: null,
+ geo_triggered_at: null,
+ system_schedule_ref_id: null,
+ system_alarm_ref_id: null,
+ created_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ updated_at: new Date(2026, 6, 20, 10, 0).toISOString(),
+ ...overrides,
+ };
+}
diff --git a/frontend/__tests__/infrastructure/audio/VoiceRecorder.test.ts b/frontend/__tests__/infrastructure/audio/VoiceRecorder.test.ts
new file mode 100644
index 0000000..714da49
--- /dev/null
+++ b/frontend/__tests__/infrastructure/audio/VoiceRecorder.test.ts
@@ -0,0 +1,115 @@
+import { beforeEach, describe, expect, it, jest } from '@jest/globals';
+
+const mockNativeStart = jest.fn(async () => undefined);
+const mockNativeStop = jest.fn(async () => undefined);
+const mockNativeCancel = jest.fn(async () => undefined);
+const mockCheck = jest.fn(async () => true);
+const mockRequest = jest.fn(async () => 'granted');
+const mockListeners = new Map void>();
+const mockRemove = jest.fn();
+
+jest.mock('react-native', () => ({
+ Platform: { OS: 'android' },
+ PermissionsAndroid: {
+ PERMISSIONS: { RECORD_AUDIO: 'android.permission.RECORD_AUDIO' },
+ RESULTS: { GRANTED: 'granted' },
+ check: (...args: unknown[]) => (mockCheck as (...values: unknown[]) => unknown)(...args),
+ request: (...args: unknown[]) => (mockRequest as (...values: unknown[]) => unknown)(...args),
+ },
+ NativeModules: {
+ TimeflowVoiceRecorder: {
+ start: (...args: unknown[]) =>
+ (mockNativeStart as (...values: unknown[]) => unknown)(...args),
+ stop: (...args: unknown[]) => (mockNativeStop as (...values: unknown[]) => unknown)(...args),
+ cancel: (...args: unknown[]) =>
+ (mockNativeCancel as (...values: unknown[]) => unknown)(...args),
+ addListener: jest.fn(),
+ removeListeners: jest.fn(),
+ },
+ },
+ NativeEventEmitter: class {
+ addListener(eventName: string, listener: (event: unknown) => void) {
+ mockListeners.set(eventName, listener);
+ return { remove: mockRemove };
+ }
+ },
+}));
+
+import { Platform } from 'react-native';
+
+import {
+ AndroidPcmVoiceRecorder,
+ BrowserPcmVoiceRecorder,
+ VoiceRecordingUnavailableError,
+ createVoiceRecorder,
+} from '@/infrastructure/audio/VoiceRecorder';
+
+describe('AndroidPcmVoiceRecorder', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockListeners.clear();
+ mockCheck.mockResolvedValue(true);
+ mockRequest.mockResolvedValue('granted');
+ (Platform as { OS: string }).OS = 'android';
+ });
+
+ it('requests permission when needed and starts the native recorder', async () => {
+ mockCheck.mockResolvedValueOnce(false);
+ const recorder = new AndroidPcmVoiceRecorder();
+
+ await recorder.start(() => undefined);
+
+ expect(mockRequest).toHaveBeenCalledWith(
+ 'android.permission.RECORD_AUDIO',
+ expect.objectContaining({ title: '麦克风权限' }),
+ );
+ expect(mockNativeStart).toHaveBeenCalledTimes(1);
+ await recorder.cancel();
+ });
+
+ it('decodes native Base64 PCM chunks into ArrayBuffer values', async () => {
+ const recorder = new AndroidPcmVoiceRecorder();
+ const onChunk = jest.fn<(chunk: ArrayBuffer) => void>();
+ await recorder.start(onChunk);
+
+ mockListeners.get('TimeflowVoiceRecorderChunk')?.('AQD+/w==');
+
+ expect(onChunk).toHaveBeenCalledTimes(1);
+ expect(Array.from(new Uint8Array(onChunk.mock.calls[0]![0] as ArrayBuffer))).toEqual([
+ 1, 0, 254, 255,
+ ]);
+ await recorder.stop();
+ expect(mockNativeStop).toHaveBeenCalledTimes(1);
+ expect(mockRemove).toHaveBeenCalledTimes(2);
+ });
+
+ it('rejects denied microphone permission without starting native capture', async () => {
+ mockCheck.mockResolvedValueOnce(false);
+ mockRequest.mockResolvedValueOnce('denied');
+ const recorder = new AndroidPcmVoiceRecorder();
+
+ await expect(recorder.start(() => undefined)).rejects.toThrow('麦克风权限未授予');
+ expect(mockNativeStart).not.toHaveBeenCalled();
+ });
+
+ it('surfaces asynchronous native failures when recording stops', async () => {
+ const recorder = new AndroidPcmVoiceRecorder();
+ await recorder.start(() => undefined);
+ mockListeners.get('TimeflowVoiceRecorderError')?.({ message: 'audio device lost' });
+
+ await expect(recorder.stop()).rejects.toThrow('audio device lost');
+ });
+
+ it('selects the platform-specific implementation', () => {
+ expect(createVoiceRecorder()).toBeInstanceOf(AndroidPcmVoiceRecorder);
+ (Platform as { OS: string }).OS = 'web';
+ expect(createVoiceRecorder()).toBeInstanceOf(BrowserPcmVoiceRecorder);
+ });
+
+ it('reports a missing native module clearly', async () => {
+ const recorder = new AndroidPcmVoiceRecorder(null);
+ await expect(recorder.start(() => undefined)).rejects.toBeInstanceOf(
+ VoiceRecordingUnavailableError,
+ );
+ });
+});
diff --git a/frontend/__tests__/infrastructure/location/LocationReporter.test.ts b/frontend/__tests__/infrastructure/location/LocationReporter.test.ts
new file mode 100644
index 0000000..2037d71
--- /dev/null
+++ b/frontend/__tests__/infrastructure/location/LocationReporter.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it, jest } from '@jest/globals';
+
+import { FakeWsServer } from '@/dev/fakes/FakeWsServer';
+import {
+ LocationReporter,
+ type LocationTransport,
+} from '@/infrastructure/location/LocationReporter';
+import { WsClient } from '@/infrastructure/ws/WsClient';
+
+describe('LocationReporter', () => {
+ it('reports location when armed and receives ack', async () => {
+ const server = new FakeWsServer();
+ const client = new WsClient({ fakeHandler: server.handleMessage });
+ server.attach(client);
+ await client.connect();
+
+ const sample = jest.fn(async () => ({
+ latitude: 31.2,
+ longitude: 121.5,
+ accuracy: 10,
+ }));
+ const reporter = new LocationReporter(client, sample);
+ const ack = await reporter.report({
+ latitude: 31.2,
+ longitude: 121.5,
+ accuracy: 10,
+ });
+ expect(ack.ok).toBe(true);
+ reporter.stop();
+ client.close();
+ });
+
+ it('surfaces a rejected location acknowledgement to the reporter', async () => {
+ const client = {
+ request: jest.fn(async () => ({
+ type: 'location.report.ack' as const,
+ request_id: 'req_location_failure',
+ ok: false as const,
+ error: { code: 'denied', message: '定位上报被拒绝', details: null },
+ })),
+ } as unknown as LocationTransport;
+ const reporter = new LocationReporter(client, async () => null);
+
+ await expect(
+ reporter.report({ latitude: 31.2, longitude: 121.5, accuracy: 10 }),
+ ).rejects.toThrow('定位上报被拒绝');
+ });
+});
diff --git a/frontend/__tests__/infrastructure/storage/deviceIdStore.test.ts b/frontend/__tests__/infrastructure/storage/deviceIdStore.test.ts
new file mode 100644
index 0000000..6da89a4
--- /dev/null
+++ b/frontend/__tests__/infrastructure/storage/deviceIdStore.test.ts
@@ -0,0 +1,41 @@
+import { beforeEach, describe, expect, it, jest } from '@jest/globals';
+
+const mockGetInfoAsync = jest.fn(async () => ({ exists: true }));
+const mockReadAsStringAsync = jest.fn(async () => ' device_existing ');
+const mockWriteAsStringAsync = jest.fn(async () => undefined);
+const mockFileSystem = {
+ documentDirectory: 'file:///documents/',
+ getInfoAsync: mockGetInfoAsync,
+ readAsStringAsync: mockReadAsStringAsync,
+ writeAsStringAsync: mockWriteAsStringAsync,
+};
+
+jest.mock('react-native', () => ({
+ Platform: { OS: 'android' },
+}));
+
+jest.mock('expo', () => ({
+ requireOptionalNativeModule: jest.fn(() => mockFileSystem),
+}));
+
+import { createDeviceIdStore } from '@/infrastructure/storage/deviceIdStore';
+
+describe('native deviceIdStore', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockGetInfoAsync.mockResolvedValue({ exists: true });
+ mockReadAsStringAsync.mockResolvedValue(' device_existing ');
+ });
+
+ it('passes the options arguments required by Expo SDK 57 native methods', async () => {
+ const store = await createDeviceIdStore();
+
+ await expect(store.get()).resolves.toBe('device_existing');
+ await store.set('device_updated');
+
+ const path = 'file:///documents/.timeflow-device-id';
+ expect(mockGetInfoAsync).toHaveBeenCalledWith(path, {});
+ expect(mockReadAsStringAsync).toHaveBeenCalledWith(path, {});
+ expect(mockWriteAsStringAsync).toHaveBeenCalledWith(path, 'device_updated', {});
+ });
+});
diff --git a/frontend/__tests__/infrastructure/ws/WsClient.test.ts b/frontend/__tests__/infrastructure/ws/WsClient.test.ts
new file mode 100644
index 0000000..bebed06
--- /dev/null
+++ b/frontend/__tests__/infrastructure/ws/WsClient.test.ts
@@ -0,0 +1,164 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { FakeWsServer } from '@/dev/fakes/FakeWsServer';
+import { WsClient } from '@/infrastructure/ws/WsClient';
+import { makeSchedule } from '@test/fixtures';
+
+describe('WsClient + FakeWsServer', () => {
+ it('completes session hello and lists schedules', async () => {
+ const server = new FakeWsServer({ userId: 'user_test' });
+ const client = new WsClient({ fakeHandler: server.handleMessage });
+ server.attach(client);
+ await client.connect();
+
+ const ready = new Promise((resolve) => {
+ client.onMessage((message) => {
+ if (!(message instanceof ArrayBuffer) && message.type === 'session.ready') {
+ resolve();
+ }
+ });
+ });
+ client.sendJson({
+ type: 'session.hello',
+ device_id: 'device_1',
+ app_version: '1.0.0',
+ });
+ await ready;
+
+ const list = await client.request({
+ type: 'schedule.list.query',
+ request_id: 'req_list_1',
+ payload: { status: null, include_deleted: false },
+ });
+ expect(list.type).toBe('schedule.list.result');
+ expect(list.ok).toBe(true);
+ client.close();
+ });
+
+ it('upserts a schedule through fake WS', async () => {
+ const server = new FakeWsServer({ userId: 'user_test' });
+ const client = new WsClient({ fakeHandler: server.handleMessage });
+ server.attach(client);
+ await client.connect();
+
+ const response = await client.request({
+ type: 'schedule.upsert.command',
+ request_id: 'req_up_1',
+ payload: {
+ source_mode: 'manual',
+ schedule_type: 'time',
+ title: '测试',
+ start_time: new Date(Date.now() + 3_600_000).toISOString(),
+ },
+ });
+ expect(response.ok).toBe(true);
+ expect(server.getSchedules()).toHaveLength(1);
+ client.close();
+ });
+
+ it('acks delete without losing synchronous fake replies', async () => {
+ const server = new FakeWsServer({
+ userId: 'user_test',
+ seedSchedules: [makeSchedule({ id: 'del_sync', user_id: 'user_test' })],
+ });
+ const client = new WsClient({ fakeHandler: server.handleMessage });
+ server.attach(client);
+ await client.connect();
+
+ const ack = await client.request({
+ type: 'schedule.deleted',
+ request_id: 'req_del_sync',
+ schedule_id: 'del_sync',
+ deleted: true,
+ timestamp: new Date().toISOString(),
+ });
+ expect(ack.type).toBe('schedule.deleted.ack');
+ expect(ack.ok).toBe(true);
+ expect(server.getSchedules()[0]?.status).toBe('deleted');
+ client.close();
+ });
+
+ it('updates status to done without marking deleted', async () => {
+ const server = new FakeWsServer({
+ userId: 'user_test',
+ seedSchedules: [makeSchedule({ id: 'status_1', user_id: 'user_test' })],
+ });
+ const client = new WsClient({ fakeHandler: server.handleMessage });
+ server.attach(client);
+ await client.connect();
+
+ const response = await client.request({
+ type: 'schedule.status.command',
+ request_id: 'req_status_1',
+ payload: { schedule_id: 'status_1', status: 'done' },
+ });
+ expect(response.ok).toBe(true);
+ expect(server.getSchedules()[0]?.status).toBe('done');
+ client.close();
+ });
+
+ it('rejects pending requests immediately when the remote socket closes unexpectedly', async () => {
+ class TestWebSocket {
+ static readonly OPEN = 1;
+ static instance: TestWebSocket | null = null;
+
+ binaryType = '';
+ readyState = 0;
+ onopen: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+ onclose: (() => void) | null = null;
+ onmessage: ((event: { data: unknown }) => void) | null = null;
+
+ constructor(_url: string) {
+ TestWebSocket.instance = this;
+ }
+
+ send(_data: string | ArrayBuffer) {}
+
+ close() {
+ this.readyState = 3;
+ this.onclose?.();
+ }
+
+ open() {
+ this.readyState = TestWebSocket.OPEN;
+ this.onopen?.();
+ }
+
+ closeUnexpectedly() {
+ this.readyState = 3;
+ this.onclose?.();
+ }
+ }
+
+ const originalWebSocket = globalThis.WebSocket;
+ Object.defineProperty(globalThis, 'WebSocket', {
+ configurable: true,
+ value: TestWebSocket,
+ writable: true,
+ });
+
+ try {
+ const client = new WsClient({ url: 'ws://test.invalid', requestTimeoutMs: 60_000 });
+ const connecting = client.connect();
+ TestWebSocket.instance?.open();
+ await connecting;
+
+ const pending = client.request({
+ type: 'schedule.list.query',
+ request_id: 'req_disconnect',
+ payload: { status: null, include_deleted: false },
+ });
+ TestWebSocket.instance?.closeUnexpectedly();
+
+ await expect(pending).rejects.toThrow('WebSocket closed unexpectedly');
+ expect(client.getConnectionStatus()).toBe('closed');
+ } finally {
+ Object.defineProperty(globalThis, 'WebSocket', {
+ configurable: true,
+ value: originalWebSocket,
+ writable: true,
+ });
+ }
+ });
+});
diff --git a/frontend/__tests__/shared/components/AppDialogProvider.test.tsx b/frontend/__tests__/shared/components/AppDialogProvider.test.tsx
new file mode 100644
index 0000000..e8db627
--- /dev/null
+++ b/frontend/__tests__/shared/components/AppDialogProvider.test.tsx
@@ -0,0 +1,58 @@
+import { describe, expect, it } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import { Pressable, Text } from 'react-native';
+
+import { AppDialogProvider, useAppDialog } from '@/shared/components/AppDialogProvider';
+
+function Harness() {
+ const { confirm, showNotice } = useAppDialog();
+ return (
+ <>
+ void showNotice({ title: '连接不可用', message: '请检查网络' })}
+ />
+
+ void confirm({
+ title: '删除日程',
+ message: '删除后无法恢复',
+ confirmLabel: '删除',
+ tone: 'danger',
+ })
+ }
+ />
+ content
+ >
+ );
+}
+
+describe('AppDialogProvider', () => {
+ it('renders notices in the app instead of a native Alert', () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.press(screen.getByLabelText('show-notice'));
+ expect(screen.getByText('连接不可用')).toBeTruthy();
+ expect(screen.getByText('请检查网络')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('知道了'));
+ expect(screen.queryByText('连接不可用')).toBeNull();
+ });
+
+ it('renders custom destructive confirmations', () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.press(screen.getByLabelText('show-confirm'));
+ expect(screen.getByText('删除后无法恢复')).toBeTruthy();
+ expect(screen.getByLabelText('取消')).toBeTruthy();
+ expect(screen.getByLabelText('删除')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/shared/components/BackButton.test.tsx b/frontend/__tests__/shared/components/BackButton.test.tsx
new file mode 100644
index 0000000..3853726
--- /dev/null
+++ b/frontend/__tests__/shared/components/BackButton.test.tsx
@@ -0,0 +1,18 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { BackButton } from '@/shared/components/BackButton';
+
+describe('BackButton', () => {
+ it('uses the default label and fires onPress', () => {
+ const onPress = jest.fn();
+ render();
+ fireEvent.press(screen.getByLabelText('返回'));
+ expect(onPress).toHaveBeenCalled();
+ });
+
+ it('accepts a custom accessibility label', () => {
+ render();
+ expect(screen.getByLabelText('关闭')).toBeTruthy();
+ });
+});
diff --git a/frontend/__tests__/shared/components/DatePickerSheet.test.tsx b/frontend/__tests__/shared/components/DatePickerSheet.test.tsx
new file mode 100644
index 0000000..76985bb
--- /dev/null
+++ b/frontend/__tests__/shared/components/DatePickerSheet.test.tsx
@@ -0,0 +1,62 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+jest.mock('react-native-calendars', () => {
+ const React = require('react');
+ const { Pressable, Text } = require('react-native');
+ return {
+ LocaleConfig: { locales: {}, defaultLocale: 'zh' },
+ Calendar: ({
+ onDayPress,
+ markedDates,
+ }: {
+ onDayPress: (day: { dateString: string }) => void;
+ markedDates?: Record;
+ }) =>
+ React.createElement(
+ Pressable,
+ {
+ accessibilityLabel: 'mock-calendar-day',
+ onPress: () => onDayPress({ dateString: '2026-08-02' }),
+ },
+ React.createElement(Text, null, `marks:${Object.keys(markedDates ?? {}).join(',')}`),
+ ),
+ };
+});
+
+import { DatePickerSheet } from '@/shared/components/DatePickerSheet';
+
+describe('DatePickerSheet', () => {
+ it('is hidden when not visible', () => {
+ render(
+ ,
+ );
+ expect(screen.queryByLabelText('mock-calendar-day')).toBeNull();
+ });
+
+ it('selects a day and can jump to today', () => {
+ const onSelect = jest.fn();
+ const onClose = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText(/marks:2026-07-31/)).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('mock-calendar-day'));
+ expect(onSelect).toHaveBeenCalled();
+ expect(onClose).toHaveBeenCalled();
+
+ fireEvent.press(screen.getByLabelText('回到今天'));
+ expect(onSelect).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/frontend/__tests__/shared/components/TimePickerSheet.test.tsx b/frontend/__tests__/shared/components/TimePickerSheet.test.tsx
new file mode 100644
index 0000000..20e45ce
--- /dev/null
+++ b/frontend/__tests__/shared/components/TimePickerSheet.test.tsx
@@ -0,0 +1,38 @@
+import { describe, expect, it, jest } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+
+import { TimePickerSheet } from '@/shared/components/TimePickerSheet';
+
+describe('TimePickerSheet', () => {
+ it('is hidden when not visible', () => {
+ render(
+ ,
+ );
+ expect(screen.queryByText('选择时间')).toBeNull();
+ });
+
+ it('lets the user change hour/minute and confirm', () => {
+ const onSelect = jest.fn();
+ const onClose = jest.fn();
+ render(
+ ,
+ );
+ expect(screen.getByText('09:05')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('15 时'));
+ fireEvent.press(screen.getByLabelText('30 分'));
+ expect(screen.getByText('15:30')).toBeTruthy();
+ fireEvent.press(screen.getByLabelText('确认时间'));
+ expect(onSelect).toHaveBeenCalledWith('15:30');
+ expect(onClose).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/__tests__/shared/hooks/useCurrentDate.test.ts b/frontend/__tests__/shared/hooks/useCurrentDate.test.ts
new file mode 100644
index 0000000..98c9a80
--- /dev/null
+++ b/frontend/__tests__/shared/hooks/useCurrentDate.test.ts
@@ -0,0 +1,33 @@
+import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
+import { act, renderHook } from '@testing-library/react-native';
+
+import { useCurrentDate } from '@/shared/hooks/useCurrentDate';
+
+describe('useCurrentDate', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date(2026, 6, 31, 12, 0, 0));
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('starts at the current time and ticks every minute', () => {
+ const { result } = renderHook(() => useCurrentDate());
+ expect(result.current.getTime()).toBe(new Date(2026, 6, 31, 12, 0, 0).getTime());
+
+ act(() => {
+ jest.advanceTimersByTime(60_000);
+ });
+ expect(result.current.getTime()).toBe(new Date(2026, 6, 31, 12, 1, 0).getTime());
+ });
+
+ it('clears the interval on unmount', () => {
+ const clearSpy = jest.spyOn(global, 'clearInterval');
+ const { unmount } = renderHook(() => useCurrentDate());
+ unmount();
+ expect(clearSpy).toHaveBeenCalled();
+ clearSpy.mockRestore();
+ });
+});
diff --git a/frontend/__tests__/shared/theme/index.test.ts b/frontend/__tests__/shared/theme/index.test.ts
new file mode 100644
index 0000000..e8b67e2
--- /dev/null
+++ b/frontend/__tests__/shared/theme/index.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { colors, spacing } from '@/shared/theme/index';
+
+describe('theme tokens', () => {
+ it('exposes the brand palette and spacing scale', () => {
+ expect(colors.deep).toBe('#15352B');
+ expect(colors.lime).toBe('#D7F36A');
+ expect(spacing.md).toBe(16);
+ });
+});
diff --git a/frontend/__tests__/shared/utils/date.test.ts b/frontend/__tests__/shared/utils/date.test.ts
new file mode 100644
index 0000000..bb9ebf7
--- /dev/null
+++ b/frontend/__tests__/shared/utils/date.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from '@jest/globals';
+
+import {
+ addDays,
+ dateKey,
+ formatDate,
+ formatFullDate,
+ formatMonthDay,
+ formatWeekRange,
+ startOfMonth,
+ startOfWeek,
+} from '@/shared/utils/date';
+
+describe('dateKey', () => {
+ it('pads single-digit months and days', () => {
+ expect(dateKey(new Date(2026, 0, 5))).toBe('2026-01-05');
+ });
+
+ it('leaves two-digit values alone', () => {
+ expect(dateKey(new Date(2026, 11, 25))).toBe('2026-12-25');
+ });
+});
+
+describe('startOfWeek', () => {
+ it('treats Monday as the first day of the week', () => {
+ expect(dateKey(startOfWeek(new Date(2026, 6, 29)))).toBe('2026-07-27');
+ });
+
+ it('maps Sunday back to the Monday that started it', () => {
+ expect(dateKey(startOfWeek(new Date(2026, 7, 2)))).toBe('2026-07-27');
+ });
+
+ it('is a no-op when the date is already Monday', () => {
+ expect(dateKey(startOfWeek(new Date(2026, 6, 27)))).toBe('2026-07-27');
+ });
+});
+
+describe('addDays', () => {
+ it('crosses a month boundary', () => {
+ expect(dateKey(addDays(new Date(2026, 6, 30), 3))).toBe('2026-08-02');
+ });
+
+ it('accepts a negative amount', () => {
+ expect(dateKey(addDays(new Date(2026, 7, 1), -1))).toBe('2026-07-31');
+ });
+});
+
+describe('startOfMonth', () => {
+ it('returns the first day of the month', () => {
+ expect(dateKey(startOfMonth(new Date(2026, 6, 29)))).toBe('2026-07-01');
+ });
+});
+
+describe('formatDate', () => {
+ it('renders month, day and weekday', () => {
+ expect(formatDate(new Date(2026, 6, 29))).toBe('7月29日 · 星期三');
+ });
+
+ it('labels Sunday as 星期日 rather than 星期一', () => {
+ expect(formatDate(new Date(2026, 7, 2))).toBe('8月2日 · 星期日');
+ });
+});
+
+describe('formatFullDate', () => {
+ it('includes the year', () => {
+ expect(formatFullDate(new Date(2026, 6, 29))).toBe('2026年7月29日 · 星期三');
+ });
+});
+
+describe('formatMonthDay', () => {
+ it('does not pad single-digit values', () => {
+ expect(formatMonthDay(new Date(2026, 0, 5))).toBe('1月5日');
+ });
+});
+
+describe('formatWeekRange', () => {
+ it('spans seven days from the given start', () => {
+ expect(formatWeekRange(new Date(2026, 6, 27))).toBe('7月27日—8月2日');
+ });
+});
diff --git a/frontend/app.json b/frontend/app.json
index 26da726..2a0a481 100644
--- a/frontend/app.json
+++ b/frontend/app.json
@@ -8,13 +8,30 @@
"userInterfaceStyle": "light",
"ios": {
"infoPlist": {
- "NSLocationWhenInUseUsageDescription": "允许 Timeflow 获取当前位置,以便在地图选点时显示你的位置。"
+ "NSLocationWhenInUseUsageDescription": "允许 Timeflow 获取当前位置,以便在地图选点和地点提醒时使用。",
+ "NSLocationAlwaysAndWhenInUseUsageDescription": "允许 Timeflow 在后台获取当前位置,以便触发地点提醒。",
+ "NSMicrophoneUsageDescription": "允许 Timeflow 录制语音,以便将语音整理成日程。",
+ "UIBackgroundModes": ["location"]
},
"supportsTablet": true
},
"android": {
"package": "com.timeflow",
- "permissions": ["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"],
+ "permissions": [
+ "ACCESS_COARSE_LOCATION",
+ "ACCESS_FINE_LOCATION",
+ "ACCESS_BACKGROUND_LOCATION",
+ "RECORD_AUDIO",
+ "SCHEDULE_EXACT_ALARM",
+ "POST_NOTIFICATIONS",
+ "USE_FULL_SCREEN_INTENT",
+ "SYSTEM_ALERT_WINDOW",
+ "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
+ "FOREGROUND_SERVICE",
+ "FOREGROUND_SERVICE_LOCATION",
+ "FOREGROUND_SERVICE_MEDIA_PLAYBACK",
+ "VIBRATE"
+ ],
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
@@ -25,15 +42,16 @@
"predictiveBackGestureEnabled": false
},
"plugins": [
+ "./plugins/withTimeflowAlarm",
+ "./plugins/withTimeflowVoiceRecorder",
[
"expo-image-picker",
{
"cameraPermission": false,
- "microphonePermission": false,
+ "microphonePermission": "允许 Timeflow 使用麦克风录制语音。",
"photosPermission": "允许 Timeflow 访问照片,以便向智能助手发送图片。"
}
- ],
- "@react-native-community/datetimepicker"
+ ]
]
}
}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index f041633..d9e20b2 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -8,11 +8,164 @@ module.exports = defineConfig([
expoConfig,
prettierConfig,
{
- ignores: ['dist/**', '.expo/**', 'web-build/**', 'node_modules/**'],
+ ignores: [
+ 'dist/**',
+ '.expo/**',
+ 'web-build/**',
+ 'node_modules/**',
+ '_backup_*/**',
+ '_shots/**',
+ '**/*.apk',
+ 'android/**',
+ 'ios/**',
+ 'modules/**',
+ ],
},
{
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
+ {
+ files: ['src/features/**/*.ts', 'src/features/**/*.tsx'],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: [
+ '@/app',
+ '@/app/*',
+ '@/features/*',
+ '@/features/*/*',
+ '@/infrastructure',
+ '@/infrastructure/*',
+ ],
+ message:
+ 'Features may depend only on contracts/shared and their own relative modules. Compose adapters in app.',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: [
+ 'src/contracts/**/*.ts',
+ 'src/contracts/**/*.tsx',
+ 'src/shared/**/*.ts',
+ 'src/shared/**/*.tsx',
+ ],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: [
+ '@/app',
+ '@/app/*',
+ '@/dev',
+ '@/dev/*',
+ '@/features/*',
+ '@/features/*/*',
+ '@/infrastructure',
+ '@/infrastructure/*',
+ ],
+ message: 'Contracts/shared must not depend on app, dev, features, or infrastructure.',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: ['src/infrastructure/**/*.ts', 'src/infrastructure/**/*.tsx'],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: ['@/app', '@/app/*', '@/dev', '@/dev/*', '@/features/*', '@/features/*/*'],
+ message: 'Infrastructure must not depend on app, dev, or feature implementations.',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: ['src/dev/**/*.ts', 'src/dev/**/*.tsx'],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: ['@/features/*', '@/features/*/*'],
+ message: 'Development fakes must not depend on feature-private implementations.',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: ['src/app/**/*.ts', 'src/app/**/*.tsx', 'App.tsx'],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ patterns: [
+ {
+ group: [
+ '@/features/*/hooks/*',
+ '@/features/*/components/*',
+ '@/features/*/data/*',
+ '@/features/*/domain/*',
+ '@/features/*/application/*',
+ '@/features/*/calendar/*',
+ '@/features/*/editor/*',
+ '@/features/*/detail/*',
+ '@/features/*/screens/*',
+ '@/features/*/model/*',
+ '@/features/*/location/*',
+ '@/features/*/native/*',
+ '@/features/*/presentation/*',
+ '@/features/*/services/*',
+ '@/features/*/utils/*',
+ ],
+ message: 'Import from @/features/ public entry instead of deep paths.',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: ['__tests__/**/*.ts', '__tests__/**/*.tsx'],
+ rules: {
+ '@typescript-eslint/no-require-imports': 'off',
+ 'import/first': 'off',
+ 'no-restricted-imports': 'off',
+ },
+ },
+ {
+ files: ['jest.setup.js'],
+ languageOptions: {
+ globals: {
+ jest: 'readonly',
+ },
+ },
+ },
+ {
+ files: ['react-native.config.js'],
+ languageOptions: {
+ globals: {
+ __dirname: 'readonly',
+ },
+ },
+ },
]);
diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js
new file mode 100644
index 0000000..496542b
--- /dev/null
+++ b/frontend/jest.setup.js
@@ -0,0 +1,34 @@
+// jest-expo installs fetch lazily; initialize it before the test environment is torn down.
+Reflect.get(globalThis, 'fetch');
+
+jest.mock('lucide-react-native', () => {
+ const React = require('react');
+ const { View } = require('react-native');
+ const Icon = (props) => React.createElement(View, { ...props, testID: props.testID ?? 'icon' });
+ return new Proxy(
+ {},
+ {
+ get: (_target, key) => (typeof key === 'string' && key !== '__esModule' ? Icon : undefined),
+ },
+ );
+});
+
+jest.mock('react-native-safe-area-context', () => {
+ return {
+ SafeAreaProvider: ({ children }) => children,
+ SafeAreaView: ({ children }) => children,
+ useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
+ };
+});
+
+jest.mock('react-native-webview', () => {
+ const React = require('react');
+ const { View } = require('react-native');
+ const WebView = React.forwardRef((props, _ref) =>
+ React.createElement(View, { testID: 'webview', ...props }),
+ );
+ WebView.displayName = 'MockWebView';
+ return {
+ WebView,
+ };
+});
diff --git a/frontend/modules/timeflow-alarm/README.md b/frontend/modules/timeflow-alarm/README.md
new file mode 100644
index 0000000..bac5b5e
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/README.md
@@ -0,0 +1,5 @@
+# timeflow-alarm
+
+Local React Native Android library providing `NativeModules.TimeflowAlarm`.
+
+Sources under `android/` are version-controlled. App-level permissions are injected by `plugins/withTimeflowAlarm.js` during `expo prebuild`. Autolinking registers `AlarmPackage` via `react-native.config.js` / the `file:modules/timeflow-alarm` dependency.
diff --git a/frontend/modules/timeflow-alarm/android/build.gradle b/frontend/modules/timeflow-alarm/android/build.gradle
new file mode 100644
index 0000000..b01e5c9
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+def getExtOrDefault(name, defaultValue) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
+}
+
+android {
+ namespace "com.timeflow.alarm"
+
+ compileSdkVersion getExtOrDefault('compileSdkVersion', 35)
+
+ defaultConfig {
+ minSdkVersion getExtOrDefault('minSdkVersion', 24)
+ targetSdkVersion getExtOrDefault('targetSdkVersion', 35)
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src/main/java']
+ assets.srcDirs = ['src/main/assets']
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation 'com.facebook.react:react-android'
+ implementation 'androidx.core:core-ktx:1.13.1'
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/AndroidManifest.xml b/frontend/modules/timeflow-alarm/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5aa4f92
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/modules/timeflow-alarm/android/src/main/assets/alarm_prompt.mp3 b/frontend/modules/timeflow-alarm/android/src/main/assets/alarm_prompt.mp3
new file mode 100644
index 0000000..95a3dcd
Binary files /dev/null and b/frontend/modules/timeflow-alarm/android/src/main/assets/alarm_prompt.mp3 differ
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmContract.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmContract.java
new file mode 100644
index 0000000..b6f00b5
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmContract.java
@@ -0,0 +1,15 @@
+package com.timeflow.alarm;
+
+final class AlarmContract {
+ static final String ACTION_FIRE_ALARM = "com.timeflow.FIRE_ALARM";
+ static final String EXTRA_ALARM_ID = "alarm_id";
+ static final String EXTRA_REQUEST_CODE = "request_code";
+ static final String EXTRA_TITLE = "alarm_title";
+ static final String CHANNEL_ID = "timeflow_alarm_channel_v1";
+ static final String PREFS_NAME = "timeflow_alarms";
+ static final String ALARMS_KEY = "pending_alarms";
+ static final String ALARM_URI_SCHEME = "timeflow-alarm";
+
+ private AlarmContract() {
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmModule.kt b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmModule.kt
new file mode 100644
index 0000000..7208aca
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmModule.kt
@@ -0,0 +1,163 @@
+package com.timeflow.alarm
+
+import android.Manifest
+import android.app.AlarmManager
+import android.app.NotificationManager
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.PowerManager
+import android.provider.Settings
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import com.facebook.react.bridge.WritableMap
+
+class AlarmModule(private val reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ override fun getName(): String = NAME
+
+ @ReactMethod
+ fun schedule(triggerAtMillis: Double, title: String?, promise: Promise) {
+ try {
+ val alarmId = AlarmScheduler.schedule(
+ reactContext,
+ triggerAtMillis.toLong(),
+ title ?: "日程提醒"
+ )
+ val result: WritableMap = Arguments.createMap()
+ result.putString("alarmId", alarmId)
+ promise.resolve(result)
+ } catch (error: IllegalArgumentException) {
+ promise.reject("TRIGGER_IN_PAST", error.message, error)
+ } catch (error: SecurityException) {
+ promise.reject("EXACT_ALARM_DENIED", error.message, error)
+ } catch (error: Exception) {
+ promise.reject("SCHEDULE_FAILED", error.message, error)
+ }
+ }
+
+ @ReactMethod
+ fun cancel(alarmId: String?, promise: Promise) {
+ try {
+ val cancelled = AlarmScheduler.cancel(reactContext, alarmId)
+ promise.resolve(cancelled)
+ } catch (error: Exception) {
+ promise.reject("CANCEL_FAILED", error.message, error)
+ }
+ }
+
+ @ReactMethod
+ fun getPermissionStatus(promise: Promise) {
+ try {
+ val status = Arguments.createMap()
+ val alarmManager =
+ reactContext.getSystemService(AlarmManager::class.java)
+ val exactAlarm = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ true
+ } else {
+ alarmManager?.canScheduleExactAlarms() == true
+ }
+ status.putBoolean("exactAlarm", exactAlarm)
+
+ val overlay = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ true
+ } else {
+ Settings.canDrawOverlays(reactContext)
+ }
+ status.putBoolean("overlay", overlay)
+
+ val notificationManager =
+ reactContext.getSystemService(NotificationManager::class.java)
+ val fullScreen = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ true
+ } else {
+ notificationManager?.canUseFullScreenIntent() == true
+ }
+ status.putBoolean("fullScreen", fullScreen)
+
+ val notifications = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ true
+ } else {
+ ContextCompat.checkSelfPermission(
+ reactContext,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ status.putBoolean("notifications", notifications)
+
+ val powerManager = reactContext.getSystemService(PowerManager::class.java)
+ val battery = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ true
+ } else {
+ powerManager?.isIgnoringBatteryOptimizations(reactContext.packageName) == true
+ }
+ status.putBoolean("battery", battery)
+
+ promise.resolve(status)
+ } catch (error: Exception) {
+ promise.reject("PERMISSION_STATUS_FAILED", error.message, error)
+ }
+ }
+
+ @ReactMethod
+ fun openPermissionSettings(kind: String?, promise: Promise) {
+ try {
+ val pkg = Uri.parse("package:${reactContext.packageName}")
+ val intent = when (kind) {
+ "exactAlarm" -> Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, pkg)
+ "overlay" -> Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, pkg)
+ "fullScreen" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, pkg)
+ } else {
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, pkg)
+ }
+ "battery" -> Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, pkg)
+ else -> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, pkg)
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ reactContext.startActivity(intent)
+ promise.resolve(true)
+ } catch (error: Exception) {
+ promise.reject("OPEN_SETTINGS_FAILED", error.message, error)
+ }
+ }
+
+ @ReactMethod
+ fun requestNotificationPermission(promise: Promise) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ promise.resolve(true)
+ return
+ }
+ val activity = reactContext.currentActivity
+ if (activity == null) {
+ promise.reject("NO_ACTIVITY", "Activity unavailable")
+ return
+ }
+ if (ContextCompat.checkSelfPermission(
+ reactContext,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ promise.resolve(true)
+ return
+ }
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 2401
+ )
+ // Result is delivered asynchronously; caller should re-check getPermissionStatus.
+ promise.resolve(false)
+ }
+
+ companion object {
+ const val NAME = "TimeflowAlarm"
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmPackage.kt b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmPackage.kt
new file mode 100644
index 0000000..8fb12c2
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmPackage.kt
@@ -0,0 +1,18 @@
+package com.timeflow.alarm
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class AlarmPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(AlarmModule(reactContext))
+ }
+
+ override fun createViewManagers(
+ reactContext: ReactApplicationContext
+ ): List> {
+ return emptyList()
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmReceiver.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmReceiver.java
new file mode 100644
index 0000000..50e5dc9
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmReceiver.java
@@ -0,0 +1,31 @@
+package com.timeflow.alarm;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+public final class AlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!AlarmContract.ACTION_FIRE_ALARM.equals(intent.getAction())) {
+ return;
+ }
+
+ int requestCode = intent.getIntExtra(AlarmContract.EXTRA_REQUEST_CODE, 0);
+ String alarmId = intent.getStringExtra(AlarmContract.EXTRA_ALARM_ID);
+ String title = intent.getStringExtra(AlarmContract.EXTRA_TITLE);
+ if (alarmId == null || alarmId.isEmpty()) {
+ alarmId = "legacy-" + requestCode;
+ }
+ Intent serviceIntent = new Intent(context, AlarmSoundService.class)
+ .putExtra(AlarmContract.EXTRA_ALARM_ID, alarmId)
+ .putExtra(AlarmContract.EXTRA_REQUEST_CODE, requestCode)
+ .putExtra(AlarmContract.EXTRA_TITLE, title);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(serviceIntent);
+ } else {
+ context.startService(serviceIntent);
+ }
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmRingUi.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmRingUi.java
new file mode 100644
index 0000000..ab01781
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmRingUi.java
@@ -0,0 +1,283 @@
+package com.timeflow.alarm;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.StateSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Reminder interrupt — clock and title sit in the absolute screen center.
+ * Colors match src/shared/theme/index.ts.
+ */
+final class AlarmRingUi {
+ private static final int COLOR_BACKGROUND = Color.parseColor("#F4F5F1");
+ private static final int COLOR_MINT = Color.parseColor("#DDEFE5");
+ private static final int COLOR_DEEP = Color.parseColor("#15352B");
+ private static final int COLOR_SUB = Color.parseColor("#6C7972");
+ private static final int COLOR_LIME = Color.parseColor("#D7F36A");
+
+ private static final String FALLBACK_TITLE = "日程提醒";
+ private static final long ENTER_MILLIS = 420L;
+ private static final long ENTER_STAGGER_MILLIS = 60L;
+ private static final float ENTER_OFFSET_DP = 10f;
+
+ private AlarmRingUi() {
+ }
+
+ static int topEdgeColor() {
+ return COLOR_MINT;
+ }
+
+ static int bottomEdgeColor() {
+ return COLOR_BACKGROUND;
+ }
+
+ static FrameLayout build(Context context, String scheduleTitle, View.OnClickListener onStop) {
+ float d = context.getResources().getDisplayMetrics().density;
+ int gutter = Math.round(28 * d);
+
+ TextView clock = text(context, formatClock(), 66, COLOR_DEEP, condensedBold());
+ clock.setLetterSpacing(-0.04f);
+ clock.setFontFeatureSettings("tnum");
+ clock.setGravity(Gravity.CENTER);
+
+ TextView date = text(context, formatDate(), 15, COLOR_SUB, regular());
+ date.setGravity(Gravity.CENTER);
+
+ RingRoot root = new RingRoot(context, () -> {
+ clock.setText(formatClock());
+ date.setText(formatDate());
+ });
+ root.setBackground(buildBackground());
+
+ TextView brand = text(context, "Timeflow", 36, COLOR_DEEP, bold());
+ brand.setLetterSpacing(-0.03f);
+ brand.setGravity(Gravity.CENTER);
+ FrameLayout.LayoutParams brandParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ );
+ brandParams.topMargin = Math.round(48 * d);
+ root.addView(brand, brandParams);
+
+ TextView moment = text(context, "此刻提醒", 13, COLOR_SUB, medium());
+ moment.setGravity(Gravity.CENTER);
+ FrameLayout.LayoutParams momentParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ );
+ momentParams.topMargin = Math.round(92 * d);
+ root.addView(moment, momentParams);
+
+ LinearLayout center = new LinearLayout(context);
+ center.setOrientation(LinearLayout.VERTICAL);
+ center.setGravity(Gravity.CENTER_HORIZONTAL);
+ FrameLayout.LayoutParams centerParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER
+ );
+ centerParams.leftMargin = gutter;
+ centerParams.rightMargin = gutter;
+ root.addView(center, centerParams);
+
+ center.addView(clock, matchWidth());
+
+ LinearLayout.LayoutParams dateParams = matchWidth();
+ dateParams.topMargin = Math.round(14 * d);
+ center.addView(date, dateParams);
+
+ View divider = new View(context);
+ GradientDrawable dividerBg = new GradientDrawable();
+ dividerBg.setColor(COLOR_LIME);
+ dividerBg.setCornerRadius(999 * d);
+ divider.setBackground(dividerBg);
+ LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams(
+ Math.round(40 * d),
+ Math.round(3 * d)
+ );
+ dividerParams.gravity = Gravity.CENTER_HORIZONTAL;
+ dividerParams.topMargin = Math.round(26 * d);
+ center.addView(divider, dividerParams);
+
+ TextView title = text(context, resolveTitle(scheduleTitle), 32, COLOR_DEEP, bold());
+ title.setMaxLines(3);
+ title.setEllipsize(TextUtils.TruncateAt.END);
+ title.setLineSpacing(0f, 1.25f);
+ title.setGravity(Gravity.CENTER);
+ LinearLayout.LayoutParams titleParams = matchWidth();
+ titleParams.topMargin = Math.round(20 * d);
+ center.addView(title, titleParams);
+
+ TextView stop = buildStopButton(context, d, onStop);
+ FrameLayout.LayoutParams stopParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ Math.round(58 * d),
+ Gravity.BOTTOM
+ );
+ stopParams.leftMargin = gutter;
+ stopParams.rightMargin = gutter;
+ stopParams.bottomMargin = Math.round(28 * d);
+ root.addView(stop, stopParams);
+
+ if (animatorsEnabled(context)) {
+ View[] sequence = {brand, moment, center, stop};
+ root.post(() -> enter(sequence, d));
+ }
+
+ return root;
+ }
+
+ private static TextView buildStopButton(Context context, float d, View.OnClickListener onStop) {
+ TextView stop = text(context, "停止提醒", 18, COLOR_DEEP, bold());
+ stop.setGravity(Gravity.CENTER);
+ stop.setContentDescription("停止提醒");
+ stop.setClickable(true);
+ stop.setFocusable(true);
+ stop.setElevation(3 * d);
+ stop.setOnClickListener(onStop);
+
+ StateListDrawable fill = new StateListDrawable();
+ fill.addState(new int[]{android.R.attr.state_focused}, pill(d, Math.round(2 * d)));
+ fill.addState(StateSet.WILD_CARD, pill(d, 0));
+ stop.setBackground(new RippleDrawable(
+ ColorStateList.valueOf(Color.argb(38, 21, 53, 43)),
+ fill,
+ null
+ ));
+ return stop;
+ }
+
+ private static GradientDrawable pill(float d, int strokeWidth) {
+ GradientDrawable pill = new GradientDrawable();
+ pill.setColor(COLOR_LIME);
+ pill.setCornerRadius(20 * d);
+ if (strokeWidth > 0) {
+ pill.setStroke(strokeWidth, COLOR_DEEP);
+ }
+ return pill;
+ }
+
+ private static void enter(View[] sequence, float d) {
+ for (int index = 0; index < sequence.length; index++) {
+ View view = sequence[index];
+ view.setAlpha(0f);
+ view.setTranslationY(ENTER_OFFSET_DP * d);
+ view.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setStartDelay(index * ENTER_STAGGER_MILLIS)
+ .setDuration(ENTER_MILLIS)
+ .setInterpolator(new DecelerateInterpolator(1.6f))
+ .start();
+ }
+ }
+
+ private static String resolveTitle(String scheduleTitle) {
+ return scheduleTitle == null || scheduleTitle.isEmpty() ? FALLBACK_TITLE : scheduleTitle;
+ }
+
+ private static String formatClock() {
+ return new SimpleDateFormat("HH:mm", Locale.getDefault()).format(new Date());
+ }
+
+ private static String formatDate() {
+ return new SimpleDateFormat("M月d日 EEEE", Locale.CHINA).format(new Date());
+ }
+
+ private static TextView text(Context context, String value, float sizeSp, int color, Typeface face) {
+ TextView view = new TextView(context);
+ view.setText(value);
+ view.setTextSize(sizeSp);
+ view.setTextColor(color);
+ view.setTypeface(face);
+ return view;
+ }
+
+ private static Typeface condensedBold() {
+ return Typeface.create("sans-serif-condensed", Typeface.BOLD);
+ }
+
+ private static Typeface medium() {
+ return Typeface.create("sans-serif-medium", Typeface.NORMAL);
+ }
+
+ private static Typeface bold() {
+ return Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
+ }
+
+ private static Typeface regular() {
+ return Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
+ }
+
+ private static GradientDrawable buildBackground() {
+ return new GradientDrawable(
+ GradientDrawable.Orientation.TOP_BOTTOM,
+ new int[]{COLOR_MINT, COLOR_BACKGROUND, COLOR_BACKGROUND, COLOR_BACKGROUND}
+ );
+ }
+
+ private static LinearLayout.LayoutParams matchWidth() {
+ return new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ }
+
+ private static boolean animatorsEnabled(Context context) {
+ return Settings.Global.getFloat(
+ context.getContentResolver(),
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f
+ ) > 0f;
+ }
+
+ private static final class RingRoot extends FrameLayout {
+ private static final long TICK_MILLIS = 20_000L;
+
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private final Runnable onTick;
+ private final Runnable ticker = new Runnable() {
+ @Override
+ public void run() {
+ onTick.run();
+ handler.postDelayed(this, TICK_MILLIS);
+ }
+ };
+
+ RingRoot(Context context, Runnable onTick) {
+ super(context);
+ this.onTick = onTick;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ handler.postDelayed(ticker, TICK_MILLIS);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ handler.removeCallbacks(ticker);
+ super.onDetachedFromWindow();
+ }
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmScheduler.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmScheduler.java
new file mode 100644
index 0000000..637cb53
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmScheduler.java
@@ -0,0 +1,273 @@
+package com.timeflow.alarm;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * UI-independent AlarmManager scheduler.
+ * Persists UUID / triggerAtMillis / requestCode for exact cancel & rebuild.
+ */
+public final class AlarmScheduler {
+ private AlarmScheduler() {
+ }
+
+ public static final class AlarmRecord {
+ public final String alarmId;
+ public final long triggerAtMillis;
+ public final int requestCode;
+ public final String title;
+ public final boolean legacy;
+
+ AlarmRecord(
+ String alarmId,
+ long triggerAtMillis,
+ int requestCode,
+ String title,
+ boolean legacy
+ ) {
+ this.alarmId = alarmId;
+ this.triggerAtMillis = triggerAtMillis;
+ this.requestCode = requestCode;
+ this.title = title;
+ this.legacy = legacy;
+ }
+ }
+
+ public static String schedule(Context context, long triggerAtMillis, String title) {
+ if (triggerAtMillis <= System.currentTimeMillis()) {
+ throw new IllegalArgumentException("trigger_in_past");
+ }
+
+ AlarmManager alarmManager =
+ (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ throw new IllegalStateException("alarm_manager_unavailable");
+ }
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
+ && !alarmManager.canScheduleExactAlarms()) {
+ throw new SecurityException("exact_alarm_denied");
+ }
+
+ List alarms = loadAlarms(context);
+ String alarmId = UUID.randomUUID().toString();
+ AlarmRecord record = new AlarmRecord(
+ alarmId,
+ triggerAtMillis,
+ nextRequestCode(alarms),
+ title == null ? "" : title,
+ false
+ );
+
+ PendingIntent operation = buildAlarmBroadcastPendingIntent(
+ context,
+ record,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
+ );
+ Intent showIntent = context.getPackageManager()
+ .getLaunchIntentForPackage(context.getPackageName());
+ if (showIntent == null) {
+ showIntent = new Intent(Intent.ACTION_MAIN)
+ .setPackage(context.getPackageName());
+ }
+ showIntent.setData(alarmUri(record.alarmId))
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ PendingIntent showPendingIntent = PendingIntent.getActivity(
+ context,
+ record.requestCode,
+ showIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
+ );
+ alarmManager.setAlarmClock(
+ new AlarmManager.AlarmClockInfo(record.triggerAtMillis, showPendingIntent),
+ operation
+ );
+
+ alarms.add(record);
+ saveAlarms(context, alarms);
+ return alarmId;
+ }
+
+ public static boolean cancel(Context context, String alarmId) {
+ if (alarmId == null || alarmId.isEmpty()) {
+ return false;
+ }
+ List alarms = loadAlarms(context);
+ int index = indexById(alarms, alarmId);
+ if (index < 0) {
+ return false;
+ }
+
+ AlarmRecord record = alarms.get(index);
+ AlarmManager alarmManager =
+ (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ throw new IllegalStateException("alarm_manager_unavailable");
+ }
+
+ PendingIntent operation = buildAlarmBroadcastPendingIntent(
+ context,
+ record,
+ PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
+ );
+ if (operation != null) {
+ alarmManager.cancel(operation);
+ operation.cancel();
+ }
+
+ alarms.remove(index);
+ saveAlarms(context, alarms);
+ return true;
+ }
+
+ public static List loadAlarms(Context context) {
+ SharedPreferences preferences =
+ context.getSharedPreferences(AlarmContract.PREFS_NAME, Context.MODE_PRIVATE);
+ String serialized = preferences.getString(AlarmContract.ALARMS_KEY, "[]");
+ List alarms = new ArrayList<>();
+ try {
+ JSONArray array = new JSONArray(serialized);
+ for (int index = 0; index < array.length(); index++) {
+ Object value = array.get(index);
+ if (!(value instanceof JSONObject)) {
+ continue;
+ }
+ JSONObject object = (JSONObject) value;
+ long triggerAt = object.optLong("trigger_at", -1L);
+ int requestCode = object.optInt("request_code", -1);
+ if (triggerAt <= 0 || requestCode < 0) {
+ continue;
+ }
+ String alarmId = object.optString("alarm_id", "");
+ boolean legacy = object.optBoolean("legacy", alarmId.isEmpty());
+ if (alarmId.isEmpty()) {
+ alarmId = "legacy-" + requestCode;
+ }
+ alarms.add(new AlarmRecord(
+ alarmId,
+ triggerAt,
+ requestCode,
+ object.optString("title", ""),
+ legacy
+ ));
+ }
+ } catch (JSONException ignored) {
+ alarms.clear();
+ }
+ return alarms;
+ }
+
+ static void removeAlarmRecord(Context context, String alarmId, int requestCode) {
+ List alarms = loadAlarms(context);
+ JSONArray remaining = new JSONArray();
+ for (AlarmRecord alarm : alarms) {
+ boolean match;
+ if (!alarm.alarmId.isEmpty()) {
+ match = alarm.alarmId.equals(alarmId);
+ } else {
+ match = alarm.requestCode == requestCode;
+ }
+ if (match) {
+ continue;
+ }
+ try {
+ JSONObject object = new JSONObject();
+ object.put("alarm_id", alarm.alarmId);
+ object.put("trigger_at", alarm.triggerAtMillis);
+ object.put("request_code", alarm.requestCode);
+ object.put("title", alarm.title);
+ object.put("legacy", alarm.legacy);
+ remaining.put(object);
+ } catch (JSONException ignored) {
+ // ignore
+ }
+ }
+ context.getSharedPreferences(AlarmContract.PREFS_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putString(AlarmContract.ALARMS_KEY, remaining.toString())
+ .apply();
+ }
+
+ static Uri alarmUri(String alarmId) {
+ return Uri.parse(
+ AlarmContract.ALARM_URI_SCHEME + "://alarm/" + Uri.encode(alarmId)
+ );
+ }
+
+ private static void saveAlarms(Context context, List alarms) {
+ JSONArray array = new JSONArray();
+ for (AlarmRecord alarm : alarms) {
+ try {
+ JSONObject object = new JSONObject();
+ object.put("alarm_id", alarm.alarmId);
+ object.put("trigger_at", alarm.triggerAtMillis);
+ object.put("request_code", alarm.requestCode);
+ object.put("title", alarm.title);
+ object.put("legacy", alarm.legacy);
+ array.put(object);
+ } catch (JSONException ignored) {
+ // ignore
+ }
+ }
+ context.getSharedPreferences(AlarmContract.PREFS_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putString(AlarmContract.ALARMS_KEY, array.toString())
+ .apply();
+ }
+
+ private static PendingIntent buildAlarmBroadcastPendingIntent(
+ Context context,
+ AlarmRecord record,
+ int flags
+ ) {
+ Intent intent = new Intent(context, AlarmReceiver.class)
+ .setAction(AlarmContract.ACTION_FIRE_ALARM)
+ .putExtra(AlarmContract.EXTRA_ALARM_ID, record.alarmId)
+ .putExtra(AlarmContract.EXTRA_REQUEST_CODE, record.requestCode)
+ .putExtra(AlarmContract.EXTRA_TITLE, record.title);
+ if (!record.legacy) {
+ intent.setData(alarmUri(record.alarmId));
+ }
+ return PendingIntent.getBroadcast(context, record.requestCode, intent, flags);
+ }
+
+ private static int nextRequestCode(List alarms) {
+ int requestCode = (int) (System.currentTimeMillis() & 0x7fffffff);
+ if (requestCode == 0) {
+ requestCode = 1;
+ }
+ while (containsRequestCode(alarms, requestCode)) {
+ requestCode = requestCode == Integer.MAX_VALUE ? 1 : requestCode + 1;
+ }
+ return requestCode;
+ }
+
+ private static boolean containsRequestCode(List alarms, int requestCode) {
+ for (AlarmRecord alarm : alarms) {
+ if (alarm.requestCode == requestCode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int indexById(List alarms, String alarmId) {
+ for (int index = 0; index < alarms.size(); index++) {
+ if (alarms.get(index).alarmId.equals(alarmId)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmSoundService.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmSoundService.java
new file mode 100644
index 0000000..0ed007c
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/AlarmSoundService.java
@@ -0,0 +1,344 @@
+package com.timeflow.alarm;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.ActivityOptions;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.graphics.PixelFormat;
+import android.media.AudioAttributes;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.provider.Settings;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+
+public final class AlarmSoundService extends Service {
+ private static final long SPEECH_REPEAT_DELAY_MILLIS = 1_500L;
+
+ private final Handler playbackHandler = new Handler(Looper.getMainLooper());
+ private final Runnable replaySpeech = this::replaySpeech;
+
+ private MediaPlayer mediaPlayer;
+ private boolean destroyed;
+ private File bundledSpeechFile;
+ private WindowManager overlayWindowManager;
+ private View overlayView;
+ private String alarmId;
+ private String alarmTitle;
+ private int requestCode;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ requestCode = intent == null
+ ? 0
+ : intent.getIntExtra(AlarmContract.EXTRA_REQUEST_CODE, 0);
+ alarmId = intent == null
+ ? null
+ : intent.getStringExtra(AlarmContract.EXTRA_ALARM_ID);
+ alarmTitle = intent == null
+ ? null
+ : intent.getStringExtra(AlarmContract.EXTRA_TITLE);
+ if (alarmId == null || alarmId.isEmpty()) {
+ alarmId = "legacy-" + requestCode;
+ }
+ if (alarmTitle == null || alarmTitle.isEmpty()) {
+ alarmTitle = "日程提醒";
+ }
+
+ createNotificationChannel();
+ Notification notification = buildNotification(alarmId, alarmTitle);
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ requestCode,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ );
+ } else {
+ startForeground(requestCode, notification);
+ }
+ removeFromSavedAlarms();
+ showAlarmOverlay(alarmTitle);
+ if (mediaPlayer == null) {
+ startBundledSpeech();
+ }
+ } catch (RuntimeException exception) {
+ stopSelf();
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ destroyed = true;
+ playbackHandler.removeCallbacksAndMessages(null);
+ removeAlarmOverlay();
+ releaseMediaPlayer();
+ deleteCachedSpeechFile();
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (manager != null) {
+ manager.cancel(requestCode);
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ static void stop(Context context) {
+ context.stopService(new Intent(context, AlarmSoundService.class));
+ }
+
+ static void start(Context context, String alarmId, int requestCode, String title) {
+ Intent intent = new Intent(context, AlarmSoundService.class)
+ .putExtra(AlarmContract.EXTRA_ALARM_ID, alarmId)
+ .putExtra(AlarmContract.EXTRA_REQUEST_CODE, requestCode)
+ .putExtra(AlarmContract.EXTRA_TITLE, title);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ } else {
+ context.startService(intent);
+ }
+ }
+
+ private Notification buildNotification(String alarmId, String title) {
+ Intent ringIntent = new Intent(this, RingActivity.class)
+ .setData(alarmUri(alarmId))
+ .putExtra(AlarmContract.EXTRA_ALARM_ID, alarmId)
+ .putExtra(AlarmContract.EXTRA_REQUEST_CODE, requestCode)
+ .putExtra(AlarmContract.EXTRA_TITLE, title)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ PendingIntent fullScreenIntent = PendingIntent.getActivity(
+ this,
+ requestCode,
+ ringIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
+ pendingIntentOptions()
+ );
+ return new Notification.Builder(this, AlarmContract.CHANNEL_ID)
+ .setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
+ .setContentTitle(title)
+ .setContentText("点击停止提醒")
+ .setCategory(Notification.CATEGORY_ALARM)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setPriority(Notification.PRIORITY_MAX)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .setFullScreenIntent(fullScreenIntent, true)
+ .build();
+ }
+
+ private Uri alarmUri(String value) {
+ return AlarmScheduler.alarmUri(value);
+ }
+
+ private android.os.Bundle pendingIntentOptions() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ return null;
+ }
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setPendingIntentCreatorBackgroundActivityStartMode(
+ backgroundActivityStartMode()
+ );
+ return options.toBundle();
+ }
+
+ private int backgroundActivityStartMode() {
+ if (Build.VERSION.SDK_INT >= 36) {
+ return ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
+ }
+ return ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return;
+ }
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (manager == null || manager.getNotificationChannel(AlarmContract.CHANNEL_ID) != null) {
+ return;
+ }
+ NotificationChannel channel = new NotificationChannel(
+ AlarmContract.CHANNEL_ID,
+ "Timeflow",
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ channel.setDescription("日程闹钟提醒");
+ channel.enableVibration(true);
+ channel.setSound(null, null);
+ manager.createNotificationChannel(channel);
+ }
+
+ private void showAlarmOverlay(String title) {
+ if (overlayView != null
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || !Settings.canDrawOverlays(this)) {
+ return;
+ }
+
+ overlayWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ if (overlayWindowManager == null) {
+ return;
+ }
+
+ View content = AlarmRingUi.build(this, title, view -> {
+ removeAlarmOverlay();
+ RingActivity.finishIfOpen();
+ stopSelf();
+ });
+ int windowType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+ : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
+ int windowFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ windowType,
+ windowFlags,
+ PixelFormat.OPAQUE
+ );
+ params.gravity = Gravity.TOP | Gravity.START;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ params.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ }
+ content.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ );
+
+ try {
+ overlayWindowManager.addView(content, params);
+ overlayView = content;
+ } catch (RuntimeException exception) {
+ overlayWindowManager = null;
+ }
+ }
+
+ private void removeAlarmOverlay() {
+ if (overlayWindowManager != null && overlayView != null) {
+ try {
+ overlayWindowManager.removeViewImmediate(overlayView);
+ } catch (RuntimeException ignored) {
+ // The system may already have removed the overlay window.
+ }
+ }
+ overlayView = null;
+ overlayWindowManager = null;
+ }
+
+ private void startBundledSpeech() {
+ if (destroyed || mediaPlayer != null) {
+ return;
+ }
+ try {
+ bundledSpeechFile = new File(getCacheDir(), "alarm_prompt_edge.mp3");
+ try (InputStream input = getAssets().open("alarm_prompt.mp3");
+ FileOutputStream output = new FileOutputStream(bundledSpeechFile, false)) {
+ byte[] buffer = new byte[8_192];
+ int count;
+ while ((count = input.read(buffer)) != -1) {
+ output.write(buffer, 0, count);
+ }
+ }
+ startAudioPlayback(bundledSpeechFile);
+ } catch (Exception exception) {
+ releaseMediaPlayer();
+ }
+ }
+
+ private void startAudioPlayback(File audioFile) {
+ if (destroyed || mediaPlayer != null || audioFile == null || !audioFile.isFile()) {
+ return;
+ }
+ try {
+ MediaPlayer player = new MediaPlayer();
+ player.setAudioAttributes(new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+ .build());
+ player.setDataSource(audioFile.getAbsolutePath());
+ player.setVolume(1.0f, 1.0f);
+ player.setOnCompletionListener(completed ->
+ playbackHandler.postDelayed(replaySpeech, SPEECH_REPEAT_DELAY_MILLIS));
+ player.setOnErrorListener((failed, what, extra) -> {
+ releaseMediaPlayer();
+ return true;
+ });
+ player.prepare();
+ mediaPlayer = player;
+ player.start();
+ } catch (Exception exception) {
+ releaseMediaPlayer();
+ }
+ }
+
+ private void replaySpeech() {
+ if (destroyed || mediaPlayer == null) {
+ return;
+ }
+ try {
+ mediaPlayer.seekTo(0);
+ mediaPlayer.start();
+ } catch (IllegalStateException ignored) {
+ releaseMediaPlayer();
+ }
+ }
+
+ private void releaseMediaPlayer() {
+ playbackHandler.removeCallbacks(replaySpeech);
+ if (mediaPlayer == null) {
+ return;
+ }
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.setOnErrorListener(null);
+ try {
+ mediaPlayer.stop();
+ } catch (IllegalStateException ignored) {
+ // The player may already have completed or failed.
+ }
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+
+ private void deleteCachedSpeechFile() {
+ if (bundledSpeechFile != null) {
+ bundledSpeechFile.delete();
+ }
+ }
+
+ private void removeFromSavedAlarms() {
+ AlarmScheduler.removeAlarmRecord(this, alarmId, requestCode);
+ }
+
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/DayRulerView.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/DayRulerView.java
new file mode 100644
index 0000000..5c08d1d
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/DayRulerView.java
@@ -0,0 +1,159 @@
+package com.timeflow.alarm;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.provider.Settings;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import java.util.Calendar;
+
+/**
+ * Today as a measured scale: one graduation per hour, a longer mark every six
+ * hours, and a lime needle resting on the minute the alarm went off. Graduations
+ * the day has already spent are drawn faint, the hours still ahead hold weight.
+ *
+ * This is the reminder screen's only structural device, and it is the only thing
+ * there that says where the interruption sits in the day.
+ */
+final class DayRulerView extends View {
+ private static final int HOURS_PER_DAY = 24;
+ private static final int HOURS_PER_QUARTER = 6;
+ private static final long BREATH_MILLIS = 1_700L;
+ private static final int ALPHA_AHEAD = 56;
+ private static final int ALPHA_SPENT = 23;
+ private static final float HALO_ALPHA_TIGHT = 0.30f;
+ private static final float HALO_ALPHA_WIDE = 0.08f;
+ private static final float BREATH_AT_REST = 0.4f;
+
+ private final Paint tickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint haloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private final float hourTickLength;
+ private final float quarterTickLength;
+ private final float tickThickness;
+ private final float needleThickness;
+ private final float haloTightHeight;
+ private final float haloWideHeight;
+ private final float haloRadius;
+ private final boolean breathing;
+
+ private ValueAnimator breathAnimator;
+ private float breath;
+ private float dayFraction;
+
+ DayRulerView(Context context, int tickColor, int needleColor) {
+ super(context);
+ float density = context.getResources().getDisplayMetrics().density;
+ hourTickLength = 9 * density;
+ quarterTickLength = 17 * density;
+ tickThickness = 1.5f * density;
+ needleThickness = 3 * density;
+ haloTightHeight = 8 * density;
+ haloWideHeight = 16 * density;
+ haloRadius = 8 * density;
+
+ tickPaint.setColor(tickColor);
+ needlePaint.setColor(needleColor);
+ haloPaint.setColor(needleColor);
+
+ breathing = animatorsEnabled(context);
+ breath = breathing ? 0f : BREATH_AT_REST;
+ syncToClock();
+ }
+
+ /** Re-reads the wall clock so the needle keeps pace with a long ring. */
+ void syncToClock() {
+ Calendar now = Calendar.getInstance();
+ int minutesIntoDay = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
+ dayFraction = minutesIntoDay / (float) (HOURS_PER_DAY * 60);
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ float height = getHeight();
+ float width = getWidth();
+ if (height <= 0 || width <= 0) {
+ return;
+ }
+
+ float needleY = clampToTrack(height * dayFraction, height, needleThickness);
+ for (int hour = 0; hour <= HOURS_PER_DAY; hour++) {
+ float y = clampToTrack(height * hour / HOURS_PER_DAY, height, tickThickness);
+ float length = hour % HOURS_PER_QUARTER == 0 ? quarterTickLength : hourTickLength;
+ tickPaint.setAlpha(y < needleY ? ALPHA_SPENT : ALPHA_AHEAD);
+ drawBar(canvas, y, length, tickThickness, tickThickness, tickPaint);
+ }
+
+ float haloHeight = haloTightHeight + (haloWideHeight - haloTightHeight) * breath;
+ float haloAlpha = HALO_ALPHA_TIGHT + (HALO_ALPHA_WIDE - HALO_ALPHA_TIGHT) * breath;
+ haloPaint.setAlpha(Math.round(haloAlpha * 255f));
+ drawBar(canvas, needleY, width, haloHeight, haloRadius, haloPaint);
+ drawBar(canvas, needleY, width, needleThickness, needleThickness, needlePaint);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (!breathing || breathAnimator != null) {
+ return;
+ }
+ breathAnimator = ValueAnimator.ofFloat(0f, 1f);
+ breathAnimator.setDuration(BREATH_MILLIS);
+ breathAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ breathAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ breathAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+ breathAnimator.addUpdateListener(animator -> {
+ breath = (float) animator.getAnimatedValue();
+ invalidate();
+ });
+ breathAnimator.start();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (breathAnimator != null) {
+ breathAnimator.cancel();
+ breathAnimator = null;
+ }
+ super.onDetachedFromWindow();
+ }
+
+ private static void drawBar(
+ Canvas canvas,
+ float centerY,
+ float length,
+ float thickness,
+ float radius,
+ Paint paint
+ ) {
+ canvas.drawRoundRect(
+ 0f,
+ centerY - thickness / 2f,
+ length,
+ centerY + thickness / 2f,
+ radius,
+ radius,
+ paint
+ );
+ }
+
+ /** Keeps the first and last marks of the day fully inside the column. */
+ private static float clampToTrack(float y, float height, float thickness) {
+ float inset = thickness / 2f;
+ return Math.min(Math.max(y, inset), height - inset);
+ }
+
+ private static boolean animatorsEnabled(Context context) {
+ return Settings.Global.getFloat(
+ context.getContentResolver(),
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f
+ ) > 0f;
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/RingActivity.java b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/RingActivity.java
new file mode 100644
index 0000000..053ae77
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/android/src/main/java/com/timeflow/alarm/RingActivity.java
@@ -0,0 +1,178 @@
+package com.timeflow.alarm;
+
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+
+import java.lang.ref.WeakReference;
+
+public final class RingActivity extends Activity {
+ private static WeakReference currentActivity = new WeakReference<>(null);
+
+ private String alarmId;
+ private String alarmTitle;
+ private int requestCode;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ currentActivity = new WeakReference<>(this);
+ requestCode = getIntent().getIntExtra(AlarmContract.EXTRA_REQUEST_CODE, 0);
+ alarmId = getIntent().getStringExtra(AlarmContract.EXTRA_ALARM_ID);
+ alarmTitle = getIntent().getStringExtra(AlarmContract.EXTRA_TITLE);
+ if (alarmId == null || alarmId.isEmpty()) {
+ alarmId = "legacy-" + requestCode;
+ }
+ if (alarmTitle == null || alarmTitle.isEmpty()) {
+ alarmTitle = "日程提醒";
+ }
+ makeVisibleOverLockScreen();
+ matchSystemBarsToReminder();
+ setContentView(buildContentView());
+ removeFromSavedAlarms();
+ AlarmSoundService.start(
+ this,
+ alarmId,
+ requestCode,
+ alarmTitle
+ );
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (currentActivity.get() == this) {
+ currentActivity.clear();
+ }
+ super.onDestroy();
+ }
+
+ static void finishIfOpen() {
+ RingActivity activity = currentActivity.get();
+ if (activity != null && !activity.isFinishing()) {
+ activity.finishAndRemoveTask();
+ }
+ }
+
+ private View buildContentView() {
+ return AlarmRingUi.build(this, alarmTitle, view -> stopAndClose());
+ }
+
+ private void makeVisibleOverLockScreen() {
+ Window window = getWindow();
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ );
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true);
+ setTurnScreenOn(true);
+ }
+ }
+
+ /**
+ * The reminder is a light surface, so the system bars have to carry dark icons —
+ * a device in dark mode would otherwise draw white icons onto near-white.
+ */
+ private void matchSystemBarsToReminder() {
+ Window window = getWindow();
+ window.setStatusBarColor(AlarmRingUi.topEdgeColor());
+ window.setNavigationBarColor(AlarmRingUi.bottomEdgeColor());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ WindowInsetsController controller = window.getInsetsController();
+ if (controller != null) {
+ controller.setSystemBarsAppearance(
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+ );
+ }
+ return;
+ }
+ window.getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ );
+ }
+
+ private void stopAndClose() {
+ AlarmSoundService.stop(this);
+ cancelNotification();
+ cancelAlarmPendingIntent();
+ finishAndRemoveTask();
+ }
+
+ private void cancelNotification() {
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (manager != null) {
+ manager.cancel(requestCode);
+ }
+ }
+
+ private void cancelAlarmPendingIntent() {
+ AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ return;
+ }
+
+ Intent activityIntent = new Intent(this, RingActivity.class)
+ .setData(alarmUri(alarmId));
+ PendingIntent activityPendingIntent = PendingIntent.getActivity(
+ this,
+ requestCode,
+ activityIntent,
+ PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
+ );
+ Intent broadcastIntent = new Intent(this, AlarmReceiver.class)
+ .setAction(AlarmContract.ACTION_FIRE_ALARM);
+ if (!isLegacyAlarm()) {
+ broadcastIntent.setData(alarmUri(alarmId));
+ }
+ PendingIntent broadcastPendingIntent = PendingIntent.getBroadcast(
+ this,
+ requestCode,
+ broadcastIntent,
+ PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
+ );
+ if (activityPendingIntent != null) {
+ alarmManager.cancel(activityPendingIntent);
+ activityPendingIntent.cancel();
+ }
+ if (broadcastPendingIntent != null) {
+ alarmManager.cancel(broadcastPendingIntent);
+ broadcastPendingIntent.cancel();
+ }
+ }
+
+ private Uri alarmUri(String value) {
+ return AlarmScheduler.alarmUri(value);
+ }
+
+ private boolean isLegacyAlarm() {
+ return alarmId != null && alarmId.startsWith("legacy-");
+ }
+
+ private void removeFromSavedAlarms() {
+ AlarmScheduler.removeAlarmRecord(this, alarmId, requestCode);
+ }
+
+
+ @Override
+ public void onBackPressed() {
+ stopAndClose();
+ }
+
+}
diff --git a/frontend/modules/timeflow-alarm/index.js b/frontend/modules/timeflow-alarm/index.js
new file mode 100644
index 0000000..8e6ac90
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/index.js
@@ -0,0 +1,3 @@
+// Native module is linked via React Native autolinking (AlarmPackage).
+// JS callers use NativeModules.TimeflowAlarm from the app layer.
+module.exports = {};
diff --git a/frontend/modules/timeflow-alarm/package.json b/frontend/modules/timeflow-alarm/package.json
new file mode 100644
index 0000000..92afe2d
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "timeflow-alarm",
+ "version": "1.0.0",
+ "description": "Native Android exact-alarm bridge for Timeflow",
+ "main": "index.js",
+ "license": "UNLICENSED",
+ "private": true,
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+}
diff --git a/frontend/modules/timeflow-alarm/react-native.config.js b/frontend/modules/timeflow-alarm/react-native.config.js
new file mode 100644
index 0000000..60b6f8e
--- /dev/null
+++ b/frontend/modules/timeflow-alarm/react-native.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ dependency: {
+ platforms: {
+ android: {
+ sourceDir: './android',
+ packageImportPath: 'import com.timeflow.alarm.AlarmPackage;',
+ packageInstance: 'new AlarmPackage()',
+ },
+ ios: null,
+ },
+ },
+};
diff --git a/frontend/modules/timeflow-voice-recorder/android/build.gradle b/frontend/modules/timeflow-voice-recorder/android/build.gradle
new file mode 100644
index 0000000..2d65c43
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/android/build.gradle
@@ -0,0 +1,37 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+def getExtOrDefault(name, defaultValue) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
+}
+
+android {
+ namespace "com.timeflow.voicerecorder"
+
+ compileSdkVersion getExtOrDefault('compileSdkVersion', 35)
+
+ defaultConfig {
+ minSdkVersion getExtOrDefault('minSdkVersion', 24)
+ targetSdkVersion getExtOrDefault('targetSdkVersion', 35)
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src/main/java']
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation 'com.facebook.react:react-android'
+ implementation 'androidx.core:core-ktx:1.13.1'
+}
diff --git a/frontend/modules/timeflow-voice-recorder/android/src/main/AndroidManifest.xml b/frontend/modules/timeflow-voice-recorder/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3ea15e4
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderModule.kt b/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderModule.kt
new file mode 100644
index 0000000..f17ddd9
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderModule.kt
@@ -0,0 +1,214 @@
+package com.timeflow.voicerecorder
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.media.AudioFormat
+import android.media.AudioRecord
+import android.media.MediaRecorder
+import android.util.Base64
+import androidx.core.content.ContextCompat
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import com.facebook.react.modules.core.DeviceEventManagerModule
+import kotlin.concurrent.thread
+import kotlin.math.max
+
+class VoiceRecorderModule(private val reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ private val stateLock = Any()
+
+ @Volatile
+ private var recording = false
+ private var audioRecord: AudioRecord? = null
+ private var recordingThread: Thread? = null
+
+ override fun getName(): String = NAME
+
+ @ReactMethod
+ fun start(promise: Promise) {
+ if (ContextCompat.checkSelfPermission(
+ reactContext,
+ Manifest.permission.RECORD_AUDIO
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ promise.reject("MICROPHONE_PERMISSION_DENIED", "Microphone permission is not granted")
+ return
+ }
+
+ synchronized(stateLock) {
+ if (recording || audioRecord != null) {
+ promise.reject("ALREADY_RECORDING", "Voice recording is already active")
+ return
+ }
+
+ val minimumBufferSize = AudioRecord.getMinBufferSize(
+ SAMPLE_RATE_HZ,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT
+ )
+ if (minimumBufferSize <= 0) {
+ promise.reject("AUDIO_CONFIG_UNSUPPORTED", "16 kHz mono PCM recording is unsupported")
+ return
+ }
+
+ val recorder = try {
+ AudioRecord(
+ MediaRecorder.AudioSource.MIC,
+ SAMPLE_RATE_HZ,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ max(minimumBufferSize, CHUNK_BYTES * 2)
+ )
+ } catch (error: Exception) {
+ promise.reject("RECORDER_INIT_FAILED", error.message, error)
+ return
+ }
+
+ if (recorder.state != AudioRecord.STATE_INITIALIZED) {
+ recorder.release()
+ promise.reject("RECORDER_INIT_FAILED", "AudioRecord initialization failed")
+ return
+ }
+
+ try {
+ recorder.startRecording()
+ } catch (error: Exception) {
+ recorder.release()
+ promise.reject("RECORDER_START_FAILED", error.message, error)
+ return
+ }
+
+ audioRecord = recorder
+ recording = true
+ recordingThread = thread(name = "TimeflowVoiceRecorder") {
+ captureAudio(recorder)
+ }
+ }
+
+ promise.resolve(null)
+ }
+
+ @ReactMethod
+ fun stop(promise: Promise) {
+ stopInternal()
+ promise.resolve(null)
+ }
+
+ @ReactMethod
+ fun cancel(promise: Promise) {
+ stopInternal()
+ promise.resolve(null)
+ }
+
+ @ReactMethod
+ fun addListener(eventName: String) {
+ // Required by NativeEventEmitter. Android event delivery needs no setup.
+ }
+
+ @ReactMethod
+ fun removeListeners(count: Double) {
+ // Required by NativeEventEmitter. Android event delivery needs no teardown.
+ }
+
+ override fun invalidate() {
+ stopInternal()
+ super.invalidate()
+ }
+
+ private fun captureAudio(recorder: AudioRecord) {
+ val buffer = ByteArray(CHUNK_BYTES)
+ try {
+ while (recording && audioRecord === recorder) {
+ val bytesRead = recorder.read(buffer, 0, buffer.size)
+ when {
+ bytesRead > 0 -> {
+ val data = Base64.encodeToString(buffer, 0, bytesRead, Base64.NO_WRAP)
+ emit(AUDIO_CHUNK_EVENT, data)
+ }
+ bytesRead == AudioRecord.ERROR_INVALID_OPERATION ||
+ bytesRead == AudioRecord.ERROR_BAD_VALUE ||
+ bytesRead == AudioRecord.ERROR_DEAD_OBJECT -> {
+ emitError("AudioRecord read failed with code $bytesRead")
+ break
+ }
+ }
+ }
+ } catch (error: Exception) {
+ if (recording) emitError(error.message ?: "Voice recording failed")
+ } finally {
+ finishRecorder(recorder)
+ }
+ }
+
+ private fun stopInternal() {
+ val recorder: AudioRecord?
+ val worker: Thread?
+ synchronized(stateLock) {
+ recording = false
+ recorder = audioRecord
+ worker = recordingThread
+ }
+
+ try {
+ if (recorder?.recordingState == AudioRecord.RECORDSTATE_RECORDING) recorder.stop()
+ } catch (_: Exception) {
+ // The capture thread may already have stopped and released the recorder.
+ }
+
+ if (worker != null && worker !== Thread.currentThread()) {
+ try {
+ worker.join(STOP_TIMEOUT_MS)
+ } catch (_: InterruptedException) {
+ Thread.currentThread().interrupt()
+ }
+ }
+ if (recorder != null) finishRecorder(recorder)
+ }
+
+ private fun finishRecorder(recorder: AudioRecord) {
+ val shouldRelease = synchronized(stateLock) {
+ if (audioRecord !== recorder) {
+ false
+ } else {
+ recording = false
+ audioRecord = null
+ if (recordingThread === Thread.currentThread()) recordingThread = null
+ true
+ }
+ }
+ if (!shouldRelease) return
+
+ try {
+ if (recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) recorder.stop()
+ } catch (_: Exception) {
+ // Recorder is already stopped.
+ }
+ recorder.release()
+ }
+
+ private fun emit(eventName: String, value: Any) {
+ if (!reactContext.hasActiveReactInstance()) return
+ reactContext
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
+ .emit(eventName, value)
+ }
+
+ private fun emitError(message: String) {
+ val payload = Arguments.createMap()
+ payload.putString("message", message)
+ emit(ERROR_EVENT, payload)
+ }
+
+ companion object {
+ const val NAME = "TimeflowVoiceRecorder"
+ const val AUDIO_CHUNK_EVENT = "TimeflowVoiceRecorderChunk"
+ const val ERROR_EVENT = "TimeflowVoiceRecorderError"
+ const val SAMPLE_RATE_HZ = 16_000
+ const val CHUNK_BYTES = 3_200
+ const val STOP_TIMEOUT_MS = 1_500L
+ }
+}
diff --git a/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderPackage.kt b/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderPackage.kt
new file mode 100644
index 0000000..d0c30c1
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/android/src/main/java/com/timeflow/voicerecorder/VoiceRecorderPackage.kt
@@ -0,0 +1,18 @@
+package com.timeflow.voicerecorder
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class VoiceRecorderPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(VoiceRecorderModule(reactContext))
+ }
+
+ override fun createViewManagers(
+ reactContext: ReactApplicationContext
+ ): List> {
+ return emptyList()
+ }
+}
diff --git a/frontend/modules/timeflow-voice-recorder/index.js b/frontend/modules/timeflow-voice-recorder/index.js
new file mode 100644
index 0000000..842ad90
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/index.js
@@ -0,0 +1,3 @@
+// Native module is linked via React Native autolinking (VoiceRecorderPackage).
+// JS callers use NativeModules.TimeflowVoiceRecorder from the app layer.
+module.exports = {};
diff --git a/frontend/modules/timeflow-voice-recorder/package.json b/frontend/modules/timeflow-voice-recorder/package.json
new file mode 100644
index 0000000..df8e0db
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "timeflow-voice-recorder",
+ "version": "1.0.0",
+ "description": "Native Android PCM voice recorder bridge for Timeflow",
+ "main": "index.js",
+ "license": "UNLICENSED",
+ "private": true,
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+}
diff --git a/frontend/modules/timeflow-voice-recorder/react-native.config.js b/frontend/modules/timeflow-voice-recorder/react-native.config.js
new file mode 100644
index 0000000..4e25e04
--- /dev/null
+++ b/frontend/modules/timeflow-voice-recorder/react-native.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ dependency: {
+ platforms: {
+ android: {
+ sourceDir: './android',
+ packageImportPath: 'import com.timeflow.voicerecorder.VoiceRecorderPackage;',
+ packageInstance: 'new VoiceRecorderPackage()',
+ },
+ ios: null,
+ },
+ },
+};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index fb6871f..8038b04 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,7 +10,6 @@
"dependencies": {
"@baidumap/jsapi-loader": "^1.0.0",
"@expo/metro-runtime": "~57.0.7",
- "@react-native-community/datetimepicker": "9.1.0",
"expo": "~57.0.8",
"expo-image-picker": "~57.0.6",
"expo-status-bar": "~57.0.1",
@@ -18,17 +17,25 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.0",
+ "react-native-calendars": "^1.1314.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-svg": "15.15.4",
"react-native-web": "^0.21.2",
- "react-native-webview": "13.16.1"
+ "react-native-webview": "13.16.1",
+ "timeflow-alarm": "file:modules/timeflow-alarm",
+ "timeflow-voice-recorder": "file:modules/timeflow-voice-recorder"
},
"devDependencies": {
"@baidumap/jsapi-v4-types": "^4.0.2",
+ "@react-native/jest-preset": "^0.86.2",
+ "@testing-library/react-native": "^13.2.0",
+ "@types/jest": "29.5.14",
"@types/react": "~19.2.2",
"eslint": "^9.39.5",
"eslint-config-expo": "^57.0.0",
"eslint-config-prettier": "^10.1.8",
+ "jest": "~29.7.0",
+ "jest-expo": "~57.0.2",
"prettier": "^3.9.5",
"typescript": "~6.0.3"
},
@@ -37,6 +44,22 @@
"npm": ">=10.8.2 <11"
}
},
+ "modules/timeflow-alarm": {
+ "version": "1.0.0",
+ "license": "UNLICENSED",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "modules/timeflow-voice-recorder": {
+ "version": "1.0.0",
+ "license": "UNLICENSED",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -445,6 +468,61 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-decorators": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz",
@@ -502,6 +580,48 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
@@ -517,6 +637,19 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
@@ -529,6 +662,45 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-optional-chaining": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
@@ -541,6 +713,38 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
@@ -1095,6 +1299,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1840,1290 +2051,3109 @@
"node": ">=12"
}
},
- "node_modules/@jest/schemas": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
- "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
- "license": "MIT",
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "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,
+ "license": "ISC",
"dependencies": {
- "@sinclair/typebox": "^0.27.8"
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": ">=8"
}
},
- "node_modules/@jest/types": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
- "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jest/schemas": "^29.6.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "sprintf-js": "~1.0.2"
}
},
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
+ "engines": {
+ "node": ">=6"
}
},
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
"engines": {
- "node": ">=6.0.0"
+ "node": ">=8"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
- "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
+ "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
- "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
- "optional": true,
"dependencies": {
- "@tybys/wasm-util": "^0.10.3"
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
},
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
},
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@nolyfill/is-core-module": {
- "version": "1.0.39",
- "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
- "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
+ "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12.4.0"
+ "node": ">=8"
}
},
- "node_modules/@react-native-community/datetimepicker": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-9.1.0.tgz",
- "integrity": "sha512-eadbnk+I2vxvW30iTAsm/qlCnMMAadkifIMYNEB2lzhxN/SvlKc7S2V4k5DyrwjdCbqdcMk3t9K6fnUMcAV34w==",
+ "node_modules/@jest/console": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
+ "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "invariant": "^2.2.4"
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz",
+ "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/reporters": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^29.7.0",
+ "jest-config": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-resolve-dependencies": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
- "expo": ">=52.0.0",
- "react": "*",
- "react-native": "*",
- "react-native-windows": "*"
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
},
"peerDependenciesMeta": {
- "expo": {
- "optional": true
- },
- "react-native-windows": {
+ "node-notifier": {
"optional": true
}
}
},
- "node_modules/@react-native/assets-registry": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.0.tgz",
- "integrity": "sha512-nIaXbm2jX1OTYp0qbviJ3O6KZivoE8z3BnhUQ2LsqfZSWRoOK/n1qsiAr6oALiNKWnXY3j2KPwtYORnZzp8xew==",
+ "node_modules/@jest/core/node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
"license": "MIT",
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": ">=8"
}
},
- "node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
- "integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
+ "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==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.29.0",
- "@react-native/codegen": "0.86.0"
+ "@jest/types": "^29.6.3"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@react-native/codegen": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
- "integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
+ "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,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/parser": "^7.29.0",
- "hermes-parser": "0.36.0",
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1",
- "tinyglobby": "^0.2.15",
- "yargs": "^17.6.2"
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
},
- "peerDependencies": {
- "@babel/core": "*"
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@react-native/codegen/node_modules/hermes-estree": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
- "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT"
+ "node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
},
- "node_modules/@react-native/codegen/node_modules/hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
- "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
+ "node_modules/@jest/fake-timers": {
+ "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,
"license": "MIT",
"dependencies": {
- "hermes-estree": "0.36.0"
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@react-native/community-cli-plugin": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.0.tgz",
- "integrity": "sha512-Jv8p1ebEPfTzs8gmrjsdT2XMXFfeAg45Pman+XPLFGaSeGAZkutRFRyX9Cs9aGTSOyIA9YPJ6vDNb1ayTf1FKQ==",
+ "node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
+ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@react-native/dev-middleware": "0.86.0",
- "debug": "^4.4.0",
- "invariant": "^2.2.4",
- "metro": "^0.84.3",
- "metro-config": "^0.84.3",
- "metro-core": "^0.84.3",
- "semver": "^7.1.3"
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
+ "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
- "@react-native-community/cli": "*",
- "@react-native/metro-config": "0.86.0"
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
},
"peerDependenciesMeta": {
- "@react-native-community/cli": {
- "optional": true
- },
- "@react-native/metro-config": {
+ "node-notifier": {
"optional": true
}
}
},
- "node_modules/@react-native/debugger-frontend": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.0.tgz",
- "integrity": "sha512-7Mb3nDfyJeys+ELF75Ageu7VKERlnIMoO+aNPoXqTXvz+b41L6l2CqMyLpDHxkBSlenij6gEepPNgaIyWHbJZw==",
- "license": "BSD-3-Clause",
+ "node_modules/@jest/reporters/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@react-native/debugger-shell": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.0.tgz",
- "integrity": "sha512-Y0zEkZzLz8ou6o/VLml1A31X/rMgc6DRjwxwzPMa94qRTMY070WeBCNTITQo4kKTBAUgbxh07oXPQqp0Tpja8w==",
- "license": "MIT",
+ "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "cross-spawn": "^7.0.6",
- "debug": "^4.4.0",
- "fb-dotslash": "0.5.8"
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": ">=10"
}
},
- "node_modules/@react-native/dev-middleware": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.0.tgz",
- "integrity": "sha512-20pTO6yTybmvXvro520H6C7jydIQnLKOl5qFtVEcHSdFrY63r3OGei+Rx9bILgSRmH6jgnfEcijcMx7pwWuQtw==",
- "license": "MIT",
+ "node_modules/@jest/reporters/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "@isaacs/ttlcache": "^1.4.1",
- "@react-native/debugger-frontend": "0.86.0",
- "@react-native/debugger-shell": "0.86.0",
- "chrome-launcher": "^0.15.2",
- "chromium-edge-launcher": "^0.3.0",
- "connect": "^3.6.5",
- "debug": "^4.4.0",
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1",
- "open": "^7.0.3",
- "serve-static": "^1.16.2",
- "ws": "^7.5.10"
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "*"
}
},
- "node_modules/@react-native/gradle-plugin": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.0.tgz",
- "integrity": "sha512-a1RcfaEDqWExCGfCwadIxt4l8FvKYgFqeMf2uzeKyAOnb+vTGNIeCvifFL2MqvgaeYxlER437HbMIajGcuJ1pQ==",
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "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",
- "integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==",
+ "node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
+ "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "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",
- "integrity": "sha512-kG0wfCGghUKlfxkJyyHCDVutWVYWK7/DG58ojA/4v9EfulgF+osuSQmlbNb3rcKX58qutm7JcldSeVLgGFha9g==",
- "license": "MIT"
- },
- "node_modules/@react-native/virtualized-lists": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.0.tgz",
- "integrity": "sha512-4/ZLXdf/OSpPDVO0AsQ1SJdRIzt5t9BNQ46QwGgxvX7/cirYR5k8KXctNGGgW8lQo2gZChEfY2zFCZg9nM/jiw==",
+ "node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
+ "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1"
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
},
"engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
- },
- "peerDependencies": {
- "@types/react": "^19.2.0",
- "react": "*",
- "react-native": "0.86.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@rtsao/scc": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
- "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@sinclair/typebox": {
- "version": "0.27.12",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
- "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==",
- "license": "MIT"
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "node_modules/@jest/test-sequencer": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
+ "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==",
"dev": true,
"license": "MIT",
- "optional": true,
"dependencies": {
- "tslib": "^2.4.0"
+ "@jest/test-result": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
+ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/@types/istanbul-lib-coverage": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
- "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
- "license": "MIT"
- },
- "node_modules/@types/istanbul-lib-report": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
- "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"license": "MIT",
"dependencies": {
- "@types/istanbul-lib-coverage": "*"
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@types/istanbul-reports": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
- "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "node_modules/@jest/types": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"license": "MIT",
"dependencies": {
- "@types/istanbul-lib-report": "*"
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "26.1.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
- "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
- "undici-types": "~8.3.0"
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@types/react": {
- "version": "19.2.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
- "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "devOptional": true,
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
- "csstype": "^3.2.2"
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@types/yargs": {
- "version": "17.0.35",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
- "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"dependencies": {
- "@types/yargs-parser": "*"
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
}
},
- "node_modules/@types/yargs-parser": {
- "version": "21.0.3",
- "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
- "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
- "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.64.0",
- "@typescript-eslint/type-utils": "8.64.0",
- "@typescript-eslint/utils": "8.64.0",
- "@typescript-eslint/visitor-keys": "8.64.0",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "@tybys/wasm-util": "^0.10.3"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.64.0",
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "node_modules/@nolyfill/is-core-module": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+ "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 4"
+ "node": ">=12.4.0"
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
- "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
- "dev": true,
+ "node_modules/@react-native/assets-registry": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.0.tgz",
+ "integrity": "sha512-nIaXbm2jX1OTYp0qbviJ3O6KZivoE8z3BnhUQ2LsqfZSWRoOK/n1qsiAr6oALiNKWnXY3j2KPwtYORnZzp8xew==",
"license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.64.0",
- "@typescript-eslint/types": "8.64.0",
- "@typescript-eslint/typescript-estree": "8.64.0",
- "@typescript-eslint/visitor-keys": "8.64.0",
- "debug": "^4.4.3"
- },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@typescript-eslint/project-service": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
- "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
- "dev": true,
+ "node_modules/@react-native/babel-plugin-codegen": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
+ "integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.64.0",
- "@typescript-eslint/types": "^8.64.0",
- "debug": "^4.4.3"
+ "@babel/traverse": "^7.29.0",
+ "@react-native/codegen": "0.86.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
- "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
- "dev": true,
+ "node_modules/@react-native/codegen": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
+ "integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.64.0",
- "@typescript-eslint/visitor-keys": "8.64.0"
+ "@babel/core": "^7.25.2",
+ "@babel/parser": "^7.29.0",
+ "hermes-parser": "0.36.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "tinyglobby": "^0.2.15",
+ "yargs": "^17.6.2"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "peerDependencies": {
+ "@babel/core": "*"
}
},
- "node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
- "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
- "dev": true,
+ "node_modules/@react-native/codegen/node_modules/hermes-estree": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
+ "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
+ "license": "MIT"
+ },
+ "node_modules/@react-native/codegen/node_modules/hermes-parser": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
+ "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
"license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "dependencies": {
+ "hermes-estree": "0.36.0"
+ }
+ },
+ "node_modules/@react-native/community-cli-plugin": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.0.tgz",
+ "integrity": "sha512-Jv8p1ebEPfTzs8gmrjsdT2XMXFfeAg45Pman+XPLFGaSeGAZkutRFRyX9Cs9aGTSOyIA9YPJ6vDNb1ayTf1FKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-native/dev-middleware": "0.86.0",
+ "debug": "^4.4.0",
+ "invariant": "^2.2.4",
+ "metro": "^0.84.3",
+ "metro-config": "^0.84.3",
+ "metro-core": "^0.84.3",
+ "semver": "^7.1.3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "@react-native-community/cli": "*",
+ "@react-native/metro-config": "0.86.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-community/cli": {
+ "optional": true
+ },
+ "@react-native/metro-config": {
+ "optional": true
+ }
}
},
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
- "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
- "dev": true,
+ "node_modules/@react-native/debugger-frontend": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.0.tgz",
+ "integrity": "sha512-7Mb3nDfyJeys+ELF75Ageu7VKERlnIMoO+aNPoXqTXvz+b41L6l2CqMyLpDHxkBSlenij6gEepPNgaIyWHbJZw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@react-native/debugger-shell": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.0.tgz",
+ "integrity": "sha512-Y0zEkZzLz8ou6o/VLml1A31X/rMgc6DRjwxwzPMa94qRTMY070WeBCNTITQo4kKTBAUgbxh07oXPQqp0Tpja8w==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.64.0",
- "@typescript-eslint/typescript-estree": "8.64.0",
- "@typescript-eslint/utils": "8.64.0",
- "debug": "^4.4.3",
- "ts-api-utils": "^2.5.0"
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.4.0",
+ "fb-dotslash": "0.5.8"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/@react-native/dev-middleware": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.0.tgz",
+ "integrity": "sha512-20pTO6yTybmvXvro520H6C7jydIQnLKOl5qFtVEcHSdFrY63r3OGei+Rx9bILgSRmH6jgnfEcijcMx7pwWuQtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/ttlcache": "^1.4.1",
+ "@react-native/debugger-frontend": "0.86.0",
+ "@react-native/debugger-shell": "0.86.0",
+ "chrome-launcher": "^0.15.2",
+ "chromium-edge-launcher": "^0.3.0",
+ "connect": "^3.6.5",
+ "debug": "^4.4.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "open": "^7.0.3",
+ "serve-static": "^1.16.2",
+ "ws": "^7.5.10"
},
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@typescript-eslint/types": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
- "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
- "dev": true,
+ "node_modules/@react-native/gradle-plugin": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.0.tgz",
+ "integrity": "sha512-a1RcfaEDqWExCGfCwadIxt4l8FvKYgFqeMf2uzeKyAOnb+vTGNIeCvifFL2MqvgaeYxlER437HbMIajGcuJ1pQ==",
"license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
- "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
+ "node_modules/@react-native/jest-preset": {
+ "version": "0.86.2",
+ "resolved": "https://registry.npmjs.org/@react-native/jest-preset/-/jest-preset-0.86.2.tgz",
+ "integrity": "sha512-wneoqwKWdv6wIcWotVgp6NMlMWcJDjxDnp7fVYym2Pn6JLgAKgkbmuHGmxW0cLCGoa9wtcO680uciv+Kzx0G7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.64.0",
- "@typescript-eslint/tsconfig-utils": "8.64.0",
- "@typescript-eslint/types": "8.64.0",
- "@typescript-eslint/visitor-keys": "8.64.0",
- "debug": "^4.4.3",
- "minimatch": "^10.2.2",
- "semver": "^7.7.3",
- "tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.5.0"
+ "@jest/create-cache-key-function": "^29.7.0",
+ "@react-native/js-polyfills": "0.86.2",
+ "babel-jest": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "regenerator-runtime": "^0.13.2"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": ">= 20.19.4"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "react": "^19.2.3"
}
},
- "node_modules/@typescript-eslint/utils": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
- "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
+ "node_modules/@react-native/jest-preset/node_modules/@react-native/js-polyfills": {
+ "version": "0.86.2",
+ "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.2.tgz",
+ "integrity": "sha512-bIwNcGBaQ74shB5z1mRkxOpjikimuwsnOCEkZSzL67Z1FTyK1ObpENfyd2QvcvVW9Cjl+tHuw9ynpBnb2jPoJQ==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "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",
+ "integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "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",
+ "integrity": "sha512-kG0wfCGghUKlfxkJyyHCDVutWVYWK7/DG58ojA/4v9EfulgF+osuSQmlbNb3rcKX58qutm7JcldSeVLgGFha9g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-native/virtualized-lists": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.0.tgz",
+ "integrity": "sha512-4/ZLXdf/OSpPDVO0AsQ1SJdRIzt5t9BNQ46QwGgxvX7/cirYR5k8KXctNGGgW8lQo2gZChEfY2zFCZg9nM/jiw==",
+ "license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.64.0",
- "@typescript-eslint/types": "8.64.0",
- "@typescript-eslint/typescript-estree": "8.64.0"
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "@types/react": "^19.2.0",
+ "react": "*",
+ "react-native": "0.86.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.64.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
- "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.12",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
+ "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==",
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "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,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@testing-library/react-native": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.2.0.tgz",
+ "integrity": "sha512-3FX+vW/JScXkoH8VSCRTYF4KCHC56y4AI6TMDISfLna6r+z8kaSEmxH1I6NVaFOxoWX9yaHDyI26xh7BykmqKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.64.0",
- "eslint-visitor-keys": "^5.0.0"
+ "chalk": "^4.1.2",
+ "jest-matcher-utils": "^29.7.0",
+ "pretty-format": "^29.7.0",
+ "redent": "^3.0.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">=18"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "peerDependencies": {
+ "jest": ">=29.0.0",
+ "react": ">=18.2.0",
+ "react-native": ">=0.71",
+ "react-test-renderer": ">=18.2.0"
+ },
+ "peerDependenciesMeta": {
+ "jest": {
+ "optional": true
+ }
}
},
- "node_modules/@ungap/structured-clone": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
- "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
- "license": "ISC"
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
+ "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
},
- "node_modules/@unrs/resolver-binding-android-arm-eabi": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz",
- "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@unrs/resolver-binding-android-arm64": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz",
- "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
},
- "node_modules/@unrs/resolver-binding-darwin-arm64": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz",
- "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
},
- "node_modules/@unrs/resolver-binding-darwin-x64": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz",
- "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@types/babel__generator": {
+ "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,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
},
- "node_modules/@unrs/resolver-binding-freebsd-x64": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz",
- "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@types/babel__template": {
+ "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,
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz",
- "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@types/babel__traverse": {
+ "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,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz",
- "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "license": "MIT"
},
- "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz",
- "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@types/graceful-fs": {
+ "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,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@types/node": "*"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz",
- "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-loong64-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz",
- "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-loong64-musl": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz",
- "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==",
- "cpu": [
- "loong64"
- ],
+ "node_modules/@types/jest": {
+ "version": "29.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
+ "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "expect": "^29.0.0",
+ "pretty-format": "^29.0.0"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz",
- "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==",
- "cpu": [
- "ppc64"
- ],
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz",
- "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==",
- "cpu": [
- "riscv64"
- ],
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "license": "MIT"
},
- "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz",
- "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==",
- "cpu": [
- "riscv64"
- ],
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz",
- "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==",
- "cpu": [
- "s390x"
- ],
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
},
- "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz",
- "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "license": "MIT"
},
- "node_modules/@unrs/resolver-binding-linux-x64-musl": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz",
- "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "license": "MIT"
},
- "node_modules/@unrs/resolver-binding-openharmony-arm64": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz",
- "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@types/yargs": {
+ "version": "17.0.35",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
+ "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
"license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
},
- "node_modules/@unrs/resolver-binding-wasm32-wasi": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz",
- "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==",
- "cpu": [
- "wasm32"
- ],
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
+ "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"dev": true,
"license": "MIT",
- "optional": true,
"dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/type-utils": "8.64.0",
+ "@typescript-eslint/utils": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
- "node": ">=14.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.64.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
- "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
- "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz",
- "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@xmldom/xmldom": {
- "version": "0.8.13",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
- "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
- "license": "MIT",
"engines": {
- "node": ">=10.0.0"
+ "node": ">= 4"
}
},
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
+ "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "event-target-shim": "^5.0.0"
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "debug": "^4.4.3"
},
"engines": {
- "node": ">=6.5"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
+ "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
+ "@typescript-eslint/tsconfig-utils": "^8.64.0",
+ "@typescript-eslint/types": "^8.64.0",
+ "debug": "^4.4.3"
},
"engines": {
- "node": ">= 0.6"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/acorn": {
- "version": "8.17.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
- "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
+ "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
+ "dependencies": {
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0"
},
"engines": {
- "node": ">=0.4.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
+ "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "license": "MIT",
"engines": {
- "node": ">= 14"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/agent-cli-detector": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.4.tgz",
- "integrity": "sha512-qPgevFvpaQoBaRJVKzr8R7h1WPvV3DtbgRIQlne4le66KBzXx5hNBwo/+NTw67LgkKBlhCzksrdautpUdlls0Q==",
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
+ "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "agent-cli-detector": "dist/cli.js"
+ "dependencies": {
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0",
+ "@typescript-eslint/utils": "8.64.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
- "node": ">=18.18"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/ajv": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
- "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
+ "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/anser": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
- "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==",
- "license": "MIT"
- },
- "node_modules/ansi-escapes": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
- "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
+ "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "type-fest": "^0.21.3"
- },
- "engines": {
- "node": ">=8"
+ "@typescript-eslint/project-service": "8.64.0",
+ "@typescript-eslint/tsconfig-utils": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-escapes/node_modules/type-fest": {
- "version": "0.21.3",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
- "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
- "license": "(MIT OR CC0-1.0)",
"engines": {
- "node": ">=10"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "license": "MIT"
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "license": "Python-2.0"
- },
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
- "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
+ "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "is-array-buffer": "^3.0.5"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/array-includes": {
- "version": "3.1.9",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
- "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
+ "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.24.0",
- "es-object-atoms": "^1.1.1",
- "get-intrinsic": "^1.3.0",
- "is-string": "^1.1.1",
- "math-intrinsics": "^1.1.0"
+ "@typescript-eslint/types": "8.64.0",
+ "eslint-visitor-keys": "^5.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
+ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "license": "ISC"
+ },
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz",
+ "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/array.prototype.findlastindex": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
- "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz",
+ "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.9",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "es-shim-unscopables": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/array.prototype.flat": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
- "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz",
+ "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
- "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz",
+ "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
- "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz",
+ "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.3",
- "es-errors": "^1.3.0",
- "es-shim-unscopables": "^1.0.2"
- },
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz",
+ "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz",
+ "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz",
+ "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz",
+ "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-loong64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz",
+ "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-loong64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz",
+ "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz",
+ "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz",
+ "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz",
+ "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz",
+ "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz",
+ "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz",
+ "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-openharmony-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz",
+ "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz",
+ "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
+ "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
+ "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz",
+ "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "deprecated": "Use your platform's native atob() and btoa() methods instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/agent-cli-detector": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.4.tgz",
+ "integrity": "sha512-qPgevFvpaQoBaRJVKzr8R7h1WPvV3DtbgRIQlne4le66KBzXx5hNBwo/+NTw67LgkKBlhCzksrdautpUdlls0Q==",
+ "license": "MIT",
+ "bin": {
+ "agent-cli-detector": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/anser": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
+ "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "license": "MIT"
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/babel-jest": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "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,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.5",
+ "core-js-compat": "^3.43.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-react-compiler": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
+ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ }
+ },
+ "node_modules/babel-plugin-react-native-web": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz",
+ "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==",
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-syntax-hermes-parser": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
+ "integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-parser": "0.36.0"
+ }
+ },
+ "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
+ "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
+ "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.36.0"
+ }
+ },
+ "node_modules/babel-plugin-transform-flow-enums": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz",
+ "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-flow": "^7.12.1"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
+ "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/babel-preset-expo": {
+ "version": "57.0.4",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.4.tgz",
+ "integrity": "sha512-EkFcNoE23HVzQT6ZNXs/adN8+G7rqEAF6tQn6LpRPYa1YY7wmX5GxhJF0kaYMNtBndNrMTN4+0rYE17VG13KFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.20.5",
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/plugin-proposal-decorators": "^7.12.9",
+ "@babel/plugin-proposal-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-transform-async-generator-functions": "^7.25.4",
+ "@babel/plugin-transform-async-to-generator": "^7.24.7",
+ "@babel/plugin-transform-block-scoping": "^7.25.0",
+ "@babel/plugin-transform-class-properties": "^7.25.4",
+ "@babel/plugin-transform-class-static-block": "^7.27.1",
+ "@babel/plugin-transform-classes": "^7.25.4",
+ "@babel/plugin-transform-destructuring": "^7.24.8",
+ "@babel/plugin-transform-export-namespace-from": "^7.25.9",
+ "@babel/plugin-transform-flow-strip-types": "^7.25.2",
+ "@babel/plugin-transform-for-of": "^7.24.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.24.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
+ "@babel/plugin-transform-parameters": "^7.24.7",
+ "@babel/plugin-transform-private-methods": "^7.24.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
+ "@babel/plugin-transform-react-display-name": "^7.24.7",
+ "@babel/plugin-transform-react-jsx": "^7.28.6",
+ "@babel/plugin-transform-react-jsx-development": "^7.27.1",
+ "@babel/plugin-transform-react-pure-annotations": "^7.27.1",
+ "@babel/plugin-transform-runtime": "^7.24.7",
+ "@babel/plugin-transform-typescript": "^7.25.2",
+ "@babel/plugin-transform-unicode-regex": "^7.24.7",
+ "@babel/preset-typescript": "^7.23.0",
+ "@react-native/babel-plugin-codegen": "0.86.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "babel-plugin-react-native-web": "~0.21.0",
+ "babel-plugin-syntax-hermes-parser": "^0.36.0",
+ "babel-plugin-transform-flow-enums": "^0.0.2",
+ "debug": "^4.3.4"
+ },
+ "peerDependencies": {
+ "@babel/runtime": "^7.20.0",
+ "expo": "*",
+ "expo-widgets": "^57.0.6",
+ "react-refresh": ">=0.14.0 <1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/runtime": {
+ "optional": true
+ },
+ "expo": {
+ "optional": true
+ },
+ "expo-widgets": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.43",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
+ "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "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/bplist-creator": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
+ "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==",
+ "license": "MIT",
+ "dependencies": {
+ "stream-buffers": "2.2.x"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz",
+ "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==",
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "1.6.x"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.6",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
+ "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.42",
+ "caniuse-lite": "^1.0.30001803",
+ "electron-to-chromium": "^1.5.389",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chrome-launcher": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
+ "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "*",
+ "escape-string-regexp": "^4.0.0",
+ "is-wsl": "^2.2.0",
+ "lighthouse-logger": "^1.0.0"
+ },
+ "bin": {
+ "print-chrome-path": "bin/print-chrome-path.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/chromium-edge-launcher": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.3.0.tgz",
+ "integrity": "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "*",
+ "escape-string-regexp": "^4.0.0",
+ "is-wsl": "^2.2.0",
+ "lighthouse-logger": "^1.0.0",
+ "mkdirp": "^1.0.4"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "license": "MIT"
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
+ "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/connect": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
+ "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.2",
+ "parseurl": "~1.3.3",
+ "utils-merge": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/connect/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/connect/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
+ "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/create-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
+ "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "prompts": "^2.0.1"
+ },
+ "bin": {
+ "create-jest": "bin/create-jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cross-fetch": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
+ "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.7.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-in-js-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
+ "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
+ "license": "MIT",
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.3"
+ }
+ },
+ "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-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.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/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dedent": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
+ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
- "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
+ "es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "is-array-buffer": "^3.0.4"
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -3132,30 +5162,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
- "license": "MIT"
- },
- "node_modules/async-function": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
- "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/available-typed-arrays": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "possible-typed-array-names": "^1.0.0"
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
@@ -3164,369 +5180,438 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/babel-plugin-polyfill-corejs2": {
- "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==",
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.28.6",
- "@babel/helper-define-polyfill-provider": "^0.6.8",
- "semver": "^6.3.1"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ "engines": {
+ "node": ">=0.4.0"
}
},
- "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
}
},
- "node_modules/babel-plugin-polyfill-corejs3": {
- "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==",
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.5",
- "core-js-compat": "^3.43.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
}
},
- "node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.6.8",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
- "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.6.8"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/babel-plugin-react-compiler": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
- "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/types": "^7.26.0"
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/babel-plugin-react-native-web": {
- "version": "0.21.2",
- "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz",
- "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==",
+ "node_modules/dnssd-advertise": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz",
+ "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==",
"license": "MIT"
},
- "node_modules/babel-plugin-syntax-hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
- "integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
- "license": "MIT",
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "hermes-parser": "0.36.0"
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
- "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT"
- },
- "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
- "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
+ "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": {
- "hermes-estree": "0.36.0"
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
- "node_modules/babel-plugin-transform-flow-enums": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz",
- "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==",
+ "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/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/plugin-syntax-flow": "^7.12.1"
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/babel-preset-expo": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.4.tgz",
- "integrity": "sha512-EkFcNoE23HVzQT6ZNXs/adN8+G7rqEAF6tQn6LpRPYa1YY7wmX5GxhJF0kaYMNtBndNrMTN4+0rYE17VG13KFg==",
- "license": "MIT",
+ "node_modules/domexception/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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": {
- "@babel/generator": "^7.20.5",
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/plugin-proposal-decorators": "^7.12.9",
- "@babel/plugin-proposal-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-transform-async-generator-functions": "^7.25.4",
- "@babel/plugin-transform-async-to-generator": "^7.24.7",
- "@babel/plugin-transform-block-scoping": "^7.25.0",
- "@babel/plugin-transform-class-properties": "^7.25.4",
- "@babel/plugin-transform-class-static-block": "^7.27.1",
- "@babel/plugin-transform-classes": "^7.25.4",
- "@babel/plugin-transform-destructuring": "^7.24.8",
- "@babel/plugin-transform-export-namespace-from": "^7.25.9",
- "@babel/plugin-transform-flow-strip-types": "^7.25.2",
- "@babel/plugin-transform-for-of": "^7.24.7",
- "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
- "@babel/plugin-transform-modules-commonjs": "^7.24.8",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
- "@babel/plugin-transform-object-rest-spread": "^7.24.7",
- "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
- "@babel/plugin-transform-optional-chaining": "^7.24.8",
- "@babel/plugin-transform-parameters": "^7.24.7",
- "@babel/plugin-transform-private-methods": "^7.24.7",
- "@babel/plugin-transform-private-property-in-object": "^7.24.7",
- "@babel/plugin-transform-react-display-name": "^7.24.7",
- "@babel/plugin-transform-react-jsx": "^7.28.6",
- "@babel/plugin-transform-react-jsx-development": "^7.27.1",
- "@babel/plugin-transform-react-pure-annotations": "^7.27.1",
- "@babel/plugin-transform-runtime": "^7.24.7",
- "@babel/plugin-transform-typescript": "^7.25.2",
- "@babel/plugin-transform-unicode-regex": "^7.24.7",
- "@babel/preset-typescript": "^7.23.0",
- "@react-native/babel-plugin-codegen": "0.86.0",
- "babel-plugin-react-compiler": "^1.0.0",
- "babel-plugin-react-native-web": "~0.21.0",
- "babel-plugin-syntax-hermes-parser": "^0.36.0",
- "babel-plugin-transform-flow-enums": "^0.0.2",
- "debug": "^4.3.4"
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
},
- "peerDependencies": {
- "@babel/runtime": "^7.20.0",
- "expo": "*",
- "expo-widgets": "^57.0.6",
- "react-refresh": ">=0.14.0 <1.0.0"
+ "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"
},
- "peerDependenciesMeta": {
- "@babel/runtime": {
- "optional": true
- },
- "expo": {
- "optional": true
- },
- "expo-widgets": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
}
},
- "node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
"engines": {
- "node": "18 || 20 || >=22"
+ "node": ">= 0.4"
}
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
- "node_modules/baseline-browser-mapping": {
- "version": "2.10.43",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
- "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.392",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz",
+ "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==",
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
+ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=6.0.0"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
}
},
- "node_modules/big-integer": {
- "version": "1.6.52",
- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
- "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
- "license": "Unlicense",
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
"engines": {
- "node": ">=0.6"
+ "node": ">= 0.8"
}
},
- "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/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/bplist-creator": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
- "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==",
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "stream-buffers": "2.2.x"
+ "is-arrayish": "^0.2.1"
}
},
- "node_modules/bplist-parser": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz",
- "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==",
+ "node_modules/error-stack-parser": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
+ "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"license": "MIT",
"dependencies": {
- "big-integer": "1.6.x"
- },
- "engines": {
- "node": ">= 5.10.0"
+ "stackframe": "^1.3.4"
}
},
- "node_modules/brace-expansion": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
- "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^4.0.2"
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
},
"engines": {
- "node": "20 || >=22"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "node_modules/es-abstract-get": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
+ "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "fill-range": "^7.1.1"
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.2",
+ "is-callable": "^1.2.7",
+ "object-inspect": "^1.13.4"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/browserslist": {
- "version": "4.28.6",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
- "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
- "dependencies": {
- "baseline-browser-mapping": "^2.10.42",
- "caniuse-lite": "^1.0.30001803",
- "electron-to-chromium": "^1.5.389",
- "node-releases": "^2.0.51",
- "update-browserslist-db": "^1.2.3"
- },
- "bin": {
- "browserslist": "cli.js"
- },
"engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ "node": ">= 0.4"
}
},
- "node_modules/bser": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
- "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
- "license": "Apache-2.0",
+ "node_modules/es-iterator-helpers": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz",
+ "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "node-int64": "^0.4.0"
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/buffer-from": {
+ "node_modules/es-object-atoms": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "license": "MIT"
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.4"
}
},
- "node_modules/call-bind": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
- "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "get-intrinsic": "^1.3.0",
- "set-function-length": "^1.2.2"
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "node_modules/es-to-primitive": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
+ "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
+ "es-abstract-get": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.1.0",
+ "is-symbol": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
@@ -3535,20 +5620,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
- "node_modules/camelcase": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3557,1259 +5647,1480 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/caniuse-lite": {
- "version": "1.0.30001806",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
- "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
+ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.6",
+ "@eslint/js": "9.39.5",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
},
"engines": {
- "node": ">=10"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-expo": {
+ "version": "57.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.0.tgz",
+ "integrity": "sha512-T7OTN9xrSZYjLw4qTkL1Mn2WfAUVmMGY38+OYAcraI1uiTFVH6jfkSkv84WLTqnneblLcb6AsMDV+SHcUj3hGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
+ "@typescript-eslint/parser": "^8.59.0",
+ "eslint-import-resolver-typescript": "^3.6.3",
+ "eslint-plugin-expo": "^1.1.0",
+ "eslint-plugin-import": "^2.30.0",
+ "eslint-plugin-react": "^7.37.3",
+ "eslint-plugin-react-hooks": "^7.0.0",
+ "globals": "^16.0.0"
},
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "peerDependencies": {
+ "eslint": ">=8.10"
}
},
- "node_modules/chrome-launcher": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
- "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/node": "*",
- "escape-string-regexp": "^4.0.0",
- "is-wsl": "^2.2.0",
- "lighthouse-logger": "^1.0.0"
- },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
"bin": {
- "print-chrome-path": "bin/print-chrome-path.js"
+ "eslint-config-prettier": "bin/cli.js"
},
- "engines": {
- "node": ">=12.13.0"
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
}
},
- "node_modules/chromium-edge-launcher": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.3.0.tgz",
- "integrity": "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==",
- "license": "Apache-2.0",
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.10",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz",
+ "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/node": "*",
- "escape-string-regexp": "^4.0.0",
- "is-wsl": "^2.2.0",
- "lighthouse-logger": "^1.0.0",
- "mkdirp": "^1.0.4"
+ "debug": "^3.2.7",
+ "is-core-module": "^2.16.1",
+ "resolve": "^2.0.0-next.6"
}
},
- "node_modules/ci-info": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
- "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
- "license": "MIT"
- },
- "node_modules/cli-cursor": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
- "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "restore-cursor": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
+ "ms": "^2.1.1"
}
},
- "node_modules/cli-spinners": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
- "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "node_modules/eslint-import-resolver-node/node_modules/resolve": {
+ "version": "2.0.0-next.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
+ "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.2",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
"engines": {
- "node": ">=6"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
+ "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
+ "dev": true,
"license": "ISC",
"dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
+ "@nolyfill/is-core-module": "1.0.39",
+ "debug": "^4.4.0",
+ "get-tsconfig": "^4.10.0",
+ "is-bun-module": "^2.0.0",
+ "stable-hash": "^0.0.5",
+ "tinyglobby": "^0.2.13",
+ "unrs-resolver": "^1.6.2"
},
"engines": {
- "node": ">=12"
- }
- },
- "node_modules/clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-resolver-typescript"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
}
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "node_modules/eslint-module-utils": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz",
+ "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "color-name": "~1.1.4"
+ "debug": "^3.2.7"
},
"engines": {
- "node": ">=7.0.0"
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
}
},
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
- "node_modules/commander": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
- "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 10"
+ "dependencies": {
+ "ms": "^2.1.1"
}
},
- "node_modules/compressible": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
- "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "node_modules/eslint-plugin-expo": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.1.0.tgz",
+ "integrity": "sha512-vPP0EPx7IA7ZfP49dY4rq9RV5jqkFWG+Pih3/oGjzIRjMI+ogcOE8i6isYkLXAdw/yvFV2BRZkTaQaiOGQqn6Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "mime-db": ">= 1.43.0 < 2"
+ "@typescript-eslint/types": "^8.59.0",
+ "@typescript-eslint/utils": "^8.59.0",
+ "eslint": "^9.24.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.10"
}
},
- "node_modules/compression": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
- "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "compressible": "~2.0.18",
- "debug": "2.6.9",
- "negotiator": "~0.6.4",
- "on-headers": "~1.1.0",
- "safe-buffer": "5.2.1",
- "vary": "~1.1.2"
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
},
"engines": {
- "node": ">= 0.8.0"
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
}
},
- "node_modules/compression/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
}
},
- "node_modules/compression/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/compression/node_modules/negotiator": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
- "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
}
},
- "node_modules/connect": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
- "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "debug": "2.6.9",
- "finalhandler": "1.1.2",
- "parseurl": "~1.3.3",
- "utils-merge": "1.0.1"
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
},
"engines": {
- "node": ">= 0.10.0"
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
- "node_modules/connect/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ms": "2.0.0"
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
}
},
- "node_modules/connect/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/core-js-compat": {
- "version": "3.49.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
- "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "node_modules/eslint-plugin-react-hooks/node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
+ "hermes-estree": "0.25.1"
}
},
- "node_modules/cross-fetch": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
- "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
- "license": "MIT",
+ "node_modules/eslint-plugin-react/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "node-fetch": "^2.7.0"
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
}
},
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
+ "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.2",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
},
"engines": {
- "node": ">= 8"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/css-in-js-utils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
- "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
- "license": "MIT",
- "dependencies": {
- "hyphenate-style-name": "^1.0.3"
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
}
},
- "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==",
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
"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"
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/css-tree/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "license": "BSD-3-Clause",
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
"engines": {
- "node": ">=0.10.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "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",
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 6"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/fb55"
+ "url": "https://opencollective.com/eslint"
}
},
- "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/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
},
- "node_modules/data-view-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
- "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/data-view-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
- "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.2"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 0.4"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://github.com/sponsors/inspect-js"
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/data-view-byte-offset": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
- "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "node_modules/esprima": {
+ "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,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=4"
}
},
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "ms": "^2.1.3"
+ "estraverse": "^5.1.0"
},
"engines": {
- "node": ">=6.0"
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
},
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "engines": {
+ "node": ">=4.0"
}
},
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
- "license": "MIT"
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
},
- "node_modules/deepmerge": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
- "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "license": "MIT",
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/defaults": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
- "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
- "dependencies": {
- "clone": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">= 0.6"
}
},
- "node_modules/define-data-property": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
- "node_modules/define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "node_modules/execa/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/execa/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
+ "mimic-fn": "^2.1.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=6"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.8.0"
}
},
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "node_modules/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
"engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
+ "node_modules/expo": {
+ "version": "57.0.8",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.8.tgz",
+ "integrity": "sha512-0IxxoPZbT54IH4fHL5NihkvED9HBVQx3uNdPvyv8pFUHWJ81RdFjL0aJys1IB7hbo09KTz4xW4I2aWVOXKAcJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.0",
+ "@expo/cli": "^57.0.10",
+ "@expo/config": "~57.0.6",
+ "@expo/config-plugins": "~57.0.6",
+ "@expo/devtools": "~57.0.1",
+ "@expo/dom-webview": "~57.0.1",
+ "@expo/fingerprint": "^0.20.6",
+ "@expo/local-build-cache-provider": "^57.0.4",
+ "@expo/log-box": "^57.0.1",
+ "@expo/metro": "~56.0.0",
+ "@expo/metro-config": "~57.0.7",
+ "@ungap/structured-clone": "^1.3.0",
+ "babel-preset-expo": "~57.0.4",
+ "expo-asset": "~57.0.7",
+ "expo-constants": "~57.0.7",
+ "expo-file-system": "~57.0.1",
+ "expo-font": "~57.0.1",
+ "expo-keep-awake": "~57.0.1",
+ "expo-modules-autolinking": "~57.0.9",
+ "expo-modules-core": "~57.0.7",
+ "pretty-format": "^29.7.0",
+ "react-refresh": "^0.14.2",
+ "whatwg-url-minimum": "^0.1.2"
+ },
+ "bin": {
+ "expo": "bin/cli",
+ "expo-modules-autolinking": "bin/autolinking",
+ "fingerprint": "bin/fingerprint"
+ },
+ "peerDependencies": {
+ "@expo/dom-webview": "*",
+ "@expo/metro-runtime": "*",
+ "react": "*",
+ "react-dom": "*",
+ "react-native": "*",
+ "react-native-web": "*",
+ "react-native-webview": "*"
+ },
+ "peerDependenciesMeta": {
+ "@expo/dom-webview": {
+ "optional": true
+ },
+ "@expo/metro-runtime": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native-web": {
+ "optional": true
+ },
+ "react-native-webview": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-asset": {
+ "version": "57.0.7",
+ "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.7.tgz",
+ "integrity": "sha512-TA6MRQq1HL0N6KGvNMzL6djdH5tGJ/eHPIhML+6bVu52mnXldmup2swdvP37zDnvoMUUXRVGsvwVHFmvhCbW6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.11.4",
+ "expo-constants": "~57.0.7"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
}
},
- "node_modules/dnssd-advertise": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz",
- "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==",
- "license": "MIT"
- },
- "node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "license": "Apache-2.0",
+ "node_modules/expo-constants": {
+ "version": "57.0.7",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.7.tgz",
+ "integrity": "sha512-ShDwaKnh3UieCQ/dG0kO8PuicTTatn3WDGmXbq/fukyzPXWhdUxf37DINVDQS6D3DDG7nfqItij+QvnDHSQhTg==",
+ "license": "MIT",
"dependencies": {
- "esutils": "^2.0.2"
+ "@expo/env": "~2.4.2"
},
- "engines": {
- "node": ">=0.10.0"
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
}
},
- "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==",
+ "node_modules/expo-font": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
+ "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
"license": "MIT",
"dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
+ "fontfaceobserver": "^2.1.0"
},
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
}
},
- "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/expo-image-loader": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.1.tgz",
+ "integrity": "sha512-uhrZKLT/cTl2mXyR28kPpVkS5O+PK9N1QA/07IFM4f5T4g0lTW1JHT3NEWwEEsGFldPmVX4j7LwUVVZxE+woug==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
},
- "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",
+ "node_modules/expo-image-picker": {
+ "version": "57.0.6",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.6.tgz",
+ "integrity": "sha512-6Of7SzyFVC+WFuFxhD4+nRTQ9joqldPhbiVWRsumg4ybttKmY8GxrnXAUSIDfa9DSk9PhgIAy2C1c5y1+CnU3g==",
+ "license": "MIT",
"dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
+ "expo-image-loader": "~57.0.1"
},
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
+ "peerDependencies": {
+ "expo": "*"
}
},
- "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",
+ "node_modules/expo-modules-autolinking": {
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.9.tgz",
+ "integrity": "sha512-lj2nsAKMMRLXSFnGgaaQrWJ2fdSpLPc/bca6Rkiw4g8zYPn7qX4MRUJgavvzm/hBrzvMnlhXVgJtidOGuwBh+w==",
+ "license": "MIT",
"dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
+ "@expo/require-utils": "^57.0.4",
+ "@expo/spawn-async": "^1.8.0",
+ "chalk": "^4.1.0",
+ "commander": "^7.2.0"
},
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
+ "bin": {
+ "expo-modules-autolinking": "bin/expo-modules-autolinking.js"
}
},
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
+ "node_modules/expo-modules-core": {
+ "version": "57.0.7",
+ "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.7.tgz",
+ "integrity": "sha512-5HrbCfgYmLs0a5dzfM4GmRGelVTPIg+eYp0vmSbWnKRTOPj5DyVOfg1rHGEsuNKp07QwDxfLJfQVp8CNbuvxMQ==",
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
+ "@expo/expo-modules-macros-plugin": "0.6.1",
+ "expo-modules-jsi": "~57.0.4",
+ "invariant": "^2.2.4"
},
- "engines": {
- "node": ">= 0.4"
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0"
+ },
+ "peerDependenciesMeta": {
+ "react-native-worklets": {
+ "optional": true
+ }
}
},
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.392",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz",
- "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==",
- "license": "ISC"
- },
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "node_modules/expo-modules-jsi": {
+ "version": "57.0.4",
+ "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz",
+ "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==",
"license": "MIT",
- "engines": {
- "node": ">= 0.8"
+ "peerDependencies": {
+ "react-native": "*"
}
},
- "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",
+ "node_modules/expo-server": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz",
+ "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==",
+ "license": "MIT",
"engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "node": ">=20.16.0"
}
},
- "node_modules/error-stack-parser": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
- "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
+ "node_modules/expo-status-bar": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-57.0.1.tgz",
+ "integrity": "sha512-Xwaq1gAoVRWx5dPG5VhT5RSbnI9OilhZnO5qoPBnUaBAa5VzRzfdS8q0/bsPt0jR2DKLtGuP0bQ6efMJ4RIMDg==",
"license": "MIT",
- "dependencies": {
- "stackframe": "^1.3.4"
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
}
},
- "node_modules/es-abstract": {
- "version": "1.24.2",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
- "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
- "dev": true,
+ "node_modules/expo/node_modules/@expo/cli": {
+ "version": "57.0.10",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.10.tgz",
+ "integrity": "sha512-mP+B9ZrTnRE94bxPM/kCVKPyuVuOTRvvsqbXVxdXtrAfOfF94YIZLGUtw/151cGFtdUPbsBsJ9gW02LhU+QMZA==",
"license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.2",
- "arraybuffer.prototype.slice": "^1.0.4",
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "data-view-buffer": "^1.0.2",
- "data-view-byte-length": "^1.0.2",
- "data-view-byte-offset": "^1.0.1",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "es-set-tostringtag": "^2.1.0",
- "es-to-primitive": "^1.3.0",
- "function.prototype.name": "^1.1.8",
- "get-intrinsic": "^1.3.0",
- "get-proto": "^1.0.1",
- "get-symbol-description": "^1.1.0",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "internal-slot": "^1.1.0",
- "is-array-buffer": "^3.0.5",
- "is-callable": "^1.2.7",
- "is-data-view": "^1.0.2",
- "is-negative-zero": "^2.0.3",
- "is-regex": "^1.2.1",
- "is-set": "^2.0.3",
- "is-shared-array-buffer": "^1.0.4",
- "is-string": "^1.1.1",
- "is-typed-array": "^1.1.15",
- "is-weakref": "^1.1.1",
- "math-intrinsics": "^1.1.0",
- "object-inspect": "^1.13.4",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.7",
- "own-keys": "^1.0.1",
- "regexp.prototype.flags": "^1.5.4",
- "safe-array-concat": "^1.1.3",
- "safe-push-apply": "^1.0.0",
- "safe-regex-test": "^1.1.0",
- "set-proto": "^1.0.0",
- "stop-iteration-iterator": "^1.1.0",
- "string.prototype.trim": "^1.2.10",
- "string.prototype.trimend": "^1.0.9",
- "string.prototype.trimstart": "^1.0.8",
- "typed-array-buffer": "^1.0.3",
- "typed-array-byte-length": "^1.0.3",
- "typed-array-byte-offset": "^1.0.4",
- "typed-array-length": "^1.0.7",
- "unbox-primitive": "^1.1.0",
- "which-typed-array": "^1.1.19"
+ "@expo/code-signing-certificates": "^0.0.6",
+ "@expo/config": "~57.0.6",
+ "@expo/config-plugins": "~57.0.6",
+ "@expo/devcert": "^1.2.1",
+ "@expo/env": "~2.4.2",
+ "@expo/image-utils": "^0.11.4",
+ "@expo/inline-modules": "^0.1.3",
+ "@expo/json-file": "^11.0.1",
+ "@expo/log-box": "^57.0.1",
+ "@expo/metro": "~56.0.0",
+ "@expo/metro-config": "~57.0.7",
+ "@expo/metro-file-map": "^57.0.1",
+ "@expo/osascript": "^2.7.1",
+ "@expo/package-manager": "^1.13.1",
+ "@expo/plist": "^0.8.1",
+ "@expo/prebuild-config": "^57.0.9",
+ "@expo/require-utils": "^57.0.4",
+ "@expo/router-server": "^57.0.4",
+ "@expo/schema-utils": "^57.0.2",
+ "@expo/spawn-async": "^1.8.0",
+ "@expo/ws-tunnel": "^2.0.0",
+ "@expo/xcpretty": "^4.4.4",
+ "@react-native/dev-middleware": "0.86.0",
+ "accepts": "^1.3.8",
+ "agent-cli-detector": "^0.1.2",
+ "arg": "^5.0.2",
+ "bplist-creator": "0.1.0",
+ "bplist-parser": "^0.3.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.3.0",
+ "compression": "^1.7.4",
+ "connect": "^3.7.0",
+ "debug": "^4.3.4",
+ "dnssd-advertise": "^1.1.4",
+ "expo-server": "^57.0.1",
+ "fetch-nodeshim": "^0.4.10",
+ "getenv": "^2.0.0",
+ "glob": "^13.0.0",
+ "lan-network": "^0.2.1",
+ "multitars": "^1.0.0",
+ "node-forge": "^1.3.3",
+ "npm-package-arg": "^11.0.0",
+ "ora": "^3.4.0",
+ "picomatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "progress": "^2.0.3",
+ "prompts": "^2.3.2",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.6.0",
+ "send": "^0.19.0",
+ "slugify": "^1.3.4",
+ "stacktrace-parser": "^0.1.10",
+ "structured-headers": "^0.4.1",
+ "terminal-link": "^2.1.1",
+ "toqr": "^0.1.1",
+ "wrap-ansi": "^7.0.0",
+ "ws": "^8.12.1",
+ "zod": "^3.25.76"
},
- "engines": {
- "node": ">= 0.4"
+ "bin": {
+ "expo-internal": "main.js"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "expo": "*",
+ "expo-router": "*",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "expo-router": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
}
},
- "node_modules/es-abstract-get": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
- "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
- "dev": true,
+ "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": {
+ "version": "57.0.4",
+ "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.4.tgz",
+ "integrity": "sha512-ucqCP0hK8nZb9+S8QJYdQxNCkfRClzkKdg2RpWYDKDYgRIHyoRyxEDRgDgvbhH+q78yfY83abU8OTx8MMOrf1g==",
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.2",
- "is-callable": "^1.2.7",
- "object-inspect": "^1.13.4"
+ "debug": "^4.3.4"
},
- "engines": {
- "node": ">= 0.4"
+ "peerDependencies": {
+ "@expo/metro-runtime": "^57.0.7",
+ "expo": "*",
+ "expo-constants": "^57.0.7",
+ "expo-font": "^57.0.1",
+ "expo-router": "*",
+ "expo-server": "^57.0.1",
+ "react": "*",
+ "react-dom": "*",
+ "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependenciesMeta": {
+ "@expo/metro-runtime": {
+ "optional": true
+ },
+ "expo-router": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-server-dom-webpack": {
+ "optional": true
+ }
}
},
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
+ "node_modules/expo/node_modules/@expo/ws-tunnel": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz",
+ "integrity": "sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==",
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "peerDependencies": {
+ "ws": "^8.0.0"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "node_modules/expo/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": ">= 0.6"
}
},
- "node_modules/es-iterator-helpers": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz",
- "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==",
- "dev": true,
+ "node_modules/expo/node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
"license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.9",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.24.2",
- "es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.1.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.3.0",
- "globalthis": "^1.0.4",
- "gopd": "^1.2.0",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.2.0",
- "has-symbols": "^1.1.0",
- "internal-slot": "^1.1.0",
- "iterator.prototype": "^1.1.5",
- "math-intrinsics": "^1.1.0"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
}
},
- "node_modules/es-object-atoms": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
- "dev": true,
+ "node_modules/expo/node_modules/expo-file-system": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.1.tgz",
+ "integrity": "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw==",
"license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
}
},
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
+ "node_modules/expo/node_modules/expo-keep-awake": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz",
+ "integrity": "sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg==",
"license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
}
},
- "node_modules/es-shim-unscopables": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
- "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
- "dev": true,
+ "node_modules/expo/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">= 0.6"
}
},
- "node_modules/es-to-primitive": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
- "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
- "dev": true,
+ "node_modules/expo/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
- "es-abstract-get": "^1.0.0",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "is-callable": "^1.2.7",
- "is-date-object": "^1.1.0",
- "is-symbol": "^1.1.1"
+ "mime-db": "1.52.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/expo/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">= 0.6"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "node_modules/expo/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">=12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/eslint": {
- "version": "9.39.5",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
- "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
- "dev": true,
+ "node_modules/expo/node_modules/ws": {
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.2",
- "@eslint/config-helpers": "^0.4.2",
- "@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.6",
- "@eslint/js": "9.39.5",
- "@eslint/plugin-kit": "^0.4.1",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "ajv": "^6.14.0",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.4.0",
- "eslint-visitor-keys": "^4.2.1",
- "espree": "^10.4.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.5",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
+ "node": ">=10.0.0"
},
"peerDependencies": {
- "jiti": "*"
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
- "jiti": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
"optional": true
}
}
},
- "node_modules/eslint-config-expo": {
- "version": "57.0.0",
- "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.0.tgz",
- "integrity": "sha512-T7OTN9xrSZYjLw4qTkL1Mn2WfAUVmMGY38+OYAcraI1uiTFVH6jfkSkv84WLTqnneblLcb6AsMDV+SHcUj3hGw==",
+ "node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "^8.59.0",
- "@typescript-eslint/parser": "^8.59.0",
- "eslint-import-resolver-typescript": "^3.6.3",
- "eslint-plugin-expo": "^1.1.0",
- "eslint-plugin-import": "^2.30.0",
- "eslint-plugin-react": "^7.37.3",
- "eslint-plugin-react-hooks": "^7.0.0",
- "globals": "^16.0.0"
- },
- "peerDependencies": {
- "eslint": ">=8.10"
- }
+ "license": "MIT"
},
- "node_modules/eslint-config-prettier": {
- "version": "10.1.8",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
- "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "node_modules/fast-json-stable-stringify": {
+ "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,
- "license": "MIT",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fb-dotslash": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz",
+ "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==",
+ "license": "(MIT OR Apache-2.0)",
"bin": {
- "eslint-config-prettier": "bin/cli.js"
- },
- "funding": {
- "url": "https://opencollective.com/eslint-config-prettier"
+ "dotslash": "bin/dotslash"
},
- "peerDependencies": {
- "eslint": ">=7.0.0"
+ "engines": {
+ "node": ">=20"
}
},
- "node_modules/eslint-import-resolver-node": {
- "version": "0.3.10",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz",
- "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==",
- "dev": true,
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fbjs": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
+ "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
"license": "MIT",
"dependencies": {
- "debug": "^3.2.7",
- "is-core-module": "^2.16.1",
- "resolve": "^2.0.0-next.6"
+ "cross-fetch": "^3.1.5",
+ "fbjs-css-vars": "^1.0.0",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^1.0.35"
}
},
- "node_modules/eslint-import-resolver-node/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
+ "node_modules/fbjs-css-vars": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
+ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
+ "license": "MIT"
+ },
+ "node_modules/fbjs/node_modules/promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"license": "MIT",
"dependencies": {
- "ms": "^2.1.1"
+ "asap": "~2.0.3"
}
},
- "node_modules/eslint-import-resolver-node/node_modules/resolve": {
- "version": "2.0.0-next.7",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
- "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "node_modules/fetch-nodeshim": {
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz",
+ "integrity": "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "is-core-module": "^2.16.2",
- "node-exports-info": "^1.6.0",
- "object-keys": "^1.1.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
+ "flat-cache": "^4.0.0"
},
- "bin": {
- "resolve": "bin/resolve"
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
}
},
- "node_modules/eslint-import-resolver-typescript": {
- "version": "3.10.1",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
- "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "@nolyfill/is-core-module": "1.0.39",
- "debug": "^4.4.0",
- "get-tsconfig": "^4.10.0",
- "is-bun-module": "^2.0.0",
- "stable-hash": "^0.0.5",
- "tinyglobby": "^0.2.13",
- "unrs-resolver": "^1.6.2"
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
},
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": ">=10"
},
"funding": {
- "url": "https://opencollective.com/eslint-import-resolver-typescript"
- },
- "peerDependencies": {
- "eslint": "*",
- "eslint-plugin-import": "*",
- "eslint-plugin-import-x": "*"
- },
- "peerDependenciesMeta": {
- "eslint-plugin-import": {
- "optional": true
- },
- "eslint-plugin-import-x": {
- "optional": true
- }
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint-module-utils": {
- "version": "2.14.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz",
- "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==",
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "debug": "^3.2.7"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
},
"engines": {
- "node": ">=4"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
+ "node": ">=16"
}
},
- "node_modules/eslint-module-utils/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
+ "license": "ISC"
},
- "node_modules/eslint-plugin-expo": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.1.0.tgz",
- "integrity": "sha512-vPP0EPx7IA7ZfP49dY4rq9RV5jqkFWG+Pih3/oGjzIRjMI+ogcOE8i6isYkLXAdw/yvFV2BRZkTaQaiOGQqn6Q==",
+ "node_modules/flow-enums-runtime": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
+ "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==",
+ "license": "MIT"
+ },
+ "node_modules/fontfaceobserver": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
+ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "^8.59.0",
- "@typescript-eslint/utils": "^8.59.0",
- "eslint": "^9.24.0"
+ "is-callable": "^1.2.7"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "eslint": ">=8.10"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-import": {
- "version": "2.32.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
- "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@rtsao/scc": "^1.1.0",
- "array-includes": "^3.1.9",
- "array.prototype.findlastindex": "^1.2.6",
- "array.prototype.flat": "^1.3.3",
- "array.prototype.flatmap": "^1.3.3",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.12.1",
- "hasown": "^2.0.2",
- "is-core-module": "^2.16.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.8",
- "object.groupby": "^1.0.3",
- "object.values": "^1.2.1",
- "semver": "^6.3.1",
- "string.prototype.trimend": "^1.0.9",
- "tsconfig-paths": "^3.15.0"
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
},
"engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ "node": ">= 6"
}
},
- "node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
+ "engines": {
+ "node": ">= 0.6"
}
},
- "node_modules/eslint-plugin-import/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "mime-db": "1.52.0"
},
"engines": {
- "node": "*"
+ "node": ">= 0.6"
}
},
- "node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
}
},
- "node_modules/eslint-plugin-react": {
- "version": "7.37.5",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
- "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
+ "hasInstallScript": true,
"license": "MIT",
- "dependencies": {
- "array-includes": "^3.1.8",
- "array.prototype.findlast": "^1.2.5",
- "array.prototype.flatmap": "^1.3.3",
- "array.prototype.tosorted": "^1.1.4",
- "doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.2.1",
- "estraverse": "^5.3.0",
- "hasown": "^2.0.2",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.9",
- "object.fromentries": "^2.0.8",
- "object.values": "^1.2.1",
- "prop-types": "^15.8.1",
- "resolve": "^2.0.0-next.5",
- "semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.12",
- "string.prototype.repeat": "^1.0.0"
- },
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/eslint-plugin-react-hooks": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
- "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
+ "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.24.4",
- "@babel/parser": "^7.24.4",
- "hermes-parser": "^0.25.1",
- "zod": "^3.25.0 || ^4.0.0",
- "zod-validation-error": "^3.5.0 || ^4.0.0"
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2",
+ "hasown": "^2.0.4",
+ "is-callable": "^1.2.7",
+ "is-document.all": "^1.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
- "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
},
- "node_modules/eslint-plugin-react-hooks/node_modules/hermes-parser": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
- "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
- "dev": true,
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"license": "MIT",
- "dependencies": {
- "hermes-estree": "0.25.1"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "node_modules/eslint-plugin-react/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
"engines": {
- "node": "*"
+ "node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.7",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
- "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
- "is-core-module": "^2.16.2",
- "node-exports-info": "^1.6.0",
- "object-keys": "^1.1.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -4818,793 +7129,712 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
}
},
- "node_modules/eslint-scope": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
- "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
"dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "node": ">= 0.4"
}
},
- "node_modules/eslint-visitor-keys": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
- "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
"engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
+ "node": ">=10"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "resolve-pkg-maps": "^1.0.0"
},
- "engines": {
- "node": "*"
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
- "node_modules/espree": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
- "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.15.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.1"
- },
+ "node_modules/getenv": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
+ "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==",
+ "license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "node": ">=6"
}
},
- "node_modules/espree/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "18 || 20 || >=22"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/esquery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
- "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
- "license": "BSD-3-Clause",
+ "license": "ISC",
"dependencies": {
- "estraverse": "^5.1.0"
+ "is-glob": "^4.0.3"
},
"engines": {
- "node": ">=0.10"
+ "node": ">=10.13.0"
}
},
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=4.0"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
"engines": {
- "node": ">=4.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">=8"
}
},
- "node_modules/expo": {
- "version": "57.0.8",
- "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.8.tgz",
- "integrity": "sha512-0IxxoPZbT54IH4fHL5NihkvED9HBVQx3uNdPvyv8pFUHWJ81RdFjL0aJys1IB7hbo09KTz4xW4I2aWVOXKAcJQ==",
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.20.0",
- "@expo/cli": "^57.0.10",
- "@expo/config": "~57.0.6",
- "@expo/config-plugins": "~57.0.6",
- "@expo/devtools": "~57.0.1",
- "@expo/dom-webview": "~57.0.1",
- "@expo/fingerprint": "^0.20.6",
- "@expo/local-build-cache-provider": "^57.0.4",
- "@expo/log-box": "^57.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.7",
- "@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~57.0.4",
- "expo-asset": "~57.0.7",
- "expo-constants": "~57.0.7",
- "expo-file-system": "~57.0.1",
- "expo-font": "~57.0.1",
- "expo-keep-awake": "~57.0.1",
- "expo-modules-autolinking": "~57.0.9",
- "expo-modules-core": "~57.0.7",
- "pretty-format": "^29.7.0",
- "react-refresh": "^0.14.2",
- "whatwg-url-minimum": "^0.1.2"
- },
- "bin": {
- "expo": "bin/cli",
- "expo-modules-autolinking": "bin/autolinking",
- "fingerprint": "bin/fingerprint"
- },
- "peerDependencies": {
- "@expo/dom-webview": "*",
- "@expo/metro-runtime": "*",
- "react": "*",
- "react-dom": "*",
- "react-native": "*",
- "react-native-web": "*",
- "react-native-webview": "*"
+ "es-define-property": "^1.0.0"
},
- "peerDependenciesMeta": {
- "@expo/dom-webview": {
- "optional": true
- },
- "@expo/metro-runtime": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-native-web": {
- "optional": true
- },
- "react-native-webview": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/expo-asset": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.7.tgz",
- "integrity": "sha512-TA6MRQq1HL0N6KGvNMzL6djdH5tGJ/eHPIhML+6bVu52mnXldmup2swdvP37zDnvoMUUXRVGsvwVHFmvhCbW6Q==",
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.11.4",
- "expo-constants": "~57.0.7"
+ "dunder-proto": "^1.0.0"
},
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/expo-constants": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.7.tgz",
- "integrity": "sha512-ShDwaKnh3UieCQ/dG0kO8PuicTTatn3WDGmXbq/fukyzPXWhdUxf37DINVDQS6D3DDG7nfqItij+QvnDHSQhTg==",
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@expo/env": "~2.4.2"
+ "engines": {
+ "node": ">= 0.4"
},
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/expo-font": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
- "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "fontfaceobserver": "^2.1.0"
+ "has-symbols": "^1.0.3"
},
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-image-loader": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.1.tgz",
- "integrity": "sha512-uhrZKLT/cTl2mXyR28kPpVkS5O+PK9N1QA/07IFM4f5T4g0lTW1JHT3NEWwEEsGFldPmVX4j7LwUVVZxE+woug==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/expo-image-picker": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.6.tgz",
- "integrity": "sha512-6Of7SzyFVC+WFuFxhD4+nRTQ9joqldPhbiVWRsumg4ybttKmY8GxrnXAUSIDfa9DSk9PhgIAy2C1c5y1+CnU3g==",
+ "node_modules/hasown": {
+ "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==",
"license": "MIT",
"dependencies": {
- "expo-image-loader": "~57.0.1"
+ "function-bind": "^1.1.2"
},
- "peerDependencies": {
- "expo": "*"
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/expo-modules-autolinking": {
- "version": "57.0.9",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.9.tgz",
- "integrity": "sha512-lj2nsAKMMRLXSFnGgaaQrWJ2fdSpLPc/bca6Rkiw4g8zYPn7qX4MRUJgavvzm/hBrzvMnlhXVgJtidOGuwBh+w==",
+ "node_modules/hermes-compiler": {
+ "version": "250829098.0.14",
+ "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.14.tgz",
+ "integrity": "sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA==",
+ "license": "MIT"
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
+ "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
+ "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
"license": "MIT",
"dependencies": {
- "@expo/require-utils": "^57.0.4",
- "@expo/spawn-async": "^1.8.0",
- "chalk": "^4.1.0",
- "commander": "^7.2.0"
- },
- "bin": {
- "expo-modules-autolinking": "bin/expo-modules-autolinking.js"
+ "hermes-estree": "0.35.0"
}
},
- "node_modules/expo-modules-core": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.7.tgz",
- "integrity": "sha512-5HrbCfgYmLs0a5dzfM4GmRGelVTPIg+eYp0vmSbWnKRTOPj5DyVOfg1rHGEsuNKp07QwDxfLJfQVp8CNbuvxMQ==",
- "license": "MIT",
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
"dependencies": {
- "@expo/expo-modules-macros-plugin": "0.6.1",
- "expo-modules-jsi": "~57.0.4",
- "invariant": "^2.2.4"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*",
- "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0"
- },
- "peerDependenciesMeta": {
- "react-native-worklets": {
- "optional": true
- }
+ "react-is": "^16.7.0"
}
},
- "node_modules/expo-modules-jsi": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz",
- "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==",
- "license": "MIT",
- "peerDependencies": {
- "react-native": "*"
- }
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
},
- "node_modules/expo-server": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz",
- "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==",
- "license": "MIT",
+ "node_modules/hosted-git-info": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+ "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
"engines": {
- "node": ">=20.16.0"
+ "node": "^16.14.0 || >=18.0.0"
}
},
- "node_modules/expo-status-bar": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-57.0.1.tgz",
- "integrity": "sha512-Xwaq1gAoVRWx5dPG5VhT5RSbnI9OilhZnO5qoPBnUaBAa5VzRzfdS8q0/bsPt0jR2DKLtGuP0bQ6efMJ4RIMDg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
},
- "node_modules/expo/node_modules/@expo/cli": {
- "version": "57.0.10",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.10.tgz",
- "integrity": "sha512-mP+B9ZrTnRE94bxPM/kCVKPyuVuOTRvvsqbXVxdXtrAfOfF94YIZLGUtw/151cGFtdUPbsBsJ9gW02LhU+QMZA==",
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@expo/code-signing-certificates": "^0.0.6",
- "@expo/config": "~57.0.6",
- "@expo/config-plugins": "~57.0.6",
- "@expo/devcert": "^1.2.1",
- "@expo/env": "~2.4.2",
- "@expo/image-utils": "^0.11.4",
- "@expo/inline-modules": "^0.1.3",
- "@expo/json-file": "^11.0.1",
- "@expo/log-box": "^57.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.7",
- "@expo/metro-file-map": "^57.0.1",
- "@expo/osascript": "^2.7.1",
- "@expo/package-manager": "^1.13.1",
- "@expo/plist": "^0.8.1",
- "@expo/prebuild-config": "^57.0.9",
- "@expo/require-utils": "^57.0.4",
- "@expo/router-server": "^57.0.4",
- "@expo/schema-utils": "^57.0.2",
- "@expo/spawn-async": "^1.8.0",
- "@expo/ws-tunnel": "^2.0.0",
- "@expo/xcpretty": "^4.4.4",
- "@react-native/dev-middleware": "0.86.0",
- "accepts": "^1.3.8",
- "agent-cli-detector": "^0.1.2",
- "arg": "^5.0.2",
- "bplist-creator": "0.1.0",
- "bplist-parser": "^0.3.1",
- "chalk": "^4.0.0",
- "ci-info": "^3.3.0",
- "compression": "^1.7.4",
- "connect": "^3.7.0",
- "debug": "^4.3.4",
- "dnssd-advertise": "^1.1.4",
- "expo-server": "^57.0.1",
- "fetch-nodeshim": "^0.4.10",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "lan-network": "^0.2.1",
- "multitars": "^1.0.0",
- "node-forge": "^1.3.3",
- "npm-package-arg": "^11.0.0",
- "ora": "^3.4.0",
- "picomatch": "^4.0.4",
- "pretty-format": "^29.7.0",
- "progress": "^2.0.3",
- "prompts": "^2.3.2",
- "resolve-from": "^5.0.0",
- "semver": "^7.6.0",
- "send": "^0.19.0",
- "slugify": "^1.3.4",
- "stacktrace-parser": "^0.1.10",
- "structured-headers": "^0.4.1",
- "terminal-link": "^2.1.1",
- "toqr": "^0.1.1",
- "wrap-ansi": "^7.0.0",
- "ws": "^8.12.1",
- "zod": "^3.25.76"
- },
- "bin": {
- "expo-internal": "main.js"
- },
- "peerDependencies": {
- "expo": "*",
- "expo-router": "*",
- "react-native": "*"
+ "whatwg-encoding": "^2.0.0"
},
- "peerDependenciesMeta": {
- "expo-router": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.4.tgz",
- "integrity": "sha512-ucqCP0hK8nZb9+S8QJYdQxNCkfRClzkKdg2RpWYDKDYgRIHyoRyxEDRgDgvbhH+q78yfY83abU8OTx8MMOrf1g==",
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
- "debug": "^4.3.4"
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
},
- "peerDependencies": {
- "@expo/metro-runtime": "^57.0.7",
- "expo": "*",
- "expo-constants": "^57.0.7",
- "expo-font": "^57.0.1",
- "expo-router": "*",
- "expo-server": "^57.0.1",
- "react": "*",
- "react-dom": "*",
- "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
+ "engines": {
+ "node": ">= 0.8"
},
- "peerDependenciesMeta": {
- "@expo/metro-runtime": {
- "optional": true
- },
- "expo-router": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-server-dom-webpack": {
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/expo/node_modules/@expo/ws-tunnel": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz",
- "integrity": "sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==",
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
- "peerDependencies": {
- "ws": "^8.0.0"
+ "engines": {
+ "node": ">= 0.8"
}
},
- "node_modules/expo/node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 6"
}
},
- "node_modules/expo/node_modules/ci-info": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
- "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/sibiraj-s"
- }
- ],
+ "node_modules/http-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
"engines": {
- "node": ">=8"
+ "node": ">= 6.0.0"
}
},
- "node_modules/expo/node_modules/expo-file-system": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.1.tgz",
- "integrity": "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw==",
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
}
},
- "node_modules/expo/node_modules/expo-keep-awake": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz",
- "integrity": "sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg==",
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/expo/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">= 4"
}
},
- "node_modules/expo/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "node_modules/image-size": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
+ "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
- "mime-db": "1.52.0"
+ "queue": "6.0.2"
+ },
+ "bin": {
+ "image-size": "bin/image-size.js"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">=16.x"
}
},
- "node_modules/expo/node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">=4"
}
},
- "node_modules/expo/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=8"
},
"funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/expo/node_modules/ws": {
- "version": "8.21.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
- "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
+ "node": ">=0.8.19"
}
},
- "node_modules/exponential-backoff": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
- "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
- "license": "Apache-2.0"
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
},
- "node_modules/fast-json-stable-stringify": {
- "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==",
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "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,
- "license": "MIT"
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
},
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
},
- "node_modules/fb-dotslash": {
- "version": "0.5.8",
- "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz",
- "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==",
- "license": "(MIT OR Apache-2.0)",
- "bin": {
- "dotslash": "bin/dotslash"
- },
- "engines": {
- "node": ">=20"
+ "node_modules/inline-style-prefixer": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
+ "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
+ "license": "MIT",
+ "dependencies": {
+ "css-in-js-utils": "^3.1.0"
}
},
- "node_modules/fb-watchman": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
- "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
- "license": "Apache-2.0",
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "bser": "2.1.1"
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/fbjs": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
- "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
- "cross-fetch": "^3.1.5",
- "fbjs-css-vars": "^1.0.0",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^1.0.35"
+ "loose-envify": "^1.0.0"
}
},
- "node_modules/fbjs-css-vars": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
- "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
- "license": "MIT"
- },
- "node_modules/fbjs/node_modules/promise": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "asap": "~2.0.3"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/fetch-nodeshim": {
- "version": "0.4.10",
- "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz",
- "integrity": "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==",
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "flat-cache": "^4.0.0"
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "to-regex-range": "^5.0.1"
+ "has-bigints": "^1.0.2"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "unpipe": "~1.0.0"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/finalhandler/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "node_modules/is-bun-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+ "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ms": "2.0.0"
+ "semver": "^7.7.1"
}
},
- "node_modules/finalhandler/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "license": "MIT",
"dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
+ "hasown": "^2.0.3"
},
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
},
"engines": {
- "node": ">=16"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/flatted": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
- "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/flow-enums-runtime": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
- "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==",
- "license": "MIT"
- },
- "node_modules/fontfaceobserver": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
- "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==",
- "license": "BSD-2-Clause"
- },
- "node_modules/for-each": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
- "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-callable": "^1.2.7"
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -5613,40 +7843,29 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
"engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
+ "node": ">=8"
+ },
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/function.prototype.name": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
- "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
+ "node_modules/is-document.all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
+ "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.9",
- "call-bound": "^1.0.4",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.2",
- "hasown": "^2.0.4",
- "is-callable": "^1.2.7",
- "is-document.all": "^1.0.0"
+ "call-bound": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
@@ -5655,61 +7874,63 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/functions-have-names": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/generator-function": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
- "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
- "node": ">=6.9.0"
+ "node": ">=8"
}
},
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "license": "ISC",
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": "6.* || 8.* || >= 10.*"
+ "node": ">=6"
}
},
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
"get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -5718,31 +7939,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
+ "is-extglob": "^2.1.1"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=0.10.0"
}
},
- "node_modules/get-symbol-description": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
- "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6"
- },
"engines": {
"node": ">= 0.4"
},
@@ -5750,80 +7965,63 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-tsconfig": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
- "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/getenv": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
- "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==",
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"license": "MIT",
"engines": {
- "node": ">=6"
- }
- },
- "node_modules/glob": {
- "version": "13.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
- "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.2",
- "minipass": "^7.1.3",
- "path-scurry": "^2.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=0.12.0"
}
},
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "is-glob": "^4.0.3"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/globals": {
- "version": "16.5.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
- "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/globalthis": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
- "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "define-properties": "^1.2.1",
- "gopd": "^1.0.1"
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -5832,10 +8030,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5845,18 +8043,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC"
- },
- "node_modules/has-bigints": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
- "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -5864,36 +8059,46 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-define-property": "^1.0.0"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
- "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "dunder-proto": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -5902,12 +8107,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -5915,15 +8123,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
"engines": {
"node": ">= 0.4"
},
@@ -5931,693 +8136,833 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/hasown": {
- "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==",
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "function-bind": "^1.1.2"
+ "call-bound": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
- }
- },
- "node_modules/hermes-compiler": {
- "version": "250829098.0.14",
- "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.14.tgz",
- "integrity": "sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA==",
- "license": "MIT"
- },
- "node_modules/hermes-estree": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
- "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
- "license": "MIT"
- },
- "node_modules/hermes-parser": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
- "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.35.0"
- }
- },
- "node_modules/hosted-git-info": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
- "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^10.0.1"
},
- "engines": {
- "node": "^16.14.0 || >=18.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/hosted-git-info/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
- },
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/http-errors/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"license": "MIT",
"dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
+ "is-docker": "^2.0.0"
},
"engines": {
- "node": ">= 14"
+ "node": ">=8"
}
},
- "node_modules/hyphenate-style-name": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
- "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
- "license": "BSD-3-Clause"
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "license": "MIT",
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "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,
+ "license": "BSD-3-Clause",
"engines": {
- "node": ">= 4"
+ "node": ">=8"
}
},
- "node_modules/image-size": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
- "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
- "license": "MIT",
+ "node_modules/istanbul-lib-instrument": {
+ "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,
+ "license": "BSD-3-Clause",
"dependencies": {
- "queue": "6.0.2"
- },
- "bin": {
- "image-size": "bin/image-size.js"
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
},
"engines": {
- "node": ">=16.x"
+ "node": ">=8"
}
},
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
},
"engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=10"
}
},
- "node_modules/import-fresh/node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
"engines": {
- "node": ">=4"
+ "node": ">=10"
}
},
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-3-Clause",
"engines": {
- "node": ">=0.8.19"
+ "node": ">=0.10.0"
}
},
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/inline-style-prefixer": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
- "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
- "license": "MIT",
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "css-in-js-utils": "^3.1.0"
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/internal-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
- "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
- "hasown": "^2.0.2",
- "side-channel": "^1.1.0"
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
- "node_modules/invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
- "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "node_modules/jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
+ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.0.0"
+ "@jest/core": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "import-local": "^3.0.2",
+ "jest-cli": "^29.7.0"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
}
},
- "node_modules/is-array-buffer": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
- "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "node_modules/jest-changed-files": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
+ "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
+ "execa": "^5.0.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-async-function": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
- "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "node_modules/jest-circus": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz",
+ "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "async-function": "^1.0.0",
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.1",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^1.0.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^29.7.0",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^29.7.0",
+ "pure-rand": "^6.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-bigint": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
- "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "node_modules/jest-cli": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz",
+ "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "has-bigints": "^1.0.2"
+ "@jest/core": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "create-jest": "^29.7.0",
+ "exit": "^0.1.2",
+ "import-local": "^3.0.2",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
}
},
- "node_modules/is-boolean-object": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
- "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "node_modules/jest-config": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz",
+ "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-jest": "^29.7.0",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
}
},
- "node_modules/is-bun-module": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
- "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+ "node_modules/jest-config/node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
"license": "MIT",
- "dependencies": {
- "semver": "^7.7.1"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/is-callable": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "node_modules/jest-config/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
- "license": "MIT",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": "*"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/is-core-module": {
- "version": "2.16.2",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
- "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
- "license": "MIT",
+ "node_modules/jest-config/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "hasown": "^2.0.3"
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "*"
}
},
- "node_modules/is-data-view": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
- "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "node_modules/jest-diff": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "get-intrinsic": "^1.2.6",
- "is-typed-array": "^1.1.13"
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-date-object": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
- "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "node_modules/jest-docblock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
+ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
+ "detect-newline": "^3.0.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "node_modules/jest-each": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
+ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "is-docker": "cli.js"
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
},
"engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-document.all": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
- "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
+ "node_modules/jest-environment-jsdom": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
+ "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.4"
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/jsdom": "^20.0.0",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jsdom": "^20.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
}
},
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "node_modules/jest-environment-node": {
+ "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,
"license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
"engines": {
- "node": ">=0.10.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-finalizationregistry": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
- "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "node_modules/jest-expo": {
+ "version": "57.0.2",
+ "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-57.0.2.tgz",
+ "integrity": "sha512-xoKiYyu8c0fdBsFMkeFnxoTZ/0g4rLldA9isVb7VJSGBGesmhkVor7YkftkHqQ5rWiZ99IY+/uIrzTgb1nC/UA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3"
+ "@jest/create-cache-key-function": "^29.2.1",
+ "@jest/globals": "^29.2.1",
+ "babel-jest": "^29.2.1",
+ "jest-environment-jsdom": "^29.2.1",
+ "jest-snapshot": "^29.2.1",
+ "jest-watch-select-projects": "^2.0.0",
+ "jest-watch-typeahead": "2.2.1",
+ "json5": "^2.2.3",
+ "lodash": "^4.17.19",
+ "react-test-renderer": "19.2.3",
+ "server-only": "^0.0.1",
+ "stacktrace-js": "^2.0.2"
},
- "engines": {
- "node": ">= 0.4"
+ "bin": {
+ "jest": "bin/jest.js"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "@react-native/jest-preset": "^0.86.0",
+ "expo": "*",
+ "react-native": "*",
+ "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "react-server-dom-webpack": {
+ "optional": true
+ }
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+ "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-generator-function": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
- "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "node_modules/jest-haste-map": {
+ "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,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.4",
- "generator-function": "^2.0.0",
- "get-proto": "^1.0.1",
- "has-tostringtag": "^1.0.2",
- "safe-regex-test": "^1.1.0"
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
},
"engines": {
- "node": ">= 0.4"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
}
},
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "node_modules/jest-leak-detector": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
+ "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-extglob": "^2.1.1"
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
},
"engines": {
- "node": ">=0.10.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-map": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-negative-zero": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
- "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "node_modules/jest-message-util": {
+ "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,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "license": "MIT",
"engines": {
- "node": ">=0.12.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-number-object": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
- "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "node_modules/jest-mock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
+ "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-regex": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
- "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "gopd": "^1.2.0",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=6"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
}
},
- "node_modules/is-set": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "node_modules/jest-regex-util": {
+ "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,
"license": "MIT",
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-shared-array-buffer": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
- "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
+ "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3"
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-string": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
- "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "node_modules/jest-resolve-dependencies": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
+ "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "has-tostringtag": "^1.0.2"
+ "jest-regex-util": "^29.6.3",
+ "jest-snapshot": "^29.7.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-symbol": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
- "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "node_modules/jest-runner": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz",
+ "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.2",
- "has-symbols": "^1.1.0",
- "safe-regex-test": "^1.1.0"
+ "@jest/console": "^29.7.0",
+ "@jest/environment": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-leak-detector": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-resolve": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-typed-array": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
- "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "node_modules/jest-runner/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "which-typed-array": "^1.1.16"
- },
+ "license": "BSD-3-Clause",
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=0.10.0"
}
},
- "node_modules/is-weakmap": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "node_modules/jest-runner/node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
}
},
- "node_modules/is-weakref": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
- "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
+ "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3"
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/is-weakset": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
- "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "node_modules/jest-runtime/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
- "license": "MIT",
+ "license": "ISC",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-intrinsic": "^1.2.6"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": "*"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
- "license": "MIT",
+ "node_modules/jest-runtime/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "is-docker": "^2.0.0"
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": ">=8"
+ "node": "*"
}
},
- "node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/iterator.prototype": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
- "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "node_modules/jest-runtime/node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.6",
- "get-proto": "^1.0.0",
- "has-symbols": "^1.1.0",
- "set-function-name": "^2.0.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
}
},
- "node_modules/jest-get-type": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
- "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
@@ -6671,6 +9016,156 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/jest-watch-select-projects": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jest-watch-select-projects/-/jest-watch-select-projects-2.0.0.tgz",
+ "integrity": "sha512-j00nW4dXc2NiCW6znXgFLF9g8PJ0zP25cpQ1xRro/HU2GBfZQFZD0SoXnAlaoKkIY4MlfTMkKGbNXFpvCdjl1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "chalk": "^3.0.0",
+ "prompts": "^2.2.1"
+ }
+ },
+ "node_modules/jest-watch-select-projects/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watch-typeahead": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.1.tgz",
+ "integrity": "sha512-jYpYmUnTzysmVnwq49TAxlmtOAwp8QIqvZyoofQFn8fiWhEDZj33ZXzg3JA4nGnzWFm1hbWf3ADpteUokvXgFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^6.0.0",
+ "chalk": "^4.0.0",
+ "jest-regex-util": "^29.0.0",
+ "jest-watcher": "^29.0.0",
+ "slash": "^5.0.0",
+ "string-length": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "jest": "^27.0.0 || ^28.0.0 || ^29.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
+ "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/char-regex": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz",
+ "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/string-length": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz",
+ "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^2.0.0",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz",
+ "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "jest-util": "^29.7.0",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
"node_modules/jest-worker": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
@@ -6713,33 +9208,165 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
- "node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/puzrin"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/nodeca"
- }
- ],
+ "node_modules/js-yaml": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsc-safe-url": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz",
+ "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==",
+ "license": "0BSD"
+ },
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+ "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "argparse": "^2.0.1"
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
},
- "bin": {
- "js-yaml": "bin/js-yaml.js"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/jsc-safe-url": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz",
- "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==",
- "license": "0BSD"
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
},
"node_modules/jsesc": {
"version": "3.1.0",
@@ -6760,6 +9387,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -7127,6 +9761,13 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7143,6 +9784,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -7277,6 +9924,22 @@
"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",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -7647,6 +10310,16 @@
"node": ">=4"
}
},
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -7693,6 +10366,16 @@
"node": ">=10"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -7828,6 +10511,16 @@
"node": ">=18"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/npm-package-arg": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
@@ -7843,6 +10536,19 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -7861,6 +10567,13 @@
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==",
"license": "MIT"
},
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ob1": {
"version": "0.84.4",
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz",
@@ -8016,6 +10729,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "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,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/onetime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
@@ -8221,6 +10944,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8234,6 +10967,25 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parse-png": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz",
@@ -8246,6 +10998,32 @@
"node": ">=10"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -8265,6 +11043,16 @@
"node": ">=8"
}
},
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -8323,6 +11111,85 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pirates": {
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/plist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
@@ -8495,7 +11362,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -8507,9 +11373,21 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8520,6 +11398,30 @@
"node": ">=6"
}
},
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -8634,6 +11536,27 @@
}
}
},
+ "node_modules/react-native-calendars": {
+ "version": "1.1314.0",
+ "resolved": "https://registry.npmjs.org/react-native-calendars/-/react-native-calendars-1.1314.0.tgz",
+ "integrity": "sha512-4DLAVto8Qo9L3ggL2vsY9Gk8FFpJWtne8F/3wN8yUb7Xha9/SKS4B+vs7xlhWjKeqZUHws/Vi/q/6IZ8s60kcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hoist-non-react-statics": "^3.3.1",
+ "lodash": "^4.17.15",
+ "memoize-one": "^5.2.1",
+ "prop-types": "^15.5.10",
+ "react-native-swipe-gestures": "^1.0.5",
+ "recyclerlistview": "^4.0.0",
+ "xdate": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "moment": "^2.29.4"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
@@ -8659,6 +11582,12 @@
"react-native": "*"
}
},
+ "node_modules/react-native-swipe-gestures": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz",
+ "integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
+ "license": "MIT"
+ },
"node_modules/react-native-web": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -8714,13 +11643,63 @@
"node": ">=18"
}
},
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-test-renderer": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.2.3.tgz",
+ "integrity": "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-is": "^19.2.3",
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-test-renderer/node_modules/react-is": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/recyclerlistview": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.3.tgz",
+ "integrity": "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.debounce": "4.0.8",
+ "prop-types": "15.8.1",
+ "ts-object-utils": "0.0.5"
+ },
+ "peerDependencies": {
+ "react": ">= 15.2.1",
+ "react-native": ">= 0.30.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
"node_modules/reflect.getprototypeof": {
@@ -8835,6 +11814,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -8856,6 +11842,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -8881,6 +11880,16 @@
"integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==",
"license": "MIT"
},
+ "node_modules/resolve.exports": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
+ "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/restore-cursor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
@@ -8969,6 +11978,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/sax": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
@@ -8978,6 +11994,19 @@
"node": ">=11.0.0"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -9098,6 +12127,13 @@
"node": ">= 0.8"
}
},
+ "node_modules/server-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
+ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -9291,6 +12327,16 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/slugify": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
@@ -9337,6 +12383,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -9344,12 +12397,78 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stack-generator": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
+ "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stackframe": "^1.3.4"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
"license": "MIT"
},
+ "node_modules/stacktrace-gps": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
+ "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "0.5.6",
+ "stackframe": "^1.3.4"
+ }
+ },
+ "node_modules/stacktrace-gps/node_modules/source-map": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
+ "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stacktrace-js": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
+ "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-stack-parser": "^2.0.6",
+ "stack-generator": "^2.0.5",
+ "stacktrace-gps": "^3.0.4"
+ }
+ },
"node_modules/stacktrace-parser": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
@@ -9394,6 +12513,20 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -9529,6 +12662,29 @@
"node": ">=4"
}
},
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -9591,6 +12747,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/terminal-link": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
@@ -9631,12 +12794,70 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
+ "node_modules/test-exclude": {
+ "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,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/throat": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
"integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
"license": "MIT"
},
+ "node_modules/timeflow-alarm": {
+ "resolved": "modules/timeflow-alarm",
+ "link": true
+ },
+ "node_modules/timeflow-voice-recorder": {
+ "resolved": "modules/timeflow-voice-recorder",
+ "link": true
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -9715,6 +12936,22 @@
"integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==",
"license": "MIT"
},
+ "node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -9734,6 +12971,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-object-utils": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
+ "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==",
+ "license": "ISC"
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -9781,6 +13024,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-detect": {
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
@@ -9872,7 +13125,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -9973,6 +13226,16 @@
"node": ">=4"
}
},
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -10060,6 +13323,17 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -10082,6 +13356,21 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
"node_modules/validate-npm-package-name": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
@@ -10106,6 +13395,19 @@
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
"license": "MIT"
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -10136,12 +13438,36 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -10289,6 +13615,27 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": 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,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/ws": {
"version": "7.5.12",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.12.tgz",
@@ -10323,6 +13670,22 @@
"node": ">=10.0.0"
}
},
+ "node_modules/xdate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/xdate/-/xdate-0.8.3.tgz",
+ "integrity": "sha512-1NhJWPJwN+VjbkACT9XHbQK4o6exeSVtS2CxhMPwUE7xQakoEFTlwra9YcqV/uHQVyeEUYoYC46VGDJ+etnIiw==",
+ "license": "(MIT OR GPL-2.0)"
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/xml2js": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz",
@@ -10354,6 +13717,13 @@
"node": ">=8.0"
}
},
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 93c14b2..00dfd41 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,7 +10,6 @@
"dependencies": {
"@baidumap/jsapi-loader": "^1.0.0",
"@expo/metro-runtime": "~57.0.7",
- "@react-native-community/datetimepicker": "9.1.0",
"expo": "~57.0.8",
"expo-image-picker": "~57.0.6",
"expo-status-bar": "~57.0.1",
@@ -18,17 +17,25 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.0",
+ "react-native-calendars": "^1.1314.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-svg": "15.15.4",
"react-native-web": "^0.21.2",
- "react-native-webview": "13.16.1"
+ "react-native-webview": "13.16.1",
+ "timeflow-alarm": "file:modules/timeflow-alarm",
+ "timeflow-voice-recorder": "file:modules/timeflow-voice-recorder"
},
"devDependencies": {
"@baidumap/jsapi-v4-types": "^4.0.2",
+ "@react-native/jest-preset": "^0.86.2",
+ "@testing-library/react-native": "^13.2.0",
+ "@types/jest": "29.5.14",
"@types/react": "~19.2.2",
"eslint": "^9.39.5",
"eslint-config-expo": "^57.0.0",
"eslint-config-prettier": "^10.1.8",
+ "jest": "~29.7.0",
+ "jest-expo": "~57.0.2",
"prettier": "^3.9.5",
"typescript": "~6.0.3"
},
@@ -46,7 +53,40 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
- "check": "npm run lint && npm run format:check && npm run typecheck"
+ "test": "jest",
+ "check": "npm run lint && npm run format:check && npm run typecheck && npm run test"
+ },
+ "jest": {
+ "preset": "jest-expo",
+ "setupFilesAfterEnv": [
+ "/jest.setup.js"
+ ],
+ "testMatch": [
+ "/__tests__/**/*.test.ts",
+ "/__tests__/**/*.test.tsx"
+ ],
+ "moduleNameMapper": {
+ "^@/(.*)$": "/src/$1",
+ "^@test/(.*)$": "/__tests__/$1"
+ },
+ "testPathIgnorePatterns": [
+ "/node_modules/",
+ "/_backup_"
+ ],
+ "transformIgnorePatterns": [
+ "/node_modules/(?!(.pnpm|react-native|@react-native|@react-native-community|expo|@expo|@expo-google-fonts|react-navigation|@react-navigation|@sentry/react-native|native-base|standard-navigation|lucide-react-native|react-native-svg|react-native-calendars|react-native-swipe-gestures))"
+ ],
+ "collectCoverageFrom": [
+ "src/**/*.{ts,tsx}",
+ "!src/**/*.d.ts",
+ "!src/types/**",
+ "!src/contracts/**",
+ "!src/features/assistant/types.ts",
+ "!src/features/schedule/location/types.ts",
+ "!src/features/schedule/location/MapPicker/types.ts",
+ "!src/features/schedule/location/MapPicker/index.ts",
+ "!src/features/schedule/location/MapPicker/MapPicker.tsx"
+ ]
},
"private": true
}
diff --git a/frontend/plugins/withTimeflowAlarm.js b/frontend/plugins/withTimeflowAlarm.js
new file mode 100644
index 0000000..78d9912
--- /dev/null
+++ b/frontend/plugins/withTimeflowAlarm.js
@@ -0,0 +1,29 @@
+const { AndroidConfig, createRunOncePlugin, withAndroidManifest } = require('expo/config-plugins');
+
+const PACKAGE_NAME = 'timeflow-alarm';
+const PERMISSIONS = [
+ 'android.permission.POST_NOTIFICATIONS',
+ 'android.permission.SCHEDULE_EXACT_ALARM',
+ 'android.permission.SYSTEM_ALERT_WINDOW',
+ 'android.permission.USE_FULL_SCREEN_INTENT',
+ 'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
+ 'android.permission.VIBRATE',
+ 'android.permission.FOREGROUND_SERVICE',
+ 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
+];
+
+/**
+ * Ensures app-level alarm permissions survive prebuild.
+ * Native sources, AlarmPackage autolinking, and component declarations live in
+ * modules/timeflow-alarm (merged via the Android library manifest).
+ */
+function withTimeflowAlarm(config) {
+ config = AndroidConfig.Permissions.withPermissions(config, PERMISSIONS);
+ config = withAndroidManifest(config, (config) => {
+ AndroidConfig.Permissions.ensurePermissions(config.modResults, PERMISSIONS);
+ return config;
+ });
+ return config;
+}
+
+module.exports = createRunOncePlugin(withTimeflowAlarm, PACKAGE_NAME, '1.0.0');
diff --git a/frontend/plugins/withTimeflowVoiceRecorder.js b/frontend/plugins/withTimeflowVoiceRecorder.js
new file mode 100644
index 0000000..c1b6c9f
--- /dev/null
+++ b/frontend/plugins/withTimeflowVoiceRecorder.js
@@ -0,0 +1,26 @@
+const { AndroidConfig, createRunOncePlugin, withAndroidManifest } = require('expo/config-plugins');
+
+const PACKAGE_NAME = 'timeflow-voice-recorder';
+const RECORD_AUDIO = 'android.permission.RECORD_AUDIO';
+
+/** Keeps microphone and LAN ws:// support in generated release manifests. */
+function withTimeflowVoiceRecorder(config) {
+ config = AndroidConfig.Permissions.withPermissions(config, [RECORD_AUDIO]);
+ config = withAndroidManifest(config, (config) => {
+ const manifest = config.modResults;
+ AndroidConfig.Permissions.ensurePermissions(manifest, [RECORD_AUDIO]);
+
+ for (const permission of manifest.manifest['uses-permission'] ?? []) {
+ if (permission.$?.['android:name'] === RECORD_AUDIO) {
+ delete permission.$['tools:node'];
+ }
+ }
+
+ const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
+ application.$['android:usesCleartextTraffic'] = 'true';
+ return config;
+ });
+ return config;
+}
+
+module.exports = createRunOncePlugin(withTimeflowVoiceRecorder, PACKAGE_NAME, '1.0.0');
diff --git a/frontend/react-native.config.js b/frontend/react-native.config.js
new file mode 100644
index 0000000..d3f18db
--- /dev/null
+++ b/frontend/react-native.config.js
@@ -0,0 +1,12 @@
+const path = require('path');
+
+module.exports = {
+ dependencies: {
+ 'timeflow-alarm': {
+ root: path.join(__dirname, 'modules/timeflow-alarm'),
+ },
+ 'timeflow-voice-recorder': {
+ root: path.join(__dirname, 'modules/timeflow-voice-recorder'),
+ },
+ },
+};
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
deleted file mode 100644
index 1a2a935..0000000
--- a/frontend/src/api/client.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://127.0.0.1:8000/api/v1';
-
-export async function apiFetch(path: string, init?: RequestInit): Promise {
- const response = await fetch(`${API_BASE_URL}${path}`, init);
-
- if (!response.ok) {
- throw new Error(`API request failed with status ${response.status}`);
- }
-
- return (await response.json()) as T;
-}
-
-export { API_BASE_URL };
diff --git a/frontend/src/app/AppRoot.styles.ts b/frontend/src/app/AppRoot.styles.ts
new file mode 100644
index 0000000..f401a8f
--- /dev/null
+++ b/frontend/src/app/AppRoot.styles.ts
@@ -0,0 +1,8 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const appRootStyles = StyleSheet.create({
+ safeArea: { flex: 1, backgroundColor: colors.background },
+ appFrame: { flex: 1, backgroundColor: colors.background },
+});
diff --git a/frontend/src/app/AppRoot.tsx b/frontend/src/app/AppRoot.tsx
new file mode 100644
index 0000000..987472c
--- /dev/null
+++ b/frontend/src/app/AppRoot.tsx
@@ -0,0 +1,27 @@
+import { StatusBar } from 'expo-status-bar';
+import { View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { AppShell } from '@/app/AppShell';
+import type { LocationProvider } from '@/app/integrations/useLocationReporting';
+import type { VoiceRecorder } from '@/features/assistant';
+
+import { appRootStyles as styles } from './AppRoot.styles';
+
+/** 应用根布局:单屏组合,不做路由。 */
+export function AppRoot({
+ locationProvider,
+ voiceRecorder,
+}: {
+ locationProvider?: LocationProvider;
+ voiceRecorder?: VoiceRecorder;
+} = {}) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/AppShell.tsx b/frontend/src/app/AppShell.tsx
new file mode 100644
index 0000000..b4b6ee5
--- /dev/null
+++ b/frontend/src/app/AppShell.tsx
@@ -0,0 +1,208 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { useOverlay } from '@/app/overlay/OverlayProvider';
+import { useLocationReporting } from '@/app/integrations/useLocationReporting';
+import { useSession } from '@/app/session/SessionProvider';
+import { createVoiceRecorder } from '@/infrastructure/audio/VoiceRecorder';
+import {
+ AssistantChatSheet,
+ AssistantDock,
+ useAssistantSession,
+ type VoiceRecorder,
+} from '@/features/assistant';
+import {
+ StandardCreateModal,
+ ScheduleScreen,
+ scheduleDraftFromVoiceParse,
+ upsertDraftForSchedule,
+ useSessionSavedLocations,
+ useScheduleCommands,
+ type Schedule,
+ type ScheduleDraft,
+} from '@/features/schedule';
+import type { LocationProvider } from '@/app/integrations/useLocationReporting';
+import { useAppDialog } from '@/shared/components/AppDialogProvider';
+
+function errorMessage(error: unknown): string {
+ return error instanceof Error ? error.message : String(error);
+}
+
+/**
+ * App 组合根:接线 schedule 与 assistant,feature 之间不互相 import。
+ */
+export function AppShell({
+ locationProvider,
+ voiceRecorder: injectedVoiceRecorder,
+}: {
+ locationProvider?: LocationProvider;
+ voiceRecorder?: VoiceRecorder;
+} = {}) {
+ const { isOpen, push, popKind } = useOverlay();
+ const { showNotice } = useAppDialog();
+ const { client, connectionStatus, connectionError } = useSession();
+ const {
+ items: scheduleItems,
+ ready,
+ saveDraft,
+ toggleScheduleDone,
+ deleteSchedule,
+ mutation,
+ } = useScheduleCommands();
+ useLocationReporting({
+ client,
+ connectionStatus,
+ items: scheduleItems,
+ provider: locationProvider,
+ });
+
+ const [editingDraft, setEditingDraft] = useState(null);
+ const voiceRecorder = useMemo(
+ () => injectedVoiceRecorder ?? createVoiceRecorder(),
+ [injectedVoiceRecorder],
+ );
+ const { locations: savedLocations, upsert: upsertLocation } = useSessionSavedLocations();
+ const standardCreateOpen = isOpen('standardCreate');
+ const assistantOpen = isOpen('assistant');
+
+ useEffect(() => {
+ if (connectionError) {
+ void showNotice({ title: '连接不可用', message: connectionError });
+ }
+ }, [connectionError, showNotice]);
+
+ useEffect(() => {
+ if (mutation.status === 'error' && mutation.error) {
+ void showNotice({ title: '操作失败', message: mutation.error });
+ }
+ }, [mutation.error, mutation.status, showNotice]);
+
+ const closeStandardCreate = useCallback(() => {
+ setEditingDraft(null);
+ popKind('standardCreate');
+ }, [popKind]);
+
+ const openStandardCreate = useCallback(
+ (draft: ScheduleDraft | null = null) => {
+ if (!ready) {
+ void showNotice({ title: '日程服务未就绪', message: '请稍后重试' });
+ return;
+ }
+ setEditingDraft(draft);
+ if (!isOpen('standardCreate')) {
+ push({
+ kind: 'standardCreate',
+ onClose: () => setEditingDraft(null),
+ });
+ }
+ },
+ [isOpen, push, ready, showNotice],
+ );
+
+ const editSchedule = useCallback(
+ (item: Schedule) => {
+ openStandardCreate(upsertDraftForSchedule(item));
+ },
+ [openStandardCreate],
+ );
+
+ const saveStandardDraft = useCallback(
+ async (draft: ScheduleDraft) => {
+ await saveDraft(draft);
+ closeStandardCreate();
+ },
+ [closeStandardCreate, saveDraft],
+ );
+
+ const openAssistant = useCallback(() => {
+ if (!isOpen('assistant')) push({ kind: 'assistant' });
+ }, [isOpen, push]);
+
+ const closeAssistant = useCallback(() => {
+ popKind('assistant');
+ }, [popKind]);
+
+ const assistant = useAssistantSession({
+ client,
+ onConfirmDraft: async (voiceDraft) => {
+ await saveDraft(scheduleDraftFromVoiceParse(voiceDraft));
+ },
+ recorder: voiceRecorder,
+ });
+
+ const handleVoiceStart = useCallback(() => {
+ void assistant
+ .handleVoiceStart()
+ .catch((error) => showNotice({ title: '语音启动失败', message: errorMessage(error) }));
+ }, [assistant, showNotice]);
+
+ const handleVoiceEnd = useCallback(async () => {
+ openAssistant();
+ try {
+ await assistant.handleVoiceEnd();
+ } catch (error) {
+ await showNotice({ title: '语音解析失败', message: errorMessage(error) });
+ }
+ }, [assistant, openAssistant, showNotice]);
+
+ const onVoiceEnd = useCallback(() => {
+ void handleVoiceEnd();
+ }, [handleVoiceEnd]);
+
+ const onDeleteSchedule = useCallback(
+ (item: Schedule) => {
+ void deleteSchedule(item).catch(() => undefined);
+ },
+ [deleteSchedule],
+ );
+
+ const onToggleSchedule = useCallback(
+ (item: Schedule) => {
+ void toggleScheduleDone(item).catch(() => undefined);
+ },
+ [toggleScheduleDone],
+ );
+
+ return (
+ <>
+ openStandardCreate()}
+ onDeleteSchedule={onDeleteSchedule}
+ onEditSchedule={editSchedule}
+ onToggleSchedule={onToggleSchedule}
+ scheduleItems={scheduleItems}
+ />
+
+ {
+ void assistant.handleAction(messageId, action).catch((error) => {
+ void showNotice({ title: '助手操作失败', message: errorMessage(error) });
+ });
+ }}
+ onClose={closeAssistant}
+ onVoiceCancel={assistant.handleVoiceCancel}
+ onVoiceEnd={onVoiceEnd}
+ onVoiceStart={handleVoiceStart}
+ visible={assistantOpen}
+ />
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/app/integrations/reminderAlarmAdapter.ts b/frontend/src/app/integrations/reminderAlarmAdapter.ts
new file mode 100644
index 0000000..81952e6
--- /dev/null
+++ b/frontend/src/app/integrations/reminderAlarmAdapter.ts
@@ -0,0 +1,32 @@
+import type { AlarmPort } from '@/features/schedule';
+import {
+ cancelAndroidAlarm,
+ isAndroidAlarmSupported,
+ syncScheduleAlarm,
+} from '@/features/reminder';
+import type { AppDialogOptions } from '@/shared/components/AppDialogProvider';
+
+export function createReminderAlarmAdapter(
+ showNotice: (options: AppDialogOptions) => void | Promise,
+): AlarmPort {
+ return {
+ async syncForSchedule(input) {
+ try {
+ return await syncScheduleAlarm(input);
+ } catch {
+ // The schedule remains persisted even when its local alarm fails.
+ void showNotice({
+ title: '闹钟同步失败',
+ message: '日程已保存,但系统闹钟未创建成功。',
+ });
+ return null;
+ }
+ },
+ async cancel(alarmId) {
+ await cancelAndroidAlarm(alarmId);
+ // Unsupported platforms do not own the reference, so retain it in the
+ // schedule entity. Android owns and cancels the local alarm record.
+ return isAndroidAlarmSupported() ? null : (alarmId ?? null);
+ },
+ };
+}
diff --git a/frontend/src/app/integrations/scheduleConflictNotifier.ts b/frontend/src/app/integrations/scheduleConflictNotifier.ts
new file mode 100644
index 0000000..654ac4f
--- /dev/null
+++ b/frontend/src/app/integrations/scheduleConflictNotifier.ts
@@ -0,0 +1,15 @@
+import type { ScheduleConflictNotifier } from '@/features/schedule';
+import type { AppDialogOptions } from '@/shared/components/AppDialogProvider';
+
+/** App-owned UI adapter for conflict feedback emitted by the schedule use case. */
+export function createScheduleConflictNotifier(
+ showNotice: (options: AppDialogOptions) => void | Promise,
+): ScheduleConflictNotifier {
+ return (conflicts) => {
+ if (conflicts.length === 0) return;
+ void showNotice({
+ title: '当前时段已有日程',
+ message: conflicts.map((conflict) => conflict.title).join('、'),
+ });
+ };
+}
diff --git a/frontend/src/app/integrations/useLocationReporting.ts b/frontend/src/app/integrations/useLocationReporting.ts
new file mode 100644
index 0000000..7f71979
--- /dev/null
+++ b/frontend/src/app/integrations/useLocationReporting.ts
@@ -0,0 +1,35 @@
+import { useEffect, useMemo } from 'react';
+
+import type { ConnectionStatus } from '@/contracts';
+import type { Schedule } from '@/features/schedule';
+import {
+ createLocationProvider,
+ LocationReporter,
+ type LocationProvider,
+ type LocationTransport,
+} from '@/infrastructure/location/LocationReporter';
+
+export type { LocationProvider } from '@/infrastructure/location/LocationReporter';
+
+export function useLocationReporting(options: {
+ client: LocationTransport | null;
+ connectionStatus: ConnectionStatus;
+ items: Schedule[];
+ /** Tests and native hosts may provide a concrete background-aware provider. */
+ provider?: LocationProvider;
+}) {
+ const { client, connectionStatus, items, provider } = options;
+ const reporter = useMemo(
+ () => (client ? new LocationReporter(client, provider ?? createLocationProvider()) : null),
+ [client, provider],
+ );
+
+ useEffect(() => {
+ if (!reporter || connectionStatus !== 'ready') {
+ reporter?.stop();
+ return;
+ }
+ reporter.syncArmedSchedules(items);
+ return () => reporter.stop();
+ }, [connectionStatus, items, reporter]);
+}
diff --git a/frontend/src/app/overlay/OverlayProvider.tsx b/frontend/src/app/overlay/OverlayProvider.tsx
new file mode 100644
index 0000000..4acee11
--- /dev/null
+++ b/frontend/src/app/overlay/OverlayProvider.tsx
@@ -0,0 +1,109 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
+import { BackHandler } from 'react-native';
+import { useEffect } from 'react';
+
+export type OverlayKind =
+ | 'standardCreate'
+ | 'assistant'
+ | 'locationPicker'
+ | 'addressEditor'
+ | 'datePicker'
+ | 'timePicker'
+ | 'scheduleDetail'
+ | 'mapPicker';
+
+export type OverlayEntry = {
+ id: string;
+ kind: OverlayKind;
+ onClose?: () => void;
+};
+
+type OverlayContextValue = {
+ stack: OverlayEntry[];
+ push: (entry: Omit & { id?: string }) => string;
+ pop: () => void;
+ popKind: (kind: OverlayKind) => void;
+ isOpen: (kind: OverlayKind) => boolean;
+ top: OverlayEntry | null;
+};
+
+const OverlayContext = createContext(null);
+
+let overlaySeq = 0;
+
+export function OverlayProvider({ children }: { children: ReactNode }) {
+ const [stack, setStack] = useState([]);
+ // Event handlers update this synchronously so multiple pop operations in
+ // one event use the latest stack without putting callbacks in a state
+ // updater (which React may invoke more than once in StrictMode).
+ const stackRef = useRef([]);
+
+ const push = useCallback((entry: Omit & { id?: string }) => {
+ const id = entry.id ?? `overlay_${++overlaySeq}`;
+ const nextEntry = { ...entry, id };
+ const nextStack = [...stackRef.current, nextEntry];
+ stackRef.current = nextStack;
+ setStack(nextStack);
+ return id;
+ }, []);
+
+ const pop = useCallback(() => {
+ const current = stackRef.current;
+ const top = current[current.length - 1];
+ if (!top) return;
+ const nextStack = current.slice(0, -1);
+ stackRef.current = nextStack;
+ setStack(nextStack);
+ top.onClose?.();
+ }, []);
+
+ const popKind = useCallback((kind: OverlayKind) => {
+ const current = stackRef.current;
+ const index = [...current].map((item) => item.kind).lastIndexOf(kind);
+ if (index < 0) return;
+ const removed = current[index];
+ const nextStack = current.filter((_, i) => i !== index);
+ stackRef.current = nextStack;
+ setStack(nextStack);
+ removed?.onClose?.();
+ }, []);
+
+ const isOpen = useCallback(
+ (kind: OverlayKind) => stack.some((item) => item.kind === kind),
+ [stack],
+ );
+
+ const top = stack.length > 0 ? stack[stack.length - 1]! : null;
+
+ useEffect(() => {
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
+ if (stackRef.current.length === 0) return false;
+ pop();
+ return true;
+ });
+ return () => subscription.remove();
+ }, [pop]);
+
+ const value = useMemo(
+ () => ({ stack, push, pop, popKind, isOpen, top }),
+ [isOpen, pop, popKind, push, stack, top],
+ );
+
+ return {children};
+}
+
+export function useOverlay(): OverlayContextValue {
+ const value = useContext(OverlayContext);
+ if (!value) {
+ throw new Error('useOverlay must be used within OverlayProvider');
+ }
+ return value;
+}
diff --git a/frontend/src/app/providers.styles.ts b/frontend/src/app/providers.styles.ts
new file mode 100644
index 0000000..b441a0b
--- /dev/null
+++ b/frontend/src/app/providers.styles.ts
@@ -0,0 +1,31 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const providerStyles = StyleSheet.create({
+ desktopCanvas: {
+ alignItems: 'center',
+ backgroundColor: '#DDE2DF',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 24,
+ },
+ compactCanvas: { padding: 0 },
+ webAppFrame: {
+ backgroundColor: colors.background,
+ borderColor: '#C9CFCC',
+ borderRadius: 24,
+ borderWidth: 1,
+ boxShadow: '0 20px 60px rgba(20, 40, 33, 0.18)',
+ flex: 1,
+ maxHeight: 900,
+ maxWidth: 430,
+ overflow: 'hidden',
+ width: '100%',
+ },
+ compactFrame: {
+ borderRadius: 0,
+ borderWidth: 0,
+ maxHeight: '100%',
+ },
+});
diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx
new file mode 100644
index 0000000..8167d34
--- /dev/null
+++ b/frontend/src/app/providers.tsx
@@ -0,0 +1,71 @@
+import { useMemo, type ReactNode } from 'react';
+import { Platform, useWindowDimensions, View } from 'react-native';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+
+import { OverlayProvider } from '@/app/overlay/OverlayProvider';
+import { createReminderAlarmAdapter } from '@/app/integrations/reminderAlarmAdapter';
+import { createScheduleConflictNotifier } from '@/app/integrations/scheduleConflictNotifier';
+import { SessionProvider, useSession } from '@/app/session/SessionProvider';
+import type { DeviceIdStore } from '@/infrastructure/storage/deviceIdStore';
+import { ScheduleProvider } from '@/features/schedule';
+import { AppDialogProvider, useAppDialog } from '@/shared/components/AppDialogProvider';
+
+import { providerStyles as styles } from './providers.styles';
+
+/** 将 session 注入 schedule,避免 feature 反向依赖 app。 */
+function ScheduleSessionBridge({ children }: { children: ReactNode }) {
+ const { client, connectionStatus, userId, sessionEpoch } = useSession();
+ const { showNotice } = useAppDialog();
+ const alarmAdapter = useMemo(() => createReminderAlarmAdapter(showNotice), [showNotice]);
+ const notifyConflicts = useMemo(() => createScheduleConflictNotifier(showNotice), [showNotice]);
+ return (
+
+ {children}
+
+ );
+}
+
+export function AppProviders({
+ children,
+ deviceIdStore,
+}: {
+ children: ReactNode;
+ deviceIdStore?: DeviceIdStore;
+}) {
+ const { width } = useWindowDimensions();
+
+ const tree = (
+
+
+ {children}
+
+
+ );
+
+ if (Platform.OS !== 'web') {
+ return (
+
+ {tree}
+
+ );
+ }
+
+ const compact = width < 480;
+
+ return (
+
+
+
+ {tree}
+
+
+
+ );
+}
diff --git a/frontend/src/app/session/SessionProvider.tsx b/frontend/src/app/session/SessionProvider.tsx
new file mode 100644
index 0000000..e87828f
--- /dev/null
+++ b/frontend/src/app/session/SessionProvider.tsx
@@ -0,0 +1,290 @@
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
+
+import type { ConnectionStatus, SessionHello, SessionReady, WsJsonMessage } from '@/contracts';
+
+import { FakeWsServer } from '@/dev/fakes/FakeWsServer';
+import { getOrCreateDeviceId, type DeviceIdStore } from '@/infrastructure/storage/deviceIdStore';
+import { WsClient } from '@/infrastructure/ws/WsClient';
+
+import { buildSessionWebSocketUrl, resolveSessionUserId } from './sessionEndpoint';
+
+export type SessionTransportMode = 'remote' | 'fake' | 'unavailable';
+
+export type SessionContextValue = {
+ deviceId: string | null;
+ userId: string | null;
+ connectionStatus: ConnectionStatus;
+ transportMode: SessionTransportMode;
+ /** 每次成功 session.ready 递增,供 schedule 重连后 resync。 */
+ sessionEpoch: number;
+ client: WsClient | null;
+ fakeServer: FakeWsServer | null;
+ connectionError: string | null;
+};
+
+const SessionContext = createContext(null);
+
+function resolveWsUrl(): string | null {
+ const fromEnv =
+ typeof process !== 'undefined' ? process.env.EXPO_PUBLIC_WS_URL?.trim() : undefined;
+ return fromEnv || null;
+}
+
+function resolveAllowFake(): boolean {
+ const flag =
+ typeof process !== 'undefined' ? process.env.EXPO_PUBLIC_USE_FAKE_WS?.trim() : undefined;
+ const isDevBuild = typeof __DEV__ !== 'undefined' && __DEV__;
+ // Fake 只能进入开发/调试构建;release 即使误带变量也必须拒绝。
+ if (!isDevBuild) return false;
+ if (flag === '0' || flag === 'false') return false;
+ return flag === '1' || flag === 'true' || flag == null;
+}
+
+const RECONNECT_BASE_MS = 1000;
+const RECONNECT_MAX_MS = 30_000;
+const SESSION_READY_TIMEOUT_MS = 10_000;
+const UNAVAILABLE_CONNECTION_ERROR =
+ '缺少 EXPO_PUBLIC_WS_URL。开发环境可设置 EXPO_PUBLIC_USE_FAKE_WS=true 使用进程内 Fake。';
+
+function isSessionReady(message: WsJsonMessage, deviceId: string): message is SessionReady {
+ return (
+ message.type === 'session.ready' &&
+ message.device_id === deviceId &&
+ (message.user_id == null ||
+ (typeof message.user_id === 'string' && message.user_id.trim().length > 0)) &&
+ typeof message.server_time === 'string'
+ );
+}
+
+export function SessionProvider({
+ children,
+ deviceIdStore,
+}: {
+ children: ReactNode;
+ deviceIdStore?: DeviceIdStore;
+}) {
+ const [deviceId, setDeviceId] = useState(null);
+ const [userId, setUserId] = useState(null);
+ const [connectionStatus, setConnectionStatus] = useState('idle');
+ const [sessionEpoch, setSessionEpoch] = useState(0);
+ const [connectionError, setConnectionError] = useState(null);
+
+ const url = useMemo(() => resolveWsUrl(), []);
+ const allowFake = useMemo(() => resolveAllowFake(), []);
+ const transportMode: SessionTransportMode = url ? 'remote' : allowFake ? 'fake' : 'unavailable';
+
+ const remoteEndpoint = useMemo(() => {
+ if (transportMode !== 'remote' || !url || !deviceId) {
+ return { url: null, error: null };
+ }
+ try {
+ return { url: buildSessionWebSocketUrl(url, deviceId), error: null };
+ } catch (error) {
+ return {
+ url: null,
+ error: error instanceof Error ? error.message : 'WebSocket 地址不合法',
+ };
+ }
+ }, [deviceId, transportMode, url]);
+
+ const { client, fakeServer } = useMemo(() => {
+ if (transportMode === 'unavailable') {
+ return { client: null as WsClient | null, fakeServer: null as FakeWsServer | null };
+ }
+ if (transportMode === 'remote' && !remoteEndpoint.url) {
+ return { client: null as WsClient | null, fakeServer: null as FakeWsServer | null };
+ }
+ const server = transportMode === 'fake' ? new FakeWsServer() : null;
+ const ws = new WsClient({
+ url: transportMode === 'remote' ? remoteEndpoint.url : null,
+ fakeHandler: server ? server.handleMessage : undefined,
+ });
+ server?.attach(ws);
+ return { client: ws, fakeServer: server };
+ }, [remoteEndpoint.url, transportMode]);
+
+ const reconnectAttempt = useRef(0);
+ const reconnectTimer = useRef | null>(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ void getOrCreateDeviceId(deviceIdStore)
+ .then((id) => {
+ if (!cancelled) setDeviceId(id);
+ })
+ .catch((error: unknown) => {
+ if (cancelled) return;
+ setConnectionStatus('error');
+ setConnectionError(
+ error instanceof Error ? error.message : '无法初始化设备身份,请检查原生存储配置',
+ );
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [deviceIdStore]);
+
+ useEffect(() => {
+ if (!client) return;
+ if (!deviceId) return;
+
+ let cancelled = false;
+ let sessionReadyTimer: ReturnType | null = null;
+
+ const clearReconnect = () => {
+ if (reconnectTimer.current) {
+ clearTimeout(reconnectTimer.current);
+ reconnectTimer.current = null;
+ }
+ };
+
+ const clearSessionReadyTimer = () => {
+ if (!sessionReadyTimer) return;
+ clearTimeout(sessionReadyTimer);
+ sessionReadyTimer = null;
+ };
+
+ const sendHello = () => {
+ const hello: SessionHello = {
+ type: 'session.hello',
+ device_id: deviceId,
+ app_version: '1.0.0',
+ };
+ clearSessionReadyTimer();
+ sessionReadyTimer = setTimeout(() => {
+ if (cancelled) return;
+ setConnectionStatus('error');
+ setConnectionError('会话握手超时,请检查服务连接');
+ client.close();
+ scheduleReconnect();
+ }, SESSION_READY_TIMEOUT_MS);
+ client.sendJson(hello);
+ };
+
+ const connectOnce = async () => {
+ try {
+ setConnectionError(null);
+ await client.connect();
+ if (cancelled) return;
+ reconnectAttempt.current = 0;
+ sendHello();
+ } catch (error) {
+ if (cancelled) return;
+ clearSessionReadyTimer();
+ client.close();
+ setConnectionStatus('error');
+ setConnectionError(error instanceof Error ? error.message : 'WebSocket 连接失败');
+ scheduleReconnect();
+ }
+ };
+
+ const scheduleReconnect = () => {
+ if (cancelled || transportMode === 'fake') return;
+ clearReconnect();
+ const attempt = reconnectAttempt.current;
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS);
+ reconnectAttempt.current = attempt + 1;
+ setConnectionStatus('reconnecting');
+ reconnectTimer.current = setTimeout(() => {
+ void connectOnce();
+ }, delay);
+ };
+
+ const unsubscribeStatus = client.onStatus((status) => {
+ if (cancelled) return;
+ // WebSocket open 只代表 socket 可用;session.ready 才代表身份握手完成。
+ setConnectionStatus(status === 'ready' ? 'connecting' : status);
+ if (status === 'closed') {
+ clearSessionReadyTimer();
+ scheduleReconnect();
+ }
+ });
+
+ const unsubscribeMessage = client.onMessage((message) => {
+ if (message instanceof ArrayBuffer) return;
+ if (isSessionReady(message, deviceId)) {
+ clearSessionReadyTimer();
+ setUserId(resolveSessionUserId(message.user_id));
+ setSessionEpoch((value) => value + 1);
+ setConnectionStatus('ready');
+ setConnectionError(null);
+ return;
+ }
+ if (message.type === 'session.error') {
+ clearSessionReadyTimer();
+ setConnectionStatus('error');
+ setConnectionError(
+ typeof message.error === 'object' &&
+ message.error !== null &&
+ 'message' in message.error &&
+ typeof message.error.message === 'string'
+ ? message.error.message
+ : '会话握手失败',
+ );
+ }
+ });
+
+ void connectOnce();
+
+ return () => {
+ cancelled = true;
+ clearReconnect();
+ clearSessionReadyTimer();
+ unsubscribeStatus();
+ unsubscribeMessage();
+ client.close();
+ };
+ }, [client, deviceId, transportMode]);
+
+ const effectiveConnectionStatus: ConnectionStatus =
+ transportMode === 'unavailable' || remoteEndpoint.error || (connectionError && !client)
+ ? 'error'
+ : client
+ ? connectionStatus
+ : 'connecting';
+ const effectiveConnectionError =
+ transportMode === 'unavailable'
+ ? UNAVAILABLE_CONNECTION_ERROR
+ : (remoteEndpoint.error ?? connectionError);
+
+ const value = useMemo(
+ () => ({
+ deviceId,
+ userId,
+ connectionStatus: effectiveConnectionStatus,
+ transportMode,
+ sessionEpoch,
+ client,
+ fakeServer,
+ connectionError: effectiveConnectionError,
+ }),
+ [
+ client,
+ deviceId,
+ effectiveConnectionError,
+ effectiveConnectionStatus,
+ fakeServer,
+ sessionEpoch,
+ transportMode,
+ userId,
+ ],
+ );
+
+ return {children};
+}
+
+export function useSession(): SessionContextValue {
+ const value = useContext(SessionContext);
+ if (!value) {
+ throw new Error('useSession must be used within SessionProvider');
+ }
+ return value;
+}
diff --git a/frontend/src/app/session/sessionEndpoint.ts b/frontend/src/app/session/sessionEndpoint.ts
new file mode 100644
index 0000000..6b49fd8
--- /dev/null
+++ b/frontend/src/app/session/sessionEndpoint.ts
@@ -0,0 +1,15 @@
+const LEGACY_BACKEND_USER_ID = 'default_user';
+
+export function buildSessionWebSocketUrl(baseUrl: string, deviceId: string): string {
+ const url = new URL(baseUrl);
+ if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
+ throw new Error('EXPO_PUBLIC_WS_URL 必须使用 ws:// 或 wss://');
+ }
+ url.searchParams.set('device_id', deviceId);
+ return url.toString();
+}
+
+/** Current MVP backend owns a single default user but omits it from session.ready. */
+export function resolveSessionUserId(userId: unknown): string {
+ return typeof userId === 'string' && userId.trim() ? userId.trim() : LEGACY_BACKEND_USER_ID;
+}
diff --git a/frontend/src/components/AppChrome.styles.ts b/frontend/src/components/AppChrome.styles.ts
deleted file mode 100644
index 5ef43ef..0000000
--- a/frontend/src/components/AppChrome.styles.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { StyleSheet } from 'react-native';
-
-import { colors, spacing } from '../constants/theme';
-
-export const commonStyles = StyleSheet.create({
- safeArea: { flex: 1, backgroundColor: colors.background },
- appFrame: { flex: 1, backgroundColor: colors.background },
- screen: { flex: 1, paddingHorizontal: spacing.lg, paddingTop: spacing.md },
- header: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: spacing.md,
- },
- eyebrow: { color: colors.sub, fontSize: 9, marginBottom: 5 },
- pageTitle: { color: colors.ink, fontSize: 25, fontWeight: '800', letterSpacing: 0 },
- headerActions: { alignItems: 'center', flexDirection: 'row', gap: 8 },
- addButton: {
- alignItems: 'center',
- backgroundColor: '#EAE7DF',
- borderRadius: 13,
- height: 37,
- justifyContent: 'center',
- width: 37,
- },
- addButtonText: { color: colors.deep, fontSize: 20, lineHeight: 23 },
- avatar: {
- alignItems: 'center',
- backgroundColor: colors.deep,
- borderColor: colors.lime,
- borderRadius: 15,
- borderWidth: 2,
- height: 37,
- justifyContent: 'center',
- width: 37,
- },
- avatarLight: { backgroundColor: colors.lime, borderColor: colors.lime, height: 53, width: 53 },
- viewSwitch: { backgroundColor: '#EBE8E1', borderRadius: 13, flexDirection: 'row', padding: 4 },
- switchOption: {
- alignItems: 'center',
- borderRadius: 10,
- flex: 1,
- height: 32,
- justifyContent: 'center',
- },
- switchOptionActive: { backgroundColor: colors.surface, elevation: 2 },
- switchText: { color: colors.sub, fontSize: 10 },
- switchTextActive: { color: colors.ink, fontWeight: '800' },
-});
diff --git a/frontend/src/components/AppChrome.tsx b/frontend/src/components/AppChrome.tsx
deleted file mode 100644
index 160f4f7..0000000
--- a/frontend/src/components/AppChrome.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { ReactNode } from 'react';
-import { UserRound } from 'lucide-react-native';
-import { Pressable, Text, View } from 'react-native';
-
-import { colors } from '../constants/theme';
-import type { CalendarView } from '../types/home';
-import { commonStyles as styles } from './AppChrome.styles';
-
-export function Avatar({
- onPress,
- variant = 'dark',
-}: {
- onPress?: () => void;
- variant?: 'dark' | 'light';
-}) {
- const avatar = (
-
-
-
- );
-
- return onPress ? (
-
- {avatar}
-
- ) : (
- avatar
- );
-}
-
-export function ViewSwitch({
- value,
- onChange,
-}: {
- value: CalendarView;
- onChange: (view: CalendarView) => void;
-}) {
- return (
-
- {(['day', 'week', 'month'] as CalendarView[]).map((item) => (
- onChange(item)}
- style={[styles.switchOption, value === item && styles.switchOptionActive]}
- >
-
- {item === 'day' ? '日' : item === 'week' ? '周' : '月'}
-
-
- ))}
-
- );
-}
-
-export function Header({
- eyebrow,
- title,
- action,
-}: {
- eyebrow: string;
- title: string;
- action?: ReactNode;
-}) {
- return (
-
-
- {eyebrow}
- {title}
-
- {action ?? }
-
- );
-}
diff --git a/frontend/src/components/AssistantDock.tsx b/frontend/src/components/AssistantDock.tsx
deleted file mode 100644
index 15328f3..0000000
--- a/frontend/src/components/AssistantDock.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Pressable, View } from 'react-native';
-import Svg, { Path } from 'react-native-svg';
-
-import { assistantStyles as styles } from '../screens/AssistantScreen.styles';
-
-export function TempoAssistantIcon({
- color = '#D9F65A',
- size = 22,
-}: {
- color?: string;
- size?: number;
-}) {
- return (
-
- );
-}
-
-export function AssistantDock({ onPress }: { onPress: () => void }) {
- return (
-
- [
- styles.assistantDockButton,
- pressed && styles.assistantDockButtonPressed,
- ]}
- >
-
-
-
- );
-}
diff --git a/frontend/src/contracts/envelope.ts b/frontend/src/contracts/envelope.ts
new file mode 100644
index 0000000..f7ff041
--- /dev/null
+++ b/frontend/src/contracts/envelope.ts
@@ -0,0 +1,25 @@
+export type ApiError = {
+ code: string;
+ message: string;
+ details: Record | null;
+};
+
+export type WsRequest = {
+ type: TType;
+ request_id: string;
+ payload: TPayload;
+};
+
+export type WsSuccess = {
+ type: TType;
+ request_id: string;
+ ok: true;
+ payload: TPayload;
+};
+
+export type WsFailure = {
+ type: TType;
+ request_id: string;
+ ok: false;
+ error: ApiError;
+};
diff --git a/frontend/src/contracts/index.ts b/frontend/src/contracts/index.ts
new file mode 100644
index 0000000..5b1d678
--- /dev/null
+++ b/frontend/src/contracts/index.ts
@@ -0,0 +1,6 @@
+export type * from './envelope';
+export type * from './reminder';
+export type * from './schedule';
+export type * from './session';
+export type * from './transport';
+export type * from './voice';
diff --git a/frontend/src/contracts/reminder.ts b/frontend/src/contracts/reminder.ts
new file mode 100644
index 0000000..7936d25
--- /dev/null
+++ b/frontend/src/contracts/reminder.ts
@@ -0,0 +1,48 @@
+import type { ApiError } from './envelope';
+
+/**
+ * 提醒通道协议(预留)。
+ * 当前前端尚未接入 WS 提醒控制 / TTS 音频流;类型仅作与后端对齐的权威契约文档。
+ * 接入客户端前请勿在业务层依赖这些消息。
+ */
+
+/** 服务端下发提醒控制;具体展示通道由客户端根据前后台自行决定。 */
+export type ReminderControl = {
+ type: 'reminder.control';
+ schedule_id: string;
+ reason: string;
+ action: 'show';
+};
+
+export type ReminderControlAck =
+ | { type: 'reminder.control.ack'; schedule_id: string; ok: true }
+ | { type: 'reminder.control.ack'; schedule_id: string; ok: false; error: ApiError };
+
+/** 提醒 TTS 音频流开始;随后通过同一 WebSocket 连接发送 Binary Frame。 */
+export type ReminderAudioStart = {
+ type: 'reminder.audio.start';
+ schedule_id: string;
+ stream_id: string;
+ audio_format: 'mp3';
+};
+
+export type ReminderAudioEnd = {
+ type: 'reminder.audio.end';
+ schedule_id: string;
+ stream_id: string;
+};
+
+export type ReminderAudioAck =
+ | {
+ type: 'reminder.audio.ack';
+ schedule_id: string;
+ stream_id: string;
+ ok: true;
+ }
+ | {
+ type: 'reminder.audio.ack';
+ schedule_id: string;
+ stream_id: string;
+ ok: false;
+ error: ApiError;
+ };
diff --git a/frontend/src/contracts/schedule.ts b/frontend/src/contracts/schedule.ts
new file mode 100644
index 0000000..9978407
--- /dev/null
+++ b/frontend/src/contracts/schedule.ts
@@ -0,0 +1,134 @@
+import type { ApiError, WsFailure, WsRequest, WsSuccess } from './envelope';
+
+export type ScheduleSourceMode = 'manual' | 'voice';
+export type ScheduleType = 'time' | 'location';
+export type ScheduleStatus = 'scheduled' | 'done' | 'deleted';
+
+export type Schedule = {
+ id: string;
+ user_id: string;
+ source_mode: ScheduleSourceMode;
+ schedule_type: ScheduleType;
+ status: ScheduleStatus;
+ title: string;
+ notes: string | null;
+ start_time: string | null;
+ end_time: string | null;
+ timezone: string | null;
+ location_name: string | null;
+ location_address: string | null;
+ latitude: number | null;
+ longitude: number | null;
+ geofence_radius_meters: number;
+ geofence_armed: boolean;
+ time_remind_offset_minutes: number;
+ time_triggered_at: string | null;
+ geo_triggered_at: string | null;
+ system_schedule_ref_id: string | null;
+ system_alarm_ref_id: string | null;
+ created_at: string;
+ updated_at: string;
+};
+
+export type ScheduleListQueryPayload = {
+ status: ScheduleStatus | null;
+ include_deleted: boolean;
+};
+
+export type ScheduleListQuery = WsRequest<'schedule.list.query', ScheduleListQueryPayload>;
+
+export type ScheduleListResultPayload = {
+ schedules: Schedule[];
+};
+
+export type ScheduleListResult = WsSuccess<'schedule.list.result', ScheduleListResultPayload>;
+
+export type ScheduleListError = WsFailure<'schedule.list.error'>;
+export type ScheduleListResponse = ScheduleListResult | ScheduleListError;
+
+export type ScheduleConflict = {
+ schedule_id: string;
+ title: string;
+ start_time: string;
+ end_time: string | null;
+};
+
+/** 草稿业务字段(创建/语音解析共用,不含 schedule_id / source_mode)。 */
+export type ScheduleDraftFields = {
+ schedule_type: ScheduleType;
+ title: string;
+ notes?: string | null;
+ start_time?: string | null;
+ end_time?: string | null;
+ timezone?: string | null;
+ location_name?: string | null;
+ location_address?: string | null;
+ latitude?: number | null;
+ longitude?: number | null;
+ geofence_radius_meters?: number | null;
+ geofence_armed?: boolean | null;
+ time_remind_offset_minutes?: number | null;
+};
+
+export type ScheduleUpsertPayload = ScheduleDraftFields & {
+ schedule_id?: string | null;
+ source_mode: ScheduleSourceMode;
+};
+
+export type ScheduleUpsertCommand = WsRequest<'schedule.upsert.command', ScheduleUpsertPayload>;
+
+export type ScheduleUpsertResultPayload = {
+ schedule_id: string;
+ schedule_type: ScheduleType;
+ status: ScheduleStatus;
+ conflicts: ScheduleConflict[];
+ geofence_armed: boolean;
+};
+
+export type ScheduleUpsertResult = WsSuccess<'schedule.upsert.result', ScheduleUpsertResultPayload>;
+
+export type ScheduleUpsertError = WsFailure<'schedule.upsert.error'>;
+export type ScheduleUpsertResponse = ScheduleUpsertResult | ScheduleUpsertError;
+
+/** 完成 / 恢复为已安排;与删除语义分离。 */
+export type ScheduleStatusUpdatePayload = {
+ schedule_id: string;
+ status: Extract;
+};
+
+export type ScheduleStatusUpdateCommand = WsRequest<
+ 'schedule.status.command',
+ ScheduleStatusUpdatePayload
+>;
+
+export type ScheduleStatusUpdateResultPayload = {
+ schedule_id: string;
+ status: ScheduleStatus;
+};
+
+export type ScheduleStatusUpdateResult = WsSuccess<
+ 'schedule.status.result',
+ ScheduleStatusUpdateResultPayload
+>;
+
+export type ScheduleStatusUpdateError = WsFailure<'schedule.status.error'>;
+export type ScheduleStatusUpdateResponse = ScheduleStatusUpdateResult | ScheduleStatusUpdateError;
+
+/** 客户端确认删除后,通知服务端取消监听与提醒(仅删除,不含完成)。 */
+export type ScheduleDeleted = {
+ type: 'schedule.deleted';
+ request_id: string;
+ schedule_id: string;
+ deleted: true;
+ timestamp: string;
+};
+
+export type ScheduleDeletedAck =
+ | { type: 'schedule.deleted.ack'; request_id?: string; schedule_id: string; ok: true }
+ | {
+ type: 'schedule.deleted.ack';
+ request_id?: string;
+ schedule_id: string;
+ ok: false;
+ error: ApiError;
+ };
diff --git a/frontend/src/contracts/session.ts b/frontend/src/contracts/session.ts
new file mode 100644
index 0000000..1029e6d
--- /dev/null
+++ b/frontend/src/contracts/session.ts
@@ -0,0 +1,34 @@
+import type { ApiError, WsFailure, WsRequest, WsSuccess } from './envelope';
+
+export type SessionHello = {
+ type: 'session.hello';
+ device_id: string;
+ app_version: string;
+};
+
+export type SessionReady = {
+ type: 'session.ready';
+ device_id: string;
+ /** Newer servers return this; the current single-user MVP server omits it. */
+ user_id?: string;
+ server_time: string;
+};
+
+export type SessionError = {
+ type: 'session.error';
+ ok: false;
+ error: ApiError;
+};
+
+export type LocationReportPayload = {
+ schedule_scope: 'current';
+ latitude: number;
+ longitude: number;
+ accuracy: number;
+ timestamp: string;
+};
+
+export type LocationReport = WsRequest<'location.report', LocationReportPayload>;
+
+export type LocationReportAck =
+ WsSuccess<'location.report.ack', null> | WsFailure<'location.report.ack'>;
diff --git a/frontend/src/contracts/transport.ts b/frontend/src/contracts/transport.ts
new file mode 100644
index 0000000..9530433
--- /dev/null
+++ b/frontend/src/contracts/transport.ts
@@ -0,0 +1,8 @@
+export type ConnectionStatus =
+ 'idle' | 'connecting' | 'ready' | 'reconnecting' | 'closed' | 'error';
+
+export type WsJsonMessage = {
+ type: string;
+ request_id?: string;
+ [key: string]: unknown;
+};
diff --git a/frontend/src/contracts/voice.ts b/frontend/src/contracts/voice.ts
new file mode 100644
index 0000000..730d98e
--- /dev/null
+++ b/frontend/src/contracts/voice.ts
@@ -0,0 +1,66 @@
+import type { ApiError, WsFailure, WsRequest, WsSuccess } from './envelope';
+import type { ScheduleDraftFields } from './schedule';
+
+export type VoiceStreamStartPayload = {
+ audio_format: 'pcm_s16le';
+ sample_rate_hz: number;
+ channels: number;
+};
+
+export type VoiceStreamStartCommand = WsRequest<'voice.stream.start', VoiceStreamStartPayload>;
+
+export type VoiceStreamEndPayload = {
+ stream_id: string;
+};
+
+export type VoiceStreamEndCommand = WsRequest<'voice.stream.end', VoiceStreamEndPayload>;
+
+export type VoiceStreamError = WsFailure<'voice.stream.error'>;
+
+export type VoiceStreamCancelPayload = {
+ stream_id: string;
+ job_id: string | null;
+};
+
+export type VoiceStreamCancelCommand = WsRequest<'voice.stream.cancel', VoiceStreamCancelPayload>;
+
+export type VoiceStreamCancelAck =
+ | WsSuccess<'voice.stream.cancelled', { stream_id: string }>
+ | VoiceStreamError
+ | WsFailure<'voice.stream.cancel'>;
+
+export type VoiceStreamStarted = WsSuccess<
+ 'voice.stream.started',
+ { stream_id: string; job_id: string }
+>;
+
+export type VoiceStreamEnded = WsSuccess<
+ 'voice.stream.ended',
+ { stream_id: string; job_id: string; status: 'processing' }
+>;
+
+export type VoiceStreamStartResponse = VoiceStreamStarted | VoiceStreamError;
+export type VoiceStreamEndResponse = VoiceStreamEnded | VoiceStreamError;
+
+export type VoiceParseDraft = Omit;
+
+export type VoiceParseReadyResult = {
+ type: 'voice.parse.result';
+ request_id: string;
+ job_id: string;
+ status: 'ready_for_confirmation';
+ draft: VoiceParseDraft;
+ missing_fields: string[];
+ ambiguous_fields: string[];
+ needs_confirmation: true;
+};
+
+export type VoiceParseFailedResult = {
+ type: 'voice.parse.result';
+ request_id: string;
+ job_id: string;
+ status: 'failed';
+ error: ApiError;
+};
+
+export type VoiceParseResultMessage = VoiceParseReadyResult | VoiceParseFailedResult;
diff --git a/frontend/src/dev/fakes/FakeWsServer.ts b/frontend/src/dev/fakes/FakeWsServer.ts
new file mode 100644
index 0000000..136323e
--- /dev/null
+++ b/frontend/src/dev/fakes/FakeWsServer.ts
@@ -0,0 +1,270 @@
+import type {
+ LocationReport,
+ LocationReportAck,
+ Schedule,
+ ScheduleDeleted,
+ ScheduleDeletedAck,
+ ScheduleListQuery,
+ ScheduleListResponse,
+ ScheduleStatusUpdateCommand,
+ ScheduleStatusUpdateResponse,
+ ScheduleUpsertCommand,
+ ScheduleUpsertResponse,
+ SessionHello,
+ SessionReady,
+ VoiceParseResultMessage,
+ VoiceStreamCancelCommand,
+ VoiceStreamEndCommand,
+ VoiceStreamStartCommand,
+ VoiceStreamStartResponse,
+ VoiceStreamEndResponse,
+ WsJsonMessage,
+} from '@/contracts';
+import type { WsClient } from '@/infrastructure/ws/WsClient';
+
+import { upsertSchedule } from './schedule/scheduleConflicts';
+import { createFakeSchedule } from './schedule/scheduleFactory';
+
+type FakeWsServerOptions = {
+ userId?: string;
+ seedSchedules?: Schedule[];
+};
+
+/**
+ * 进程内 Fake WS:只依赖 contracts + WsClient,供本地与测试使用。
+ */
+export class FakeWsServer {
+ private readonly schedules = new Map();
+ private readonly userId: string;
+ private client: WsClient | null = null;
+ private voiceJobCounter = 0;
+
+ constructor(options: FakeWsServerOptions = {}) {
+ this.userId = options.userId ?? 'user_fake_1';
+ for (const schedule of options.seedSchedules ?? []) {
+ this.schedules.set(schedule.id, schedule);
+ }
+ }
+
+ attach(client: WsClient): void {
+ this.client = client;
+ }
+
+ getUserId(): string {
+ return this.userId;
+ }
+
+ getSchedules(): Schedule[] {
+ return [...this.schedules.values()];
+ }
+
+ handleMessage = async (message: WsJsonMessage | ArrayBuffer): Promise => {
+ if (message instanceof ArrayBuffer) {
+ return;
+ }
+
+ switch (message.type) {
+ case 'session.hello':
+ this.handleSessionHello(message as SessionHello);
+ return;
+ case 'schedule.list.query':
+ this.handleList(message as ScheduleListQuery);
+ return;
+ case 'schedule.upsert.command':
+ this.handleUpsert(message as ScheduleUpsertCommand);
+ return;
+ case 'schedule.status.command':
+ this.handleStatusUpdate(message as ScheduleStatusUpdateCommand);
+ return;
+ case 'schedule.deleted':
+ this.handleDeleted(message as ScheduleDeleted);
+ return;
+ case 'location.report':
+ this.handleLocationReport(message as LocationReport);
+ return;
+ case 'voice.stream.start':
+ this.handleVoiceStart(message as VoiceStreamStartCommand);
+ return;
+ case 'voice.stream.end':
+ this.handleVoiceEnd(message as VoiceStreamEndCommand);
+ return;
+ case 'voice.stream.cancel':
+ this.handleVoiceCancel(message as VoiceStreamCancelCommand);
+ return;
+ default:
+ return;
+ }
+ };
+
+ private reply(message: WsJsonMessage): void {
+ this.client?.emitFromServer(message);
+ }
+
+ private handleSessionHello(message: SessionHello): void {
+ const ready: SessionReady = {
+ type: 'session.ready',
+ device_id: message.device_id,
+ user_id: this.userId,
+ server_time: new Date().toISOString(),
+ };
+ this.reply(ready);
+ }
+
+ private handleList(message: ScheduleListQuery): void {
+ const includeDeleted = message.payload.include_deleted;
+ const statusFilter = message.payload.status;
+ const schedules = [...this.schedules.values()].filter((item) => {
+ if (!includeDeleted && item.status === 'deleted') return false;
+ if (statusFilter && item.status !== statusFilter) return false;
+ return true;
+ });
+ const response: ScheduleListResponse = {
+ type: 'schedule.list.result',
+ request_id: message.request_id,
+ ok: true,
+ payload: { schedules },
+ };
+ this.reply(response);
+ }
+
+ private handleUpsert(message: ScheduleUpsertCommand): void {
+ const scheduleId = message.payload.schedule_id ?? `schedule_${Date.now()}`;
+ const current = [...this.schedules.values()];
+ const existing = this.schedules.get(scheduleId) ?? null;
+ const result = upsertSchedule(message, current, scheduleId);
+ const entity = createFakeSchedule({
+ draft: { ...message.payload, schedule_id: scheduleId },
+ scheduleId,
+ userId: this.userId,
+ status: result.payload.status,
+ geofenceArmed: result.payload.geofence_armed,
+ existing,
+ });
+ this.schedules.set(scheduleId, entity);
+ const response: ScheduleUpsertResponse = result;
+ this.reply(response);
+ this.reply({
+ type: 'schedule.updated',
+ schedule: entity,
+ });
+ }
+
+ private handleStatusUpdate(message: ScheduleStatusUpdateCommand): void {
+ const existing = this.schedules.get(message.payload.schedule_id);
+ if (!existing || existing.status === 'deleted') {
+ const response: ScheduleStatusUpdateResponse = {
+ type: 'schedule.status.error',
+ request_id: message.request_id,
+ ok: false,
+ error: {
+ code: 'schedule_not_found',
+ message: '日程不存在或已删除',
+ details: null,
+ },
+ };
+ this.reply(response);
+ return;
+ }
+
+ const next: Schedule = {
+ ...existing,
+ status: message.payload.status,
+ updated_at: new Date().toISOString(),
+ };
+ this.schedules.set(next.id, next);
+ const response: ScheduleStatusUpdateResponse = {
+ type: 'schedule.status.result',
+ request_id: message.request_id,
+ ok: true,
+ payload: { schedule_id: next.id, status: next.status },
+ };
+ this.reply(response);
+ this.reply({ type: 'schedule.updated', schedule: next });
+ }
+
+ private handleDeleted(message: ScheduleDeleted): void {
+ const existing = this.schedules.get(message.schedule_id);
+ if (existing) {
+ const next: Schedule = {
+ ...existing,
+ status: 'deleted',
+ updated_at: new Date().toISOString(),
+ };
+ this.schedules.set(message.schedule_id, next);
+ this.reply({ type: 'schedule.updated', schedule: next });
+ }
+ const ack: ScheduleDeletedAck = {
+ type: 'schedule.deleted.ack',
+ request_id: message.request_id,
+ schedule_id: message.schedule_id,
+ ok: true,
+ };
+ this.reply(ack);
+ }
+
+ private handleLocationReport(_message: LocationReport): void {
+ const ack: LocationReportAck = {
+ type: 'location.report.ack',
+ request_id: _message.request_id,
+ ok: true,
+ payload: null,
+ };
+ this.reply(ack);
+ }
+
+ private handleVoiceStart(message: VoiceStreamStartCommand): void {
+ this.voiceJobCounter += 1;
+ const streamId = `stream_${this.voiceJobCounter}`;
+ const jobId = `job_${this.voiceJobCounter}`;
+ const response: VoiceStreamStartResponse = {
+ type: 'voice.stream.started',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: streamId, job_id: jobId },
+ };
+ this.reply(response);
+ }
+
+ private handleVoiceEnd(message: VoiceStreamEndCommand): void {
+ const jobId = `job_${this.voiceJobCounter || 1}`;
+ const response: VoiceStreamEndResponse = {
+ type: 'voice.stream.ended',
+ request_id: message.request_id,
+ ok: true,
+ payload: {
+ stream_id: message.payload.stream_id,
+ job_id: jobId,
+ status: 'processing',
+ },
+ };
+ this.reply(response);
+
+ const parseResult: VoiceParseResultMessage = {
+ type: 'voice.parse.result',
+ request_id: message.request_id,
+ job_id: jobId,
+ status: 'ready_for_confirmation',
+ draft: {
+ schedule_type: 'time',
+ title: '语音创建的日程',
+ start_time: new Date(Date.now() + 3_600_000).toISOString(),
+ end_time: null,
+ timezone: 'Asia/Shanghai',
+ time_remind_offset_minutes: 0,
+ },
+ missing_fields: [],
+ ambiguous_fields: [],
+ needs_confirmation: true,
+ };
+ setTimeout(() => this.reply(parseResult), 0);
+ }
+
+ private handleVoiceCancel(message: VoiceStreamCancelCommand): void {
+ this.reply({
+ type: 'voice.stream.cancelled',
+ request_id: message.request_id,
+ ok: true,
+ payload: { stream_id: message.payload.stream_id },
+ });
+ }
+}
diff --git a/frontend/src/dev/fakes/schedule/scheduleConflicts.ts b/frontend/src/dev/fakes/schedule/scheduleConflicts.ts
new file mode 100644
index 0000000..d2612b5
--- /dev/null
+++ b/frontend/src/dev/fakes/schedule/scheduleConflicts.ts
@@ -0,0 +1,61 @@
+import type {
+ Schedule,
+ ScheduleConflict,
+ ScheduleUpsertCommand,
+ ScheduleUpsertResult,
+} from '@/contracts';
+
+/** 检测与现有日程的时间重叠冲突(排除自身与已删除项)。 */
+function findScheduleConflicts(
+ command: ScheduleUpsertCommand,
+ schedules: Schedule[],
+ currentScheduleId: string,
+): ScheduleConflict[] {
+ const { end_time: endTime, start_time: startTime } = command.payload;
+ if (!startTime) return [];
+
+ const start = new Date(startTime).getTime();
+ const end = endTime ? new Date(endTime).getTime() : start;
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
+
+ return schedules
+ .filter((item) => item.id !== currentScheduleId && item.status !== 'deleted' && item.start_time)
+ .filter((item) => {
+ const itemStart = new Date(item.start_time!).getTime();
+ const itemEnd = item.end_time ? new Date(item.end_time).getTime() : itemStart;
+ return (
+ Number.isFinite(itemStart) &&
+ Number.isFinite(itemEnd) &&
+ start <= itemEnd &&
+ itemStart <= end
+ );
+ })
+ .map((item) => ({
+ schedule_id: item.id,
+ title: item.title,
+ start_time: item.start_time!,
+ end_time: item.end_time,
+ }));
+}
+
+/** 拼装本地 upsert 结果(含冲突列表与 geofence 默认值)。 */
+export function upsertSchedule(
+ command: ScheduleUpsertCommand,
+ current: Schedule[],
+ scheduleId: string,
+): ScheduleUpsertResult {
+ const existingSchedule = current.find((item) => item.id === scheduleId);
+
+ return {
+ type: 'schedule.upsert.result',
+ request_id: command.request_id,
+ ok: true,
+ payload: {
+ schedule_id: scheduleId,
+ schedule_type: command.payload.schedule_type,
+ status: 'scheduled',
+ conflicts: findScheduleConflicts(command, current, scheduleId),
+ geofence_armed: command.payload.geofence_armed ?? existingSchedule?.geofence_armed ?? true,
+ },
+ };
+}
diff --git a/frontend/src/dev/fakes/schedule/scheduleFactory.ts b/frontend/src/dev/fakes/schedule/scheduleFactory.ts
new file mode 100644
index 0000000..89863e8
--- /dev/null
+++ b/frontend/src/dev/fakes/schedule/scheduleFactory.ts
@@ -0,0 +1,39 @@
+import type { Schedule, ScheduleStatus, ScheduleUpsertPayload } from '@/contracts';
+
+export function createFakeSchedule(input: {
+ draft: ScheduleUpsertPayload;
+ scheduleId: string;
+ userId: string;
+ status: ScheduleStatus;
+ geofenceArmed: boolean;
+ existing?: Schedule | null;
+}): Schedule {
+ const { draft, existing } = input;
+ const now = new Date().toISOString();
+
+ return {
+ id: input.scheduleId,
+ user_id: existing?.user_id ?? input.userId,
+ source_mode: draft.source_mode,
+ schedule_type: draft.schedule_type,
+ status: input.status,
+ title: draft.title,
+ notes: draft.notes ?? null,
+ start_time: draft.start_time ?? null,
+ end_time: draft.end_time ?? null,
+ timezone: draft.timezone ?? null,
+ location_name: draft.location_name ?? null,
+ location_address: draft.location_address ?? null,
+ latitude: draft.latitude ?? null,
+ longitude: draft.longitude ?? null,
+ geofence_radius_meters: draft.geofence_radius_meters ?? existing?.geofence_radius_meters ?? 100,
+ geofence_armed: input.geofenceArmed,
+ time_remind_offset_minutes: draft.time_remind_offset_minutes ?? 0,
+ time_triggered_at: existing?.time_triggered_at ?? null,
+ geo_triggered_at: existing?.geo_triggered_at ?? null,
+ system_schedule_ref_id: existing?.system_schedule_ref_id ?? null,
+ system_alarm_ref_id: existing?.system_alarm_ref_id ?? null,
+ created_at: existing?.created_at ?? now,
+ updated_at: now,
+ };
+}
diff --git a/frontend/src/features/assistant/components/AssistantChatSheet.styles.ts b/frontend/src/features/assistant/components/AssistantChatSheet.styles.ts
new file mode 100644
index 0000000..6b6fc6a
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantChatSheet.styles.ts
@@ -0,0 +1,95 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const assistantChatSheetStyles = StyleSheet.create({
+ sheet: {
+ maxHeight: '86%',
+ minHeight: 360,
+ overflow: 'hidden',
+ paddingBottom: 0,
+ paddingHorizontal: 0,
+ },
+ header: {
+ alignItems: 'center',
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ gap: 11,
+ marginBottom: 0,
+ paddingBottom: 13,
+ paddingHorizontal: 18,
+ paddingTop: 14,
+ },
+ mark: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderColor: colors.lime,
+ borderRadius: 13,
+ borderWidth: 1.5,
+ height: 36,
+ justifyContent: 'center',
+ width: 36,
+ },
+ headerCopy: { flex: 1, minWidth: 0 },
+ title: { color: colors.ink, fontSize: 16, fontWeight: '800' },
+ subtitle: { color: colors.sub, fontSize: 11, marginTop: 3 },
+ list: {
+ backgroundColor: colors.background,
+ flexGrow: 1,
+ flexShrink: 1,
+ },
+ listContent: {
+ gap: 10,
+ paddingBottom: 16,
+ paddingHorizontal: 16,
+ paddingTop: 14,
+ },
+ listContentEmpty: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ },
+ empty: {
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ paddingVertical: 28,
+ },
+ emptyTitle: { color: colors.deep, fontSize: 15, fontWeight: '800' },
+ emptyHint: {
+ color: colors.sub,
+ fontSize: 12,
+ lineHeight: 18,
+ marginTop: 8,
+ textAlign: 'center',
+ },
+ rowUser: { alignItems: 'flex-end' },
+ bubbleUser: {
+ backgroundColor: colors.deep,
+ borderRadius: 16,
+ borderBottomRightRadius: 4,
+ maxWidth: '82%',
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ bubbleUserText: { color: colors.surface, fontSize: 14, lineHeight: 20 },
+ rowAssistant: { alignItems: 'flex-start' },
+ bubbleAssistant: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 16,
+ borderBottomLeftRadius: 4,
+ borderWidth: 1,
+ maxWidth: '88%',
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ bubbleAssistantText: { color: colors.ink, fontSize: 14, lineHeight: 20 },
+ draftSlot: { width: '100%' },
+ composer: {
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderTopColor: colors.line,
+ borderTopWidth: 1,
+ paddingTop: 12,
+ },
+});
diff --git a/frontend/src/features/assistant/components/AssistantChatSheet.tsx b/frontend/src/features/assistant/components/AssistantChatSheet.tsx
new file mode 100644
index 0000000..e62c320
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantChatSheet.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useRef } from 'react';
+import { ScrollView, Text, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { BottomSheetFrame } from '@/shared/components/BottomSheetFrame';
+import { colors } from '@/shared/theme';
+
+import type { AssistantMessage, AssistantMessageAction } from '../types';
+import { assistantChatSheetStyles as styles } from './AssistantChatSheet.styles';
+import { AssistantDraftCard } from './AssistantDraftCard';
+import { TempoAssistantIcon } from './TempoAssistantIcon';
+import { VoiceHoldButton } from './VoiceHoldButton';
+
+export function AssistantChatSheet({
+ isProcessing = false,
+ messages,
+ onAction,
+ onClose,
+ onVoiceCancel,
+ onVoiceEnd,
+ onVoiceStart,
+ visible,
+}: {
+ isProcessing?: boolean;
+ messages: AssistantMessage[];
+ onAction: (messageId: string, action: AssistantMessageAction) => void;
+ onClose: () => void;
+ onVoiceCancel?: () => void;
+ onVoiceEnd: () => void;
+ onVoiceStart?: () => void;
+ visible: boolean;
+}) {
+ const listRef = useRef(null);
+ const insets = useSafeAreaInsets();
+
+ useEffect(() => {
+ if (!visible) return;
+ const timer = setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 60);
+ return () => clearTimeout(timer);
+ }, [messages, visible]);
+
+ return (
+
+
+
+
+
+ 语音助手
+ 说一句话,我整理成日程给你确认
+
+ >
+ }
+ headerStyle={styles.header}
+ onClose={onClose}
+ sheetStyle={styles.sheet}
+ visible={visible}
+ >
+ listRef.current?.scrollToEnd({ animated: true })}
+ showsVerticalScrollIndicator={false}
+ style={styles.list}
+ >
+ {messages.length === 0 ? (
+
+
+ {isProcessing ? '正在整理录音…' : '等你说第一句话'}
+
+
+ {isProcessing
+ ? '识别完成后会生成一张待确认的日程草稿'
+ : '松开手指后,识别到的内容会整理成日程草稿显示在这里'}
+
+
+ ) : null}
+ {messages.map((item) => {
+ if (item.role === 'user') {
+ return (
+
+
+ {item.text}
+
+
+ );
+ }
+
+ if (item.draft) {
+ return (
+
+ onAction(item.id, action)}
+ />
+
+ );
+ }
+
+ return (
+
+
+ {item.text}
+
+
+ );
+ })}
+ {isProcessing && messages.length > 0 ? (
+
+
+
+ 正在整理录音…
+
+
+
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/assistant/components/AssistantDock.styles.ts b/frontend/src/features/assistant/components/AssistantDock.styles.ts
new file mode 100644
index 0000000..76f3180
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantDock.styles.ts
@@ -0,0 +1,20 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const assistantDockStyles = StyleSheet.create({
+ dock: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(244,245,241,0.97)',
+ borderTopColor: colors.line,
+ borderTopWidth: 1,
+ bottom: 0,
+ justifyContent: 'flex-end',
+ left: 0,
+ paddingBottom: 10,
+ paddingTop: 6,
+ position: 'absolute',
+ right: 0,
+ zIndex: 20,
+ },
+});
diff --git a/frontend/src/features/assistant/components/AssistantDock.tsx b/frontend/src/features/assistant/components/AssistantDock.tsx
new file mode 100644
index 0000000..0cf7342
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantDock.tsx
@@ -0,0 +1,32 @@
+import { View } from 'react-native';
+
+import { assistantDockStyles as styles } from './AssistantDock.styles';
+import { VoiceHoldButton } from './VoiceHoldButton';
+
+/** 日程页底部语音入口;助手弹层打开时隐藏,改由弹层内的按钮说话。 */
+export function AssistantDock({
+ hidden = false,
+ onOpen,
+ onVoiceCancel,
+ onVoiceEnd,
+ onVoiceStart,
+}: {
+ hidden?: boolean;
+ onOpen: () => void;
+ onVoiceCancel?: () => void;
+ onVoiceEnd: () => void;
+ onVoiceStart?: () => void;
+}) {
+ if (hidden) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/features/assistant/components/AssistantDraftCard.styles.ts b/frontend/src/features/assistant/components/AssistantDraftCard.styles.ts
new file mode 100644
index 0000000..669346b
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantDraftCard.styles.ts
@@ -0,0 +1,134 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const assistantDraftCardStyles = StyleSheet.create({
+ card: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 16,
+ borderWidth: 1,
+ overflow: 'hidden',
+ },
+ cardResolved: {
+ backgroundColor: '#FBFCFA',
+ },
+ head: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ gap: 11,
+ paddingHorizontal: 13,
+ paddingTop: 13,
+ },
+ icon: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 10,
+ height: 34,
+ justifyContent: 'center',
+ width: 34,
+ },
+ iconResolved: {
+ backgroundColor: '#E7EDE8',
+ },
+ copy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ titleRow: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ gap: 8,
+ justifyContent: 'space-between',
+ },
+ title: {
+ color: colors.ink,
+ flex: 1,
+ fontSize: 15,
+ fontWeight: '800',
+ lineHeight: 20,
+ },
+ when: {
+ color: colors.ink,
+ fontSize: 12,
+ fontWeight: '600',
+ lineHeight: 17,
+ marginTop: 5,
+ },
+ meta: {
+ color: colors.sub,
+ fontSize: 11,
+ lineHeight: 16,
+ marginTop: 3,
+ },
+ clarification: {
+ color: '#A16142',
+ fontSize: 11,
+ lineHeight: 16,
+ marginTop: 6,
+ },
+ chip: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 8,
+ flexDirection: 'row',
+ gap: 3,
+ paddingHorizontal: 7,
+ paddingVertical: 4,
+ },
+ chipAdded: {
+ backgroundColor: '#E4EEE6',
+ },
+ chipDismissed: {
+ backgroundColor: '#EEF0ED',
+ },
+ chipText: {
+ color: '#5C7045',
+ fontSize: 9,
+ fontWeight: '800',
+ },
+ chipTextAdded: {
+ color: '#63866E',
+ },
+ chipTextDismissed: {
+ color: colors.muted,
+ },
+ actions: {
+ borderTopColor: colors.line,
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ gap: 8,
+ marginTop: 13,
+ paddingHorizontal: 13,
+ paddingVertical: 11,
+ },
+ action: {
+ alignItems: 'center',
+ borderRadius: 11,
+ flexDirection: 'row',
+ gap: 5,
+ height: 40,
+ justifyContent: 'center',
+ },
+ actionDismiss: {
+ backgroundColor: '#E9ECE8',
+ flex: 1,
+ },
+ actionDismissText: {
+ color: colors.ink,
+ fontSize: 12,
+ fontWeight: '700',
+ },
+ actionConfirm: {
+ backgroundColor: colors.lime,
+ flex: 1.4,
+ },
+ actionConfirmText: {
+ color: colors.deep,
+ fontSize: 12,
+ fontWeight: '800',
+ },
+ bottomPad: {
+ height: 13,
+ },
+});
diff --git a/frontend/src/features/assistant/components/AssistantDraftCard.tsx b/frontend/src/features/assistant/components/AssistantDraftCard.tsx
new file mode 100644
index 0000000..654b343
--- /dev/null
+++ b/frontend/src/features/assistant/components/AssistantDraftCard.tsx
@@ -0,0 +1,93 @@
+import { CalendarClock, Check, CheckCircle2 } from 'lucide-react-native';
+import { Pressable, Text, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+import type { AssistantDraft, AssistantMessageAction } from '../types';
+import { assistantDraftCardStyles as styles } from './AssistantDraftCard.styles';
+
+const CHIP_LABEL = {
+ pending: '待确认',
+ added: '已加入日程',
+ dismissed: '已忽略',
+} as const;
+
+/** 助手把语音解析成的日程草稿:先看清内容,再决定是否加入日程。 */
+export function AssistantDraftCard({
+ actions,
+ draft,
+ onAction,
+}: {
+ actions?: AssistantMessageAction[];
+ draft: AssistantDraft;
+ onAction: (action: AssistantMessageAction) => void;
+}) {
+ const state = draft.state ?? 'pending';
+ const resolved = state !== 'pending';
+ const showActions = state === 'pending' && actions && actions.length > 0;
+
+ return (
+
+
+
+
+
+
+
+ {draft.title}
+
+ {state === 'added' ? (
+
+ ) : null}
+
+ {CHIP_LABEL[state]}
+
+
+
+ {draft.whenLabel}
+ {draft.metaLabel ? {draft.metaLabel} : null}
+ {draft.clarificationLabel ? (
+
+ {draft.clarificationLabel}
+
+ ) : null}
+
+
+ {showActions ? (
+
+ {actions.map((action) => {
+ const confirm = action.kind === 'confirm';
+ return (
+ onAction(action)}
+ style={[styles.action, confirm ? styles.actionConfirm : styles.actionDismiss]}
+ >
+ {confirm ? : null}
+
+ {action.label}
+
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/features/assistant/components/TempoAssistantIcon.tsx b/frontend/src/features/assistant/components/TempoAssistantIcon.tsx
new file mode 100644
index 0000000..6006a6a
--- /dev/null
+++ b/frontend/src/features/assistant/components/TempoAssistantIcon.tsx
@@ -0,0 +1,24 @@
+import Svg, { Path } from 'react-native-svg';
+
+export function TempoAssistantIcon({
+ color = '#D9F65A',
+ size = 22,
+}: {
+ color?: string;
+ size?: number;
+}) {
+ return (
+
+ );
+}
diff --git a/frontend/src/features/assistant/components/VoiceHoldButton.styles.ts b/frontend/src/features/assistant/components/VoiceHoldButton.styles.ts
new file mode 100644
index 0000000..f4e1932
--- /dev/null
+++ b/frontend/src/features/assistant/components/VoiceHoldButton.styles.ts
@@ -0,0 +1,54 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const voiceHoldButtonStyles = StyleSheet.create({
+ wrap: {
+ alignItems: 'center',
+ alignSelf: 'center',
+ gap: 7,
+ },
+ hintSlot: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: 15,
+ },
+ hint: {
+ color: colors.muted,
+ fontSize: 11,
+ fontWeight: '700',
+ },
+ hintCancel: {
+ color: colors.coral,
+ },
+ button: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderColor: 'transparent',
+ borderRadius: 16,
+ borderWidth: 1,
+ height: 52,
+ justifyContent: 'center',
+ width: 52,
+ },
+ buttonListening: {
+ borderColor: 'rgba(215,243,106,0.55)',
+ width: 96,
+ },
+ buttonCancel: {
+ backgroundColor: colors.peach,
+ borderColor: '#F1C6B6',
+ width: 76,
+ },
+ wave: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 4,
+ height: 24,
+ },
+ waveBar: {
+ backgroundColor: colors.lime,
+ borderRadius: 999,
+ width: 3.5,
+ },
+});
diff --git a/frontend/src/features/assistant/components/VoiceHoldButton.tsx b/frontend/src/features/assistant/components/VoiceHoldButton.tsx
new file mode 100644
index 0000000..c80e211
--- /dev/null
+++ b/frontend/src/features/assistant/components/VoiceHoldButton.tsx
@@ -0,0 +1,177 @@
+import { useEffect, useRef, useState } from 'react';
+import {
+ Animated,
+ Easing,
+ Platform,
+ Pressable,
+ Text,
+ Vibration,
+ View,
+ type GestureResponderEvent,
+ type StyleProp,
+ type ViewStyle,
+} from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+import { TempoAssistantIcon } from './TempoAssistantIcon';
+import { voiceHoldButtonStyles as styles } from './VoiceHoldButton.styles';
+
+const WAVE_BAR_HEIGHTS = [11, 18, 24, 16, 12] as const;
+const CANCEL_DISTANCE = 72;
+const HOLD_DELAY_MS = 320;
+const HAPTIC_DURATION_MS = 35;
+
+/** 轻点打开助手;按住说话、松开发送、上滑取消。 */
+export function VoiceHoldButton({
+ iconSize = 22,
+ onPress,
+ onVoiceCancel,
+ onVoiceEnd,
+ onVoiceStart,
+ style,
+}: {
+ iconSize?: number;
+ onPress?: () => void;
+ onVoiceCancel?: () => void;
+ onVoiceEnd: () => void;
+ onVoiceStart?: () => void;
+ style?: StyleProp;
+}) {
+ const [isListening, setIsListening] = useState(false);
+ const [willCancel, setWillCancel] = useState(false);
+ const [waveValues] = useState(() => WAVE_BAR_HEIGHTS.map(() => new Animated.Value(0.55)));
+ const recordingStartedRef = useRef(false);
+ const longPressTriggeredRef = useRef(false);
+ const cancelHapticFiredRef = useRef(false);
+ const willCancelRef = useRef(false);
+ const startYRef = useRef(0);
+
+ const handlePressIn = (event: GestureResponderEvent) => {
+ startYRef.current = event.nativeEvent.pageY;
+ recordingStartedRef.current = false;
+ longPressTriggeredRef.current = false;
+ cancelHapticFiredRef.current = false;
+ willCancelRef.current = false;
+ setWillCancel(false);
+ };
+
+ const handleLongPress = () => {
+ longPressTriggeredRef.current = true;
+ recordingStartedRef.current = true;
+ setIsListening(true);
+ Vibration.vibrate(HAPTIC_DURATION_MS);
+ onVoiceStart?.();
+ };
+
+ const handleTouchMove = (event: GestureResponderEvent) => {
+ if (!recordingStartedRef.current) return;
+ const shouldCancel = event.nativeEvent.pageY - startYRef.current < -CANCEL_DISTANCE;
+ if (shouldCancel && !cancelHapticFiredRef.current) {
+ cancelHapticFiredRef.current = true;
+ Vibration.vibrate(HAPTIC_DURATION_MS);
+ }
+ willCancelRef.current = shouldCancel;
+ setWillCancel(shouldCancel);
+ };
+
+ const handlePressOut = () => {
+ if (!recordingStartedRef.current) return;
+ const cancelled = willCancelRef.current;
+ recordingStartedRef.current = false;
+ willCancelRef.current = false;
+ setIsListening(false);
+ setWillCancel(false);
+ if (cancelled) onVoiceCancel?.();
+ else onVoiceEnd();
+ };
+
+ useEffect(() => {
+ if (!isListening) {
+ waveValues.forEach((value) => {
+ value.stopAnimation();
+ value.setValue(0.55);
+ });
+ return;
+ }
+
+ const animation = Animated.loop(
+ Animated.stagger(
+ 60,
+ waveValues.map((value) =>
+ Animated.sequence([
+ Animated.timing(value, {
+ duration: 170,
+ easing: Easing.inOut(Easing.ease),
+ toValue: 1,
+ useNativeDriver: Platform.OS !== 'web',
+ }),
+ Animated.timing(value, {
+ duration: 170,
+ easing: Easing.inOut(Easing.ease),
+ toValue: 0.5,
+ useNativeDriver: Platform.OS !== 'web',
+ }),
+ ]),
+ ),
+ ),
+ );
+
+ animation.start();
+ return () => {
+ animation.stop();
+ waveValues.forEach((value) => value.setValue(0.55));
+ };
+ }, [isListening, waveValues]);
+
+ const hint = willCancel ? '松开手指取消发送' : isListening ? '松开发送 · 上滑取消' : '';
+
+ return (
+
+ {hint ? (
+
+ {hint}
+
+ ) : null}
+ {
+ if (!longPressTriggeredRef.current) onPress?.();
+ }}
+ onPressIn={handlePressIn}
+ onPressOut={handlePressOut}
+ onTouchMove={handleTouchMove}
+ style={[
+ styles.button,
+ isListening && !willCancel && styles.buttonListening,
+ willCancel && styles.buttonCancel,
+ ]}
+ >
+ {isListening && !willCancel ? (
+
+ {waveValues.map((value, index) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/features/assistant/data/VoiceStreamPort.ts b/frontend/src/features/assistant/data/VoiceStreamPort.ts
new file mode 100644
index 0000000..47f963c
--- /dev/null
+++ b/frontend/src/features/assistant/data/VoiceStreamPort.ts
@@ -0,0 +1,154 @@
+import type {
+ VoiceParseReadyResult,
+ VoiceParseResultMessage,
+ VoiceStreamCancelCommand,
+ VoiceStreamCancelAck,
+ VoiceStreamEndCommand,
+ VoiceStreamStartCommand,
+ VoiceStreamStartResponse,
+ VoiceStreamEndResponse,
+ WsJsonMessage,
+} from '@/contracts';
+import { nextRequestId } from '@/shared/utils/requestId';
+
+export type VoiceParseOutcome = Pick<
+ VoiceParseReadyResult,
+ 'draft' | 'missing_fields' | 'ambiguous_fields'
+>;
+
+export type VoiceStreamPort = {
+ start(): Promise<{ streamId: string; jobId: string; resultRequestId: string }>;
+ sendAudioChunk(data: ArrayBuffer): void;
+ end(streamId: string, jobId: string, resultRequestId: string): Promise;
+ cancel(streamId: string, jobId: string | null): Promise;
+};
+
+/** Composition port; concrete microphone adapters live outside the feature. */
+export type VoiceRecorder = {
+ start(onChunk: (chunk: ArrayBuffer) => void): Promise;
+ stop(): Promise;
+ cancel(): Promise;
+};
+
+/** assistant data 层所需的最小传输面;由 app 注入 WsClient。 */
+export type VoiceTransport = {
+ onMessage(listener: (message: WsJsonMessage | ArrayBuffer) => void): () => void;
+ request(
+ message: WsJsonMessage & { request_id: string },
+ isMatch?: (response: WsJsonMessage) => boolean,
+ ): Promise;
+ sendBinary(data: ArrayBuffer): void;
+};
+
+export class WsVoiceStreamPort implements VoiceStreamPort {
+ constructor(private readonly client: VoiceTransport) {}
+
+ async start(): Promise<{ streamId: string; jobId: string; resultRequestId: string }> {
+ const request: VoiceStreamStartCommand = {
+ type: 'voice.stream.start',
+ request_id: nextRequestId('req_voice_start'),
+ payload: {
+ audio_format: 'pcm_s16le',
+ sample_rate_hz: 16000,
+ channels: 1,
+ },
+ };
+ const response = await this.client.request(request, (message) => {
+ return (
+ message.request_id === request.request_id &&
+ (message.type === 'voice.stream.started' || message.type === 'voice.stream.error')
+ );
+ });
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+ return {
+ streamId: response.payload.stream_id,
+ jobId: response.payload.job_id,
+ resultRequestId: request.request_id,
+ };
+ }
+
+ sendAudioChunk(data: ArrayBuffer): void {
+ if (data.byteLength === 0 || data.byteLength % 2 !== 0) {
+ throw new Error('PCM 音频帧不能为空且必须按 16 位采样对齐');
+ }
+ this.client.sendBinary(data);
+ }
+
+ async end(streamId: string, jobId: string, resultRequestId: string): Promise {
+ const request: VoiceStreamEndCommand = {
+ type: 'voice.stream.end',
+ request_id: nextRequestId('req_voice_end'),
+ payload: { stream_id: streamId },
+ };
+
+ let cleanupParseListener = () => undefined;
+ const parseResult = new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ cleanupParseListener();
+ reject(new Error('voice.parse.result timed out'));
+ }, 20_000);
+ const unsubscribe = this.client.onMessage((message: WsJsonMessage | ArrayBuffer) => {
+ if (message instanceof ArrayBuffer) return;
+ if (message.type !== 'voice.parse.result') return;
+ const result = message as VoiceParseResultMessage;
+ // The MVP backend correlates the final parse result to voice.stream.start,
+ // while voice.stream.ended is correlated to this end request.
+ if (result.job_id !== jobId || result.request_id !== resultRequestId) return;
+ clearTimeout(timer);
+ cleanupParseListener();
+ if (result.status === 'failed') {
+ reject(new Error(result.error.message));
+ return;
+ }
+ resolve({
+ draft: result.draft,
+ missing_fields: result.missing_fields,
+ ambiguous_fields: result.ambiguous_fields,
+ });
+ });
+ cleanupParseListener = () => {
+ clearTimeout(timer);
+ unsubscribe();
+ };
+ });
+
+ let endResponse: VoiceStreamEndResponse;
+ try {
+ endResponse = await this.client.request(request, (message) => {
+ return (
+ message.request_id === request.request_id &&
+ (message.type === 'voice.stream.ended' || message.type === 'voice.stream.error')
+ );
+ });
+ } catch (error) {
+ cleanupParseListener();
+ throw error;
+ }
+ if (!endResponse.ok) {
+ cleanupParseListener();
+ throw new Error(endResponse.error.message);
+ }
+ return parseResult;
+ }
+
+ async cancel(streamId: string, jobId: string | null): Promise {
+ const request: VoiceStreamCancelCommand = {
+ type: 'voice.stream.cancel',
+ request_id: nextRequestId('req_voice_cancel'),
+ payload: { stream_id: streamId, job_id: jobId },
+ };
+ const response = await this.client.request(request, (message) => {
+ return (
+ message.request_id === request.request_id &&
+ (message.type === 'voice.stream.cancelled' ||
+ message.type === 'voice.stream.error' ||
+ message.type === 'voice.stream.cancel')
+ );
+ });
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+ }
+}
diff --git a/frontend/src/features/assistant/hooks/useAssistantSession.ts b/frontend/src/features/assistant/hooks/useAssistantSession.ts
new file mode 100644
index 0000000..70ffe13
--- /dev/null
+++ b/frontend/src/features/assistant/hooks/useAssistantSession.ts
@@ -0,0 +1,256 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+import type { VoiceParseDraft } from '@/contracts';
+
+import {
+ WsVoiceStreamPort,
+ type VoiceRecorder,
+ type VoiceParseOutcome,
+ type VoiceStreamPort,
+ type VoiceTransport,
+} from '../data/VoiceStreamPort';
+import type { AssistantMessage, AssistantMessageAction } from '../types';
+
+function formatWhenLabel(draft: VoiceParseDraft): string {
+ if (draft.start_time) {
+ const date = new Date(draft.start_time);
+ if (!Number.isNaN(date.getTime())) {
+ return `${date.getMonth() + 1}月${date.getDate()}日 ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
+ }
+ }
+ if (draft.location_name || draft.location_address) {
+ return draft.location_name ?? draft.location_address ?? '地点提醒';
+ }
+ return '待确认时间';
+}
+
+const FIELD_LABELS: Record = {
+ title: '标题',
+ schedule_type: '提醒类型',
+ start_time: '开始时间',
+ end_time: '结束时间',
+ location_name: '地点名称',
+ location_address: '地点地址',
+};
+
+function formatFieldName(field: string): string {
+ return FIELD_LABELS[field] ?? field;
+}
+
+function formatClarificationLabel(result: VoiceParseOutcome): string | undefined {
+ const missing = result.missing_fields.map(formatFieldName);
+ const ambiguous = result.ambiguous_fields.map(formatFieldName);
+ const parts: string[] = [];
+ if (missing.length > 0) parts.push(`需要补充:${missing.join('、')}`);
+ if (ambiguous.length > 0) parts.push(`需要确认:${ambiguous.join('、')}`);
+ return parts.length > 0 ? parts.join(';') : undefined;
+}
+
+const unavailableRecorder: VoiceRecorder = {
+ async start() {
+ throw new Error('录音适配器未注入');
+ },
+ async stop() {},
+ async cancel() {},
+};
+
+type ActiveVoiceStream = {
+ streamId: string;
+ jobId: string;
+ resultRequestId: string;
+ port: VoiceStreamPort;
+ recorder: VoiceRecorder;
+ hasAudio: boolean;
+};
+
+async function cancelVoiceStream(stream: ActiveVoiceStream): Promise {
+ await stream.recorder.cancel().catch(() => undefined);
+ await stream.port.cancel(stream.streamId, stream.jobId).catch(() => undefined);
+}
+
+/**
+ * 助手会话:只产出 VoiceParseDraft,不依赖 schedule model。
+ * AppShell 负责映射为 ScheduleDraft 并写入日程。
+ */
+export function useAssistantSession(options: {
+ client: VoiceTransport | null;
+ onConfirmDraft: (draft: VoiceParseDraft) => Promise;
+ /** App/native hosts may inject an Expo Audio backed recorder. */
+ recorder?: VoiceRecorder;
+}) {
+ const voice = useMemo(
+ () => (options.client ? new WsVoiceStreamPort(options.client) : null),
+ [options.client],
+ );
+ const recorder = useMemo(() => options.recorder ?? unavailableRecorder, [options.recorder]);
+ const [messages, setMessages] = useState([]);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const activeStreamRef = useRef(null);
+ const startPromiseRef = useRef | null>(null);
+ const [pendingDrafts, setPendingDrafts] = useState>({});
+
+ useEffect(() => {
+ return () => {
+ const cancel = (stream: ActiveVoiceStream) => {
+ if (activeStreamRef.current === stream) activeStreamRef.current = null;
+ void cancelVoiceStream(stream);
+ };
+ const starting = startPromiseRef.current;
+ if (starting) {
+ void starting.then(cancel).catch(() => undefined);
+ return;
+ }
+ const stream = activeStreamRef.current;
+ if (stream) cancel(stream);
+ };
+ }, []);
+
+ const handleVoiceStart = useCallback(async () => {
+ if (!voice) throw new Error('语音通道未就绪');
+ if (activeStreamRef.current || startPromiseRef.current) {
+ throw new Error('语音录音已经开始');
+ }
+
+ const startOperation = (async (): Promise => {
+ const started = await voice.start();
+ const stream: ActiveVoiceStream = { ...started, port: voice, recorder, hasAudio: false };
+ try {
+ await recorder.start((chunk) => {
+ voice.sendAudioChunk(chunk);
+ stream.hasAudio = true;
+ });
+ } catch (error) {
+ await voice.cancel(started.streamId, started.jobId).catch(() => undefined);
+ throw error;
+ }
+ activeStreamRef.current = stream;
+ return stream;
+ })();
+ startPromiseRef.current = startOperation;
+ try {
+ await startOperation;
+ } finally {
+ if (startPromiseRef.current === startOperation) {
+ startPromiseRef.current = null;
+ }
+ }
+ }, [recorder, voice]);
+
+ const handleVoiceEnd = useCallback(async () => {
+ const starting = startPromiseRef.current;
+ if (starting) {
+ try {
+ await starting;
+ } catch {
+ // handleVoiceStart owns reporting its startup failure.
+ return;
+ }
+ }
+ const stream = activeStreamRef.current;
+ if (!stream) return;
+ activeStreamRef.current = null;
+ setIsProcessing(true);
+ try {
+ try {
+ await stream.recorder.stop();
+ } catch (error) {
+ await stream.port.cancel(stream.streamId, stream.jobId).catch(() => undefined);
+ throw error;
+ }
+ if (!stream.hasAudio) {
+ await stream.port.cancel(stream.streamId, stream.jobId).catch(() => undefined);
+ return;
+ }
+ const parseResult = await stream.port.end(
+ stream.streamId,
+ stream.jobId,
+ stream.resultRequestId,
+ );
+ const messageId = `msg_${Date.now()}`;
+ const clarificationLabel = formatClarificationLabel(parseResult);
+ const canConfirm = !clarificationLabel;
+ setPendingDrafts((current) => ({ ...current, [messageId]: parseResult }));
+ setMessages((current) => [
+ ...current,
+ {
+ id: messageId,
+ role: 'assistant',
+ createdAt: Date.now(),
+ draft: {
+ title: parseResult.draft.title,
+ whenLabel: formatWhenLabel(parseResult.draft),
+ metaLabel: parseResult.draft.location_name ?? undefined,
+ clarificationLabel,
+ state: 'pending',
+ },
+ actions: canConfirm
+ ? [
+ { id: `${messageId}_confirm`, label: '确认添加', kind: 'confirm' },
+ { id: `${messageId}_dismiss`, label: '忽略', kind: 'dismiss' },
+ ]
+ : [{ id: `${messageId}_dismiss`, label: '忽略', kind: 'dismiss' }],
+ },
+ ]);
+ } finally {
+ setIsProcessing(false);
+ }
+ }, []);
+
+ const handleVoiceCancel = useCallback(() => {
+ setIsProcessing(false);
+ const cancel = (stream: ActiveVoiceStream) => {
+ if (activeStreamRef.current === stream) activeStreamRef.current = null;
+ return cancelVoiceStream(stream);
+ };
+
+ const starting = startPromiseRef.current;
+ if (starting) {
+ void starting.then(cancel).catch(() => undefined);
+ return;
+ }
+ const stream = activeStreamRef.current;
+ if (stream) void cancel(stream);
+ }, []);
+
+ const handleAction = useCallback(
+ async (messageId: string, action: AssistantMessageAction) => {
+ if (action.kind === 'confirm') {
+ const result = pendingDrafts[messageId];
+ if (!result) return;
+ if (result.missing_fields.length > 0 || result.ambiguous_fields.length > 0) {
+ throw new Error('语音草稿仍有待确认字段,不能直接加入日程');
+ }
+ await options.onConfirmDraft(result.draft);
+ }
+ setPendingDrafts((current) => {
+ const next = { ...current };
+ delete next[messageId];
+ return next;
+ });
+ setMessages((current) =>
+ current.map((item) =>
+ item.id === messageId && item.draft
+ ? {
+ ...item,
+ actions: undefined,
+ draft: {
+ ...item.draft,
+ state: action.kind === 'confirm' ? 'added' : 'dismissed',
+ },
+ }
+ : item,
+ ),
+ );
+ },
+ [options, pendingDrafts],
+ );
+
+ return {
+ messages,
+ isProcessing,
+ handleVoiceStart,
+ handleVoiceEnd,
+ handleVoiceCancel,
+ handleAction,
+ };
+}
diff --git a/frontend/src/features/assistant/index.ts b/frontend/src/features/assistant/index.ts
new file mode 100644
index 0000000..4418d06
--- /dev/null
+++ b/frontend/src/features/assistant/index.ts
@@ -0,0 +1,5 @@
+export { AssistantChatSheet } from './components/AssistantChatSheet';
+export { AssistantDock } from './components/AssistantDock';
+export { useAssistantSession } from './hooks/useAssistantSession';
+export type { VoiceRecorder } from './data/VoiceStreamPort';
+export type { AssistantMessage, AssistantMessageAction } from './types';
diff --git a/frontend/src/features/assistant/types.ts b/frontend/src/features/assistant/types.ts
new file mode 100644
index 0000000..8c3b99c
--- /dev/null
+++ b/frontend/src/features/assistant/types.ts
@@ -0,0 +1,31 @@
+export type AssistantRole = 'user' | 'assistant';
+
+export type AssistantMessageAction = {
+ id: string;
+ label: string;
+ kind: 'confirm' | 'dismiss';
+};
+
+/** 助手解析出的草稿状态:待确认 / 已加入日程 / 已忽略。 */
+export type AssistantDraftState = 'pending' | 'added' | 'dismissed';
+
+/** 语音被解析成的日程草稿,用卡片展示而不是塞进对话文字里。 */
+export type AssistantDraft = {
+ title: string;
+ whenLabel: string;
+ metaLabel?: string;
+ /** 解析字段不完整时只展示草稿,不允许直接写入日程。 */
+ clarificationLabel?: string;
+ state?: AssistantDraftState;
+};
+
+export type AssistantMessage = {
+ id: string;
+ role: AssistantRole;
+ createdAt: number;
+ /** 纯文本内容;带 draft 的助手消息可以只给卡片。 */
+ text?: string;
+ draft?: AssistantDraft;
+ /** 助手消息可附带确认操作;点过后清空。 */
+ actions?: AssistantMessageAction[];
+};
diff --git a/frontend/src/features/reminder/hooks/useAlarmPermissionsOnLaunch.ts b/frontend/src/features/reminder/hooks/useAlarmPermissionsOnLaunch.ts
new file mode 100644
index 0000000..488ad29
--- /dev/null
+++ b/frontend/src/features/reminder/hooks/useAlarmPermissionsOnLaunch.ts
@@ -0,0 +1,157 @@
+import { useEffect, useRef } from 'react';
+import { AppState, type AppStateStatus, Platform } from 'react-native';
+
+import { useAppDialog } from '@/shared/components/AppDialogProvider';
+
+import {
+ getAndroidAlarmPermissionStatus,
+ isAndroidAlarmSupported,
+ openAndroidAlarmPermissionSettings,
+ requestAndroidNotificationPermission,
+} from '../native/alarmScheduler';
+
+type PermissionKind = 'notifications' | 'exactAlarm' | 'overlay' | 'fullScreen' | 'battery';
+
+const PERMISSION_PROMPTS: Record<
+ PermissionKind,
+ {
+ title: string;
+ message: string;
+ settingsKind?: 'exactAlarm' | 'overlay' | 'fullScreen' | 'battery';
+ }
+> = {
+ notifications: {
+ title: '需要通知权限',
+ message: '允许通知后,日程闹钟才能弹出提醒并播放语音。',
+ },
+ exactAlarm: {
+ title: '需要精确闹钟权限',
+ message: '允许后,日程提醒才能在设定时间准时触发。',
+ settingsKind: 'exactAlarm',
+ },
+ overlay: {
+ title: '需要悬浮窗权限',
+ message: '允许“显示在其他应用上层”后,才能在其他 App 上方显示停止闹钟界面。',
+ settingsKind: 'overlay',
+ },
+ fullScreen: {
+ title: '需要全屏通知权限',
+ message: '允许后,锁屏或息屏时可以直接显示响铃页面。',
+ settingsKind: 'fullScreen',
+ },
+ battery: {
+ title: '需要忽略电池优化',
+ message: '关闭电池优化可以减少系统清理闹钟进程的概率。',
+ settingsKind: 'battery',
+ },
+};
+
+async function nextMissingPermission(skipped: Set): Promise {
+ const status = await getAndroidAlarmPermissionStatus();
+ if (!status) return null;
+ const order: PermissionKind[] = [
+ 'notifications',
+ 'exactAlarm',
+ 'overlay',
+ 'fullScreen',
+ 'battery',
+ ];
+ for (const kind of order) {
+ if (skipped.has(kind)) continue;
+ if (kind === 'notifications' && !status.notifications) return kind;
+ if (kind === 'exactAlarm' && !status.exactAlarm) return kind;
+ if (kind === 'overlay' && !status.overlay) return kind;
+ if (kind === 'fullScreen' && !status.fullScreen) return kind;
+ if (kind === 'battery' && !status.battery) return kind;
+ }
+ return null;
+}
+
+/**
+ * 进入 App 时按文档逐项申请闹钟相关权限;用户从设置返回后再继续下一项。
+ * 创建日程时不再弹授权。
+ */
+export function useAlarmPermissionsOnLaunch() {
+ const { confirm } = useAppDialog();
+ const busyRef = useRef(false);
+ const awaitingReturnRef = useRef(false);
+ const skippedRef = useRef(new Set());
+
+ useEffect(() => {
+ if (Platform.OS !== 'android' || !isAndroidAlarmSupported()) return;
+
+ const runPrompt = () => {
+ void promptNext().catch(() => {
+ // Permission APIs are host-owned; a rejected prompt must not become an
+ // unhandled promise from a timer or AppState callback.
+ busyRef.current = false;
+ });
+ };
+
+ const promptNext = async () => {
+ if (busyRef.current) return;
+ busyRef.current = true;
+
+ try {
+ const missing = await nextMissingPermission(skippedRef.current);
+ if (!missing) return;
+
+ const prompt = PERMISSION_PROMPTS[missing];
+
+ if (missing === 'notifications') {
+ await requestAndroidNotificationPermission();
+ // 给系统权限弹窗一点时间;若仍未授权则本会话跳过,避免死循环
+ const status = await getAndroidAlarmPermissionStatus();
+ if (status && !status.notifications) {
+ skippedRef.current.add('notifications');
+ }
+ busyRef.current = false;
+ setTimeout(() => {
+ runPrompt();
+ }, 350);
+ return;
+ }
+
+ const shouldAuthorize = await confirm({
+ title: prompt.title,
+ message: prompt.message,
+ confirmLabel: '去授权',
+ cancelLabel: '暂不',
+ });
+ if (!shouldAuthorize) {
+ skippedRef.current.add(missing);
+ } else {
+ awaitingReturnRef.current = true;
+ await openAndroidAlarmPermissionSettings(prompt.settingsKind!);
+ }
+ } finally {
+ busyRef.current = false;
+ }
+
+ if (!awaitingReturnRef.current) {
+ setTimeout(() => {
+ runPrompt();
+ }, 200);
+ }
+ };
+
+ const onAppStateChange = (state: AppStateStatus) => {
+ if (state !== 'active') return;
+ if (!awaitingReturnRef.current) return;
+ awaitingReturnRef.current = false;
+ setTimeout(() => {
+ runPrompt();
+ }, 300);
+ };
+
+ const timer = setTimeout(() => {
+ runPrompt();
+ }, 600);
+ const subscription = AppState.addEventListener('change', onAppStateChange);
+
+ return () => {
+ clearTimeout(timer);
+ subscription.remove();
+ };
+ }, [confirm]);
+}
diff --git a/frontend/src/features/reminder/index.ts b/frontend/src/features/reminder/index.ts
new file mode 100644
index 0000000..32682c1
--- /dev/null
+++ b/frontend/src/features/reminder/index.ts
@@ -0,0 +1,3 @@
+export { cancelAndroidAlarm, isAndroidAlarmSupported } from './native/alarmScheduler';
+export { useAlarmPermissionsOnLaunch } from './hooks/useAlarmPermissionsOnLaunch';
+export { syncScheduleAlarm } from './services/syncScheduleAlarm';
diff --git a/frontend/src/features/reminder/native/alarmScheduler.ts b/frontend/src/features/reminder/native/alarmScheduler.ts
new file mode 100644
index 0000000..c4148d4
--- /dev/null
+++ b/frontend/src/features/reminder/native/alarmScheduler.ts
@@ -0,0 +1,90 @@
+import { NativeModules, Platform } from 'react-native';
+
+type AlarmPermissionStatus = {
+ exactAlarm: boolean;
+ overlay: boolean;
+ fullScreen: boolean;
+ notifications: boolean;
+ battery: boolean;
+};
+
+type TimeflowAlarmNative = {
+ schedule: (triggerAtMillis: number, title?: string | null) => Promise<{ alarmId: string }>;
+ cancel: (alarmId: string) => Promise;
+ getPermissionStatus: () => Promise;
+ openPermissionSettings: (
+ kind: 'exactAlarm' | 'overlay' | 'fullScreen' | 'battery' | 'app',
+ ) => Promise;
+ requestNotificationPermission: () => Promise;
+};
+
+const NativeAlarm = NativeModules.TimeflowAlarm as TimeflowAlarmNative | undefined;
+
+export function isAndroidAlarmSupported(): boolean {
+ return Platform.OS === 'android' && NativeAlarm != null;
+}
+
+export async function scheduleAndroidAlarm(
+ triggerAtMillis: number,
+ title: string,
+): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm) return null;
+ const result = await NativeAlarm.schedule(triggerAtMillis, title);
+ return result.alarmId;
+}
+
+export async function cancelAndroidAlarm(alarmId: string | null | undefined): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm || !alarmId) return;
+ try {
+ await NativeAlarm.cancel(alarmId);
+ } catch {
+ // Best-effort cancel; missing records should not block schedule edits.
+ }
+}
+
+export async function getAndroidAlarmPermissionStatus(): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm) return null;
+ return NativeAlarm.getPermissionStatus();
+}
+
+export async function openAndroidAlarmPermissionSettings(
+ kind: 'exactAlarm' | 'overlay' | 'fullScreen' | 'battery' | 'app',
+): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm) return;
+ await NativeAlarm.openPermissionSettings(kind);
+}
+
+export async function requestAndroidNotificationPermission(): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm) return false;
+ return NativeAlarm.requestNotificationPermission();
+}
+
+/** 静默检查,不弹系统设置页。用于创建日程时。 */
+export async function areAndroidAlarmPermissionsGranted(): Promise {
+ if (!isAndroidAlarmSupported() || !NativeAlarm) return false;
+ const status = await NativeAlarm.getPermissionStatus();
+ return (
+ status.exactAlarm &&
+ status.overlay &&
+ status.fullScreen &&
+ status.notifications &&
+ status.battery
+ );
+}
+
+/**
+ * Reminder fires at start_time minus time_remind_offset_minutes.
+ * Returns null when no future alarm should be registered.
+ */
+export function computeScheduleAlarmTriggerMillis(
+ startTime: string | null | undefined,
+ offsetMinutes: number | null | undefined,
+): number | null {
+ if (!startTime) return null;
+ const startMs = Date.parse(startTime);
+ if (!Number.isFinite(startMs)) return null;
+ const offset = (offsetMinutes ?? 0) * 60_000;
+ const triggerAtMillis = startMs - offset;
+ if (triggerAtMillis <= Date.now()) return null;
+ return triggerAtMillis;
+}
diff --git a/frontend/src/features/reminder/services/syncScheduleAlarm.ts b/frontend/src/features/reminder/services/syncScheduleAlarm.ts
new file mode 100644
index 0000000..9a430dc
--- /dev/null
+++ b/frontend/src/features/reminder/services/syncScheduleAlarm.ts
@@ -0,0 +1,41 @@
+import {
+ areAndroidAlarmPermissionsGranted,
+ cancelAndroidAlarm,
+ computeScheduleAlarmTriggerMillis,
+ isAndroidAlarmSupported,
+ scheduleAndroidAlarm,
+} from '../native/alarmScheduler';
+
+export async function syncScheduleAlarm(input: {
+ scheduleType: 'time' | 'location';
+ title: string;
+ startTime: string | null;
+ offsetMinutes: number;
+ previousAlarmId: string | null;
+ shouldArm: boolean;
+}): Promise {
+ if (!isAndroidAlarmSupported()) {
+ return input.previousAlarmId;
+ }
+
+ if (input.previousAlarmId) {
+ await cancelAndroidAlarm(input.previousAlarmId);
+ }
+
+ if (!input.shouldArm || input.scheduleType !== 'time') {
+ return null;
+ }
+
+ const triggerAtMillis = computeScheduleAlarmTriggerMillis(input.startTime, input.offsetMinutes);
+ if (triggerAtMillis == null) {
+ return null;
+ }
+
+ // 权限在进入 App 时申请;创建时只静默检查,不再跳转设置页。
+ const ready = await areAndroidAlarmPermissionsGranted();
+ if (!ready) {
+ return null;
+ }
+
+ return scheduleAndroidAlarm(triggerAtMillis, input.title);
+}
diff --git a/frontend/src/features/schedule/application/AlarmPort.ts b/frontend/src/features/schedule/application/AlarmPort.ts
new file mode 100644
index 0000000..6c3c3c0
--- /dev/null
+++ b/frontend/src/features/schedule/application/AlarmPort.ts
@@ -0,0 +1,24 @@
+import type { Schedule } from '@/contracts';
+
+export type AlarmPort = {
+ /**
+ * Returns the system reference that should be persisted after syncing.
+ * Adapters for platforms without a local alarm should return the previous
+ * reference, while adapters that own the alarm lifecycle may return null
+ * when no alarm is armed.
+ */
+ syncForSchedule(input: {
+ scheduleType: Schedule['schedule_type'];
+ title: string;
+ startTime: string | null;
+ offsetMinutes: number;
+ previousAlarmId: string | null;
+ shouldArm: boolean;
+ }): Promise;
+ /**
+ * Cancels an alarm and returns the reference that should remain on the
+ * schedule entity. This keeps platform-specific reference semantics out of
+ * the application service.
+ */
+ cancel(alarmId: string | null | undefined): Promise;
+};
diff --git a/frontend/src/features/schedule/application/ScheduleNotificationPort.ts b/frontend/src/features/schedule/application/ScheduleNotificationPort.ts
new file mode 100644
index 0000000..0458d08
--- /dev/null
+++ b/frontend/src/features/schedule/application/ScheduleNotificationPort.ts
@@ -0,0 +1,4 @@
+import type { ScheduleConflict } from '@/contracts';
+
+/** UI-facing feedback supplied by the app composition root. */
+export type ScheduleConflictNotifier = (conflicts: readonly ScheduleConflict[]) => void;
diff --git a/frontend/src/features/schedule/application/ScheduleService.ts b/frontend/src/features/schedule/application/ScheduleService.ts
new file mode 100644
index 0000000..67eb74f
--- /dev/null
+++ b/frontend/src/features/schedule/application/ScheduleService.ts
@@ -0,0 +1,149 @@
+import type { Schedule, ScheduleUpsertPayload as ScheduleDraft } from '@/contracts';
+import { nextRequestId } from '@/shared/utils/requestId';
+
+import type { AlarmPort } from './AlarmPort';
+import { scheduleFromUpsertPayload, toUpsertCommand } from '../data/adapters';
+import type { ScheduleCache } from '../data/ScheduleCache';
+import type { ScheduleRepositoryPort } from '../data/ScheduleRepositoryPort';
+import { markDeleted, nextStatusAfterToggle, withStatus } from '../domain/scheduleStatus';
+import type { ScheduleConflictNotifier } from './ScheduleNotificationPort';
+
+export type ScheduleServiceDeps = {
+ repository: ScheduleRepositoryPort & { dispose?: () => void };
+ cache: ScheduleCache;
+ getUserId: () => string;
+ alarmAdapter: AlarmPort;
+ /** UI feedback is supplied by the app composition root, never by this use case. */
+ notifyConflicts?: ScheduleConflictNotifier;
+};
+
+export class ScheduleService {
+ private readonly alarm: AlarmPort;
+ private pushUnsubscribe: (() => void) | null = null;
+ private loadGeneration = 0;
+
+ constructor(private readonly deps: ScheduleServiceDeps) {
+ this.alarm = deps.alarmAdapter;
+ }
+
+ async bootstrap(): Promise {
+ const generation = ++this.loadGeneration;
+ const schedules = await this.deps.repository.list({
+ status: null,
+ include_deleted: false,
+ });
+ if (generation !== this.loadGeneration) return;
+ this.deps.cache.replaceAll(schedules);
+ if (!this.pushUnsubscribe) {
+ this.pushUnsubscribe = this.deps.repository.subscribe((event) => {
+ this.deps.cache.applyPush(event);
+ });
+ }
+ }
+
+ /** 重连后强制重新拉取列表。 */
+ async resync(): Promise {
+ await this.bootstrap();
+ }
+
+ dispose(): void {
+ this.loadGeneration += 1;
+ this.pushUnsubscribe?.();
+ this.pushUnsubscribe = null;
+ this.deps.repository.dispose?.();
+ }
+
+ getItems(): Schedule[] {
+ return this.deps.cache.getSnapshot();
+ }
+
+ subscribe(listener: (items: Schedule[]) => void): () => void {
+ return this.deps.cache.subscribe(listener);
+ }
+
+ async saveDraft(draft: ScheduleDraft): Promise {
+ const userId = this.deps.getUserId();
+ const requestId = nextRequestId('req_schedule');
+ // A missing ID means create. The backend owns ID generation; sending a
+ // client-generated ID makes the MVP backend treat the command as an edit.
+ const command = toUpsertCommand(draft, requestId);
+ const existing = draft.schedule_id
+ ? (this.deps.cache.getSnapshot().find((item) => item.id === draft.schedule_id) ?? null)
+ : null;
+
+ const response = await this.deps.repository.upsert(command);
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+
+ if (response.payload.conflicts.length > 0) {
+ try {
+ this.deps.notifyConflicts?.(response.payload.conflicts);
+ } catch {
+ // User feedback must not turn a successful server write into a failed
+ // mutation when a host notifier is unavailable.
+ }
+ }
+
+ const scheduleId = response.payload.schedule_id;
+
+ const offsetMinutes = draft.time_remind_offset_minutes ?? 0;
+ const syncedSystemScheduleRefId = await this.alarm.syncForSchedule({
+ scheduleType: draft.schedule_type,
+ title: draft.title,
+ startTime: draft.start_time ?? null,
+ offsetMinutes,
+ previousAlarmId: existing?.system_schedule_ref_id ?? null,
+ shouldArm: response.payload.status === 'scheduled',
+ });
+
+ const entity = scheduleFromUpsertPayload({
+ draft: { ...draft, schedule_id: scheduleId },
+ scheduleId,
+ userId,
+ status: response.payload.status,
+ geofenceArmed: response.payload.geofence_armed,
+ existing,
+ systemScheduleRefId: syncedSystemScheduleRefId,
+ });
+
+ this.deps.cache.upsert(entity);
+ return entity;
+ }
+
+ async toggleDone(schedule: Schedule): Promise {
+ const nextStatus = nextStatusAfterToggle(schedule.status);
+ if (!nextStatus || nextStatus === 'deleted') return;
+
+ const response = await this.deps.repository.updateStatus(schedule.id, nextStatus);
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+
+ let systemScheduleRefId = schedule.system_schedule_ref_id;
+ if (nextStatus === 'done') {
+ systemScheduleRefId = await this.alarm.cancel(systemScheduleRefId);
+ } else {
+ systemScheduleRefId = await this.alarm.syncForSchedule({
+ scheduleType: schedule.schedule_type,
+ title: schedule.title,
+ startTime: schedule.start_time,
+ offsetMinutes: schedule.time_remind_offset_minutes,
+ previousAlarmId: schedule.system_schedule_ref_id,
+ shouldArm: true,
+ });
+ }
+
+ this.deps.cache.upsert(withStatus(schedule, response.payload.status, systemScheduleRefId));
+ }
+
+ async deleteSchedule(schedule: Schedule): Promise {
+ if (schedule.status === 'deleted') return;
+ const response = await this.deps.repository.notifyDeleted(schedule.id);
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+ const systemScheduleRefId = await this.alarm.cancel(schedule.system_schedule_ref_id);
+ this.deps.cache.upsert(markDeleted(schedule, systemScheduleRefId));
+ }
+}
diff --git a/frontend/src/features/schedule/calendar/MonthView.styles.ts b/frontend/src/features/schedule/calendar/MonthView.styles.ts
new file mode 100644
index 0000000..c0a4a55
--- /dev/null
+++ b/frontend/src/features/schedule/calendar/MonthView.styles.ts
@@ -0,0 +1,102 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const monthStyles = StyleSheet.create({
+ monthContent: { paddingBottom: 28 },
+ monthCard: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 20,
+ borderWidth: 1,
+ marginBottom: 14,
+ paddingHorizontal: 12,
+ paddingVertical: 14,
+ },
+ monthHeader: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 11,
+ minHeight: 46,
+ paddingHorizontal: 2,
+ },
+ monthNavButton: {
+ alignItems: 'center',
+ backgroundColor: '#EFEDE7',
+ borderRadius: 9,
+ height: 29,
+ justifyContent: 'center',
+ minWidth: 42,
+ paddingHorizontal: 9,
+ },
+ monthNavText: { color: colors.sub, fontSize: 10, fontWeight: '700' },
+ monthYear: { color: '#8C938F', fontSize: 6, textAlign: 'center' },
+ monthTitle: {
+ color: colors.ink,
+ fontSize: 16,
+ fontWeight: '700',
+ marginTop: 2,
+ textAlign: 'center',
+ },
+ weekdayRow: { flexDirection: 'row', gap: 3, height: 20, marginBottom: 0 },
+ weekday: { color: colors.sub, flex: 1, fontSize: 10, paddingBottom: 6, textAlign: 'center' },
+ monthGrid: { gap: 3 },
+ monthWeekRow: { flexDirection: 'row', gap: 3 },
+ monthDay: {
+ alignItems: 'center',
+ backgroundColor: '#EFEFEF',
+ borderRadius: 10,
+ flex: 1,
+ height: 40,
+ justifyContent: 'center',
+ },
+ monthDayActive: { backgroundColor: colors.deep },
+ monthDaySelected: { backgroundColor: colors.limeSoft },
+ monthDayMuted: { backgroundColor: 'rgba(239, 239, 239, 0.3)' },
+ monthDayText: { color: colors.ink, fontSize: 12 },
+ monthDayTextActive: { color: colors.surface, fontWeight: '800' },
+ monthDayTextSelected: { color: colors.ink, fontWeight: '700' },
+ monthDayTextMuted: { color: '#C2C6C3' },
+ monthDot: {
+ alignItems: 'center',
+ backgroundColor: '#87A16C',
+ borderRadius: 2,
+ height: 4,
+ justifyContent: 'center',
+ marginTop: 3,
+ width: 4,
+ },
+ monthDayActiveDot: { backgroundColor: colors.lime },
+ monthCompletedMarker: {
+ backgroundColor: '#7CA38A',
+ borderRadius: 6,
+ height: 12,
+ marginTop: 2,
+ width: 12,
+ },
+ monthSelectedHeading: {
+ alignItems: 'center',
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 10,
+ marginTop: 0,
+ minHeight: 35,
+ paddingBottom: 6,
+ },
+ monthSelectedTitle: { color: colors.ink, fontSize: 16, fontWeight: '600' },
+ scheduleEmpty: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderStyle: 'dashed',
+ borderWidth: 1,
+ color: colors.sub,
+ fontSize: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 18,
+ textAlign: 'center',
+ },
+});
diff --git a/frontend/src/features/schedule/calendar/MonthView.tsx b/frontend/src/features/schedule/calendar/MonthView.tsx
new file mode 100644
index 0000000..b4c0db0
--- /dev/null
+++ b/frontend/src/features/schedule/calendar/MonthView.tsx
@@ -0,0 +1,158 @@
+import { Check } from 'lucide-react-native';
+import { Pressable, ScrollView, Text, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+import type { Schedule } from '@/contracts';
+import { dateKey, formatMonthDay, WEEKDAY_LABELS } from '@/shared/utils/date';
+
+import { ScheduleRow } from './ScheduleRow';
+import type { ScheduleIndex } from './scheduleIndex';
+import { schedulesOnDate } from './scheduleIndex';
+import { monthStyles as styles } from './MonthView.styles';
+
+export function MonthView({
+ now,
+ selectedDate,
+ scheduleIndex,
+ visibleMonth,
+ onMonthChange,
+ onOpenSchedule,
+ onSelectDate,
+ onToggleSchedule,
+}: {
+ now: Date;
+ selectedDate: Date;
+ scheduleIndex: ScheduleIndex;
+ onMonthChange: (month: Date) => void;
+ onOpenSchedule: (scheduleId: string) => void;
+ onSelectDate: (date: Date) => void;
+ onToggleSchedule?: (schedule: Schedule) => void;
+ visibleMonth: Date;
+}) {
+ const year = visibleMonth.getFullYear();
+ const month = visibleMonth.getMonth();
+ const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7;
+ const days = Array.from(
+ { length: 42 },
+ (_, index) => new Date(year, month, index - firstDayOffset + 1),
+ );
+ const selectedKey = dateKey(selectedDate);
+ const todayKey = dateKey(now);
+ const selectedItems = schedulesOnDate(scheduleIndex, selectedDate);
+ const dateItems = scheduleIndex.byDateKey;
+
+ return (
+
+
+
+ onMonthChange(new Date(year, month - 1, 1))}
+ style={styles.monthNavButton}
+ >
+ 上月
+
+
+ {year}
+ {month + 1}月
+
+ onMonthChange(new Date(year, month + 1, 1))}
+ style={styles.monthNavButton}
+ >
+ 下月
+
+
+
+ {WEEKDAY_LABELS.map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {Array.from({ length: 6 }, (_, rowIndex) => (
+
+ {days.slice(rowIndex * 7, rowIndex * 7 + 7).map((day) => {
+ const inMonth = day.getMonth() === month;
+ const key = dateKey(day);
+ const active = key === todayKey;
+ const selected = key === selectedKey;
+ const dayItems = dateItems.get(key) ?? [];
+ const hasCompletedMarker =
+ inMonth &&
+ dayItems.length > 0 &&
+ dayItems.every((item) => item.status === 'done');
+ const hasRegularMarker =
+ inMonth && dayItems.some((item) => item.status === 'scheduled');
+ return (
+ onSelectDate(day)}
+ style={[
+ styles.monthDay,
+ active && styles.monthDayActive,
+ selected && !active && styles.monthDaySelected,
+ !inMonth && styles.monthDayMuted,
+ ]}
+ >
+
+ {day.getDate()}
+
+ {(hasCompletedMarker || hasRegularMarker) && (
+
+ {hasCompletedMarker && (
+
+ )}
+
+ )}
+
+ );
+ })}
+
+ ))}
+
+
+
+ {formatMonthDay(selectedDate)}
+
+ {selectedItems.length > 0 ? (
+ selectedItems.map((item, index) => (
+ onOpenSchedule(item.id)}
+ onToggle={onToggleSchedule ? () => onToggleSchedule(item) : undefined}
+ showConnector={index < selectedItems.length - 1}
+ />
+ ))
+ ) : (
+ 这一天暂无详细安排
+ )}
+
+ );
+}
diff --git a/frontend/src/features/schedule/calendar/ScheduleRow.tsx b/frontend/src/features/schedule/calendar/ScheduleRow.tsx
new file mode 100644
index 0000000..bb9f6df
--- /dev/null
+++ b/frontend/src/features/schedule/calendar/ScheduleRow.tsx
@@ -0,0 +1,81 @@
+import { Check } from 'lucide-react-native';
+import { Pressable, Text, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+import type { Schedule } from '@/contracts';
+
+import { scheduleColor, scheduleRange, scheduleTime } from '../presentation/scheduleFormat';
+import { scheduleRowStyles as styles } from './scheduleRow.styles';
+
+export function ScheduleRow({
+ compact = false,
+ item,
+ onPress,
+ onToggle,
+ showConnector = true,
+}: {
+ compact?: boolean;
+ item: Schedule;
+ onPress?: () => void;
+ onToggle?: () => void;
+ showConnector?: boolean;
+}) {
+ const done = item.status === 'done';
+
+ return (
+
+
+ {scheduleTime(item)}
+
+
+ {
+ event?.stopPropagation?.();
+ onToggle?.();
+ }}
+ style={[
+ styles.scheduleDot,
+ { backgroundColor: scheduleColor(item) },
+ done && styles.scheduleDotCompleted,
+ ]}
+ >
+ {done ? : null}
+
+ {showConnector && }
+
+
+
+
+ {item.title}
+
+
+ {(item.location_name || item.notes) && (
+ {item.location_name ?? item.notes}
+ )}
+
+ {scheduleRange(item)}
+
+
+
+ );
+}
diff --git a/frontend/src/features/schedule/calendar/scheduleIndex.ts b/frontend/src/features/schedule/calendar/scheduleIndex.ts
new file mode 100644
index 0000000..176dc02
--- /dev/null
+++ b/frontend/src/features/schedule/calendar/scheduleIndex.ts
@@ -0,0 +1,48 @@
+import type { Schedule } from '@/contracts';
+import { dateKey } from '@/shared/utils/date';
+import { scheduleDate } from '../presentation/scheduleFormat';
+
+export type ScheduleIndex = {
+ byDateKey: Map;
+ locationSchedules: Schedule[];
+ timeSchedules: Schedule[];
+ markedDateKeys: string[];
+};
+
+/** 一次遍历活跃日程,按日期/类型分桶,供月视图使用。 */
+export function buildScheduleIndex(items: Schedule[]): ScheduleIndex {
+ const byDateKey = new Map();
+ const locationSchedules: Schedule[] = [];
+ const timeSchedules: Schedule[] = [];
+ const markedKeys = new Set();
+
+ for (const item of items) {
+ if (item.status === 'deleted') continue;
+
+ if (item.schedule_type === 'location') {
+ locationSchedules.push(item);
+ }
+ if (item.schedule_type === 'time') {
+ timeSchedules.push(item);
+ }
+
+ const date = scheduleDate(item);
+ if (!date) continue;
+ const key = dateKey(date);
+ const bucket = byDateKey.get(key);
+ if (bucket) bucket.push(item);
+ else byDateKey.set(key, [item]);
+ if (item.schedule_type === 'time') markedKeys.add(key);
+ }
+
+ return {
+ byDateKey,
+ locationSchedules,
+ timeSchedules,
+ markedDateKeys: [...markedKeys],
+ };
+}
+
+export function schedulesOnDate(index: ScheduleIndex, date: Date): Schedule[] {
+ return index.byDateKey.get(dateKey(date)) ?? [];
+}
diff --git a/frontend/src/features/schedule/calendar/scheduleRow.styles.ts b/frontend/src/features/schedule/calendar/scheduleRow.styles.ts
new file mode 100644
index 0000000..870565f
--- /dev/null
+++ b/frontend/src/features/schedule/calendar/scheduleRow.styles.ts
@@ -0,0 +1,67 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const scheduleRowStyles = StyleSheet.create({
+ scheduleRow: { alignItems: 'flex-start', flexDirection: 'row', minHeight: 84 },
+ scheduleRowCompact: { minHeight: 72 },
+ scheduleRowCompleted: { opacity: 0.82 },
+ scheduleTime: {
+ color: '#7D8983',
+ fontSize: 11,
+ lineHeight: 14,
+ paddingTop: 7,
+ width: 58,
+ },
+ scheduleTimeCompact: { paddingTop: 6, width: 52 },
+ scheduleRail: { alignItems: 'center', alignSelf: 'stretch', paddingTop: 6, width: 17 },
+ scheduleRailCompact: { paddingTop: 8, width: 12 },
+ scheduleDot: { borderRadius: 6, height: 11, width: 11, zIndex: 1 },
+ scheduleDotCompleted: {
+ alignItems: 'center',
+ backgroundColor: '#7CA38A',
+ justifyContent: 'center',
+ },
+ scheduleLine: {
+ backgroundColor: '#D7DCD7',
+ bottom: 0,
+ position: 'absolute',
+ top: 19,
+ width: 1,
+ },
+ scheduleCopy: {
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flex: 1,
+ minWidth: 0,
+ paddingBottom: 15,
+ paddingTop: 3,
+ },
+ scheduleCopyCompact: { marginBottom: 4, paddingBottom: 14, paddingTop: 4 },
+ scheduleHeading: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ scheduleTitleCompact: { fontSize: 13, lineHeight: 22 },
+ scheduleTitle: {
+ color: colors.ink,
+ flex: 1,
+ fontSize: 14,
+ fontWeight: '700',
+ lineHeight: 19,
+ },
+ scheduleTitleCompleted: { color: '#7F8882', textDecorationLine: 'line-through' },
+ scheduleMeta: {
+ alignSelf: 'flex-start',
+ backgroundColor: '#ECF2D7',
+ borderRadius: 8,
+ color: '#70814F',
+ fontSize: 8,
+ marginTop: 6,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ },
+ scheduleRange: { color: '#818B85', fontSize: 10, lineHeight: 14, marginTop: 7 },
+ scheduleRangeCompact: { color: '#747C77', fontSize: 11, lineHeight: 16, marginTop: 4 },
+});
diff --git a/frontend/src/features/schedule/data/ScheduleCache.ts b/frontend/src/features/schedule/data/ScheduleCache.ts
new file mode 100644
index 0000000..42550a8
--- /dev/null
+++ b/frontend/src/features/schedule/data/ScheduleCache.ts
@@ -0,0 +1,52 @@
+import type { Schedule } from '@/contracts';
+
+import { compareSchedules } from '../domain/scheduleOrdering';
+
+import type { SchedulePushEvent } from './ScheduleRepositoryPort';
+
+/** 本地列表真相:由 list/upsert/push 更新,供 UI 订阅。 */
+export class ScheduleCache {
+ private items: Schedule[] = [];
+ private readonly listeners = new Set<(items: Schedule[]) => void>();
+
+ getSnapshot(): Schedule[] {
+ return this.items;
+ }
+
+ subscribe(listener: (items: Schedule[]) => void): () => void {
+ this.listeners.add(listener);
+ listener(this.items);
+ return () => this.listeners.delete(listener);
+ }
+
+ replaceAll(schedules: Schedule[]): void {
+ this.items = [...schedules].sort(compareSchedules);
+ this.emit();
+ }
+
+ upsert(schedule: Schedule): void {
+ const index = this.items.findIndex((item) => item.id === schedule.id);
+ if (index < 0) {
+ this.items = [...this.items, schedule].sort(compareSchedules);
+ } else {
+ const next = [...this.items];
+ next[index] = schedule;
+ this.items = next.sort(compareSchedules);
+ }
+ this.emit();
+ }
+
+ applyPush(event: SchedulePushEvent): void {
+ if (event.type === 'schedule.snapshot') {
+ this.replaceAll(event.schedules);
+ return;
+ }
+ this.upsert(event.schedule);
+ }
+
+ private emit(): void {
+ for (const listener of this.listeners) {
+ listener(this.items);
+ }
+ }
+}
diff --git a/frontend/src/features/schedule/data/ScheduleRepositoryPort.ts b/frontend/src/features/schedule/data/ScheduleRepositoryPort.ts
new file mode 100644
index 0000000..15bc196
--- /dev/null
+++ b/frontend/src/features/schedule/data/ScheduleRepositoryPort.ts
@@ -0,0 +1,24 @@
+import type {
+ Schedule,
+ ScheduleDeletedAck,
+ ScheduleListQueryPayload,
+ ScheduleStatus,
+ ScheduleStatusUpdateResponse,
+ ScheduleUpsertCommand,
+ ScheduleUpsertResponse,
+} from '@/contracts';
+
+export type SchedulePushEvent =
+ | { type: 'schedule.updated'; schedule: Schedule }
+ | { type: 'schedule.snapshot'; schedules: Schedule[] };
+
+export interface ScheduleRepositoryPort {
+ list(query: ScheduleListQueryPayload): Promise;
+ upsert(command: ScheduleUpsertCommand): Promise;
+ updateStatus(
+ scheduleId: string,
+ status: Extract,
+ ): Promise;
+ notifyDeleted(scheduleId: string): Promise;
+ subscribe(listener: (event: SchedulePushEvent) => void): () => void;
+}
diff --git a/frontend/src/features/schedule/data/ScheduleTransport.ts b/frontend/src/features/schedule/data/ScheduleTransport.ts
new file mode 100644
index 0000000..c779f03
--- /dev/null
+++ b/frontend/src/features/schedule/data/ScheduleTransport.ts
@@ -0,0 +1,14 @@
+import type { WsJsonMessage } from '@/contracts';
+
+/**
+ * schedule data 层所需的最小传输面。
+ * app 注入 WsClient;feature 不依赖 SessionProvider。
+ */
+export type ScheduleTransport = {
+ onMessage(listener: (message: WsJsonMessage | ArrayBuffer) => void): () => void;
+ request(
+ message: WsJsonMessage & { request_id: string },
+ isMatch?: (response: WsJsonMessage) => boolean,
+ ): Promise;
+ sendJson(message: WsJsonMessage): void;
+};
diff --git a/frontend/src/features/schedule/data/WsScheduleRepository.ts b/frontend/src/features/schedule/data/WsScheduleRepository.ts
new file mode 100644
index 0000000..928d3c9
--- /dev/null
+++ b/frontend/src/features/schedule/data/WsScheduleRepository.ts
@@ -0,0 +1,112 @@
+import type {
+ Schedule,
+ ScheduleDeleted,
+ ScheduleDeletedAck,
+ ScheduleListQuery,
+ ScheduleListResponse,
+ ScheduleListQueryPayload,
+ ScheduleStatus,
+ ScheduleStatusUpdateCommand,
+ ScheduleStatusUpdateResponse,
+ ScheduleUpsertCommand,
+ ScheduleUpsertResponse,
+ WsJsonMessage,
+} from '@/contracts';
+import { nextRequestId } from '@/shared/utils/requestId';
+
+import type { SchedulePushEvent, ScheduleRepositoryPort } from './ScheduleRepositoryPort';
+import type { ScheduleTransport } from './ScheduleTransport';
+
+export class WsScheduleRepository implements ScheduleRepositoryPort {
+ private readonly listeners = new Set<(event: SchedulePushEvent) => void>();
+ private readonly unsubscribeClient: () => void;
+
+ constructor(private readonly client: ScheduleTransport) {
+ this.unsubscribeClient = this.client.onMessage((message) => {
+ if (message instanceof ArrayBuffer) return;
+ this.routePush(message);
+ });
+ }
+
+ dispose(): void {
+ this.unsubscribeClient();
+ this.listeners.clear();
+ }
+
+ async list(query: ScheduleListQueryPayload): Promise {
+ const request: ScheduleListQuery = {
+ type: 'schedule.list.query',
+ request_id: nextRequestId('req_list'),
+ payload: query,
+ };
+ const response = await this.client.request(request, (message) => {
+ return (
+ message.request_id === request.request_id &&
+ (message.type === 'schedule.list.result' || message.type === 'schedule.list.error')
+ );
+ });
+ if (!response.ok) {
+ throw new Error(response.error.message);
+ }
+ return response.payload.schedules;
+ }
+
+ async upsert(command: ScheduleUpsertCommand): Promise {
+ return this.client.request(command, (message) => {
+ return (
+ message.request_id === command.request_id &&
+ (message.type === 'schedule.upsert.result' || message.type === 'schedule.upsert.error')
+ );
+ });
+ }
+
+ async updateStatus(
+ scheduleId: string,
+ status: Extract,
+ ): Promise {
+ const command: ScheduleStatusUpdateCommand = {
+ type: 'schedule.status.command',
+ request_id: nextRequestId('req_status'),
+ payload: { schedule_id: scheduleId, status },
+ };
+ return this.client.request(command, (message) => {
+ return (
+ message.request_id === command.request_id &&
+ (message.type === 'schedule.status.result' || message.type === 'schedule.status.error')
+ );
+ });
+ }
+
+ async notifyDeleted(scheduleId: string): Promise {
+ const command: ScheduleDeleted = {
+ type: 'schedule.deleted',
+ request_id: nextRequestId('req_deleted'),
+ schedule_id: scheduleId,
+ deleted: true,
+ timestamp: new Date().toISOString(),
+ };
+ // 先登记 pending 再发送,避免 Fake 同步 ACK 竞态。
+ return this.client.request(command, (message) => {
+ return (
+ message.type === 'schedule.deleted.ack' &&
+ (message.request_id == null || message.request_id === command.request_id) &&
+ message.schedule_id === scheduleId
+ );
+ });
+ }
+
+ subscribe(listener: (event: SchedulePushEvent) => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private routePush(message: WsJsonMessage): void {
+ if (message.type === 'schedule.updated' && message.schedule) {
+ const event: SchedulePushEvent = {
+ type: 'schedule.updated',
+ schedule: message.schedule as Schedule,
+ };
+ for (const listener of this.listeners) listener(event);
+ }
+ }
+}
diff --git a/frontend/src/features/schedule/data/adapters.ts b/frontend/src/features/schedule/data/adapters.ts
new file mode 100644
index 0000000..34c9313
--- /dev/null
+++ b/frontend/src/features/schedule/data/adapters.ts
@@ -0,0 +1,116 @@
+import type {
+ Schedule,
+ ScheduleDraftFields,
+ ScheduleUpsertCommand,
+ ScheduleUpsertPayload,
+ VoiceParseDraft,
+} from '@/contracts';
+
+type ScheduleDraft = ScheduleUpsertPayload;
+
+/** 草稿业务字段归一化(`?? null`);调用方再补 source_mode / 特有默认值。 */
+function normalizeScheduleDraftFields(fields: ScheduleDraftFields) {
+ return {
+ schedule_type: fields.schedule_type,
+ title: fields.title,
+ notes: fields.notes ?? null,
+ start_time: fields.start_time ?? null,
+ end_time: fields.end_time ?? null,
+ timezone: fields.timezone ?? null,
+ location_name: fields.location_name ?? null,
+ location_address: fields.location_address ?? null,
+ latitude: fields.latitude ?? null,
+ longitude: fields.longitude ?? null,
+ geofence_radius_meters: fields.geofence_radius_meters ?? null,
+ geofence_armed: fields.geofence_armed ?? null,
+ time_remind_offset_minutes: fields.time_remind_offset_minutes ?? null,
+ };
+}
+
+/** Schedule → wire upsert payload / 编辑回填草稿(同一份字段投影)。 */
+function toUpsertPayload(schedule: Schedule): ScheduleUpsertPayload {
+ return {
+ schedule_id: schedule.id,
+ source_mode: schedule.source_mode,
+ schedule_type: schedule.schedule_type,
+ title: schedule.title,
+ notes: schedule.notes,
+ start_time: schedule.start_time,
+ end_time: schedule.end_time,
+ timezone: schedule.timezone,
+ location_name: schedule.location_name,
+ location_address: schedule.location_address,
+ latitude: schedule.latitude,
+ longitude: schedule.longitude,
+ geofence_radius_meters: schedule.geofence_radius_meters,
+ geofence_armed: schedule.geofence_armed,
+ time_remind_offset_minutes: schedule.time_remind_offset_minutes,
+ };
+}
+
+export function upsertDraftForSchedule(schedule: Schedule): ScheduleDraft {
+ return toUpsertPayload(schedule);
+}
+
+/** AppShell:将语音解析草稿映射为日程草稿。 */
+export function scheduleDraftFromVoiceParse(draft: VoiceParseDraft): ScheduleDraft {
+ return {
+ source_mode: 'voice',
+ ...normalizeScheduleDraftFields({
+ ...draft,
+ time_remind_offset_minutes: draft.time_remind_offset_minutes ?? 0,
+ }),
+ };
+}
+
+export function toUpsertCommand(draft: ScheduleDraft, requestId: string): ScheduleUpsertCommand {
+ return {
+ type: 'schedule.upsert.command',
+ request_id: requestId,
+ payload: draft,
+ };
+}
+
+/** draft → Schedule 实体(保存时组装)。 */
+export function scheduleFromUpsertPayload(input: {
+ draft: ScheduleDraft;
+ scheduleId: string;
+ userId: string;
+ status: Schedule['status'];
+ geofenceArmed: boolean;
+ existing?: Schedule | null;
+ systemScheduleRefId?: string | null;
+}): Schedule {
+ const { draft, existing } = input;
+ const fields = normalizeScheduleDraftFields(draft);
+ const now = new Date().toISOString();
+ return {
+ id: input.scheduleId,
+ user_id: existing?.user_id ?? input.userId,
+ source_mode: draft.source_mode,
+ schedule_type: fields.schedule_type,
+ status: input.status,
+ title: fields.title,
+ notes: fields.notes,
+ start_time: fields.start_time,
+ end_time: fields.end_time,
+ timezone: fields.timezone,
+ location_name: fields.location_name,
+ location_address: fields.location_address,
+ latitude: fields.latitude,
+ longitude: fields.longitude,
+ geofence_radius_meters:
+ fields.geofence_radius_meters ?? existing?.geofence_radius_meters ?? 100,
+ geofence_armed: input.geofenceArmed,
+ time_remind_offset_minutes: fields.time_remind_offset_minutes ?? 0,
+ time_triggered_at: existing?.time_triggered_at ?? null,
+ geo_triggered_at: existing?.geo_triggered_at ?? null,
+ system_schedule_ref_id:
+ input.systemScheduleRefId !== undefined
+ ? input.systemScheduleRefId
+ : (existing?.system_schedule_ref_id ?? null),
+ system_alarm_ref_id: existing?.system_alarm_ref_id ?? null,
+ created_at: existing?.created_at ?? now,
+ updated_at: now,
+ };
+}
diff --git a/frontend/src/features/schedule/detail/ScheduleDetailSheet.tsx b/frontend/src/features/schedule/detail/ScheduleDetailSheet.tsx
new file mode 100644
index 0000000..56e5714
--- /dev/null
+++ b/frontend/src/features/schedule/detail/ScheduleDetailSheet.tsx
@@ -0,0 +1,200 @@
+import {
+ CalendarClock,
+ CheckCircle2,
+ Clock3,
+ Pencil,
+ RotateCcw,
+ Trash2,
+} from 'lucide-react-native';
+import { Modal, Pressable, ScrollView, Text, View } from 'react-native';
+
+import { BackButton } from '@/shared/components/BackButton';
+import { useAppDialog } from '@/shared/components/AppDialogProvider';
+import type { Schedule } from '@/contracts';
+import { colors } from '@/shared/theme';
+import { formatFullDate } from '@/shared/utils/date';
+
+import {
+ scheduleColor,
+ scheduleDate,
+ scheduleDuration,
+ scheduleRange,
+ scheduleSourceLabel,
+ scheduleStatusLabel,
+} from '../presentation/scheduleFormat';
+import { detailStyles as styles } from './detail.styles';
+
+export function ScheduleDetailSheet({
+ onClose,
+ onDelete,
+ onEdit,
+ onOpenDay,
+ onToggle,
+ schedule,
+}: {
+ onClose: () => void;
+ onDelete?: () => void;
+ onEdit?: () => void;
+ onOpenDay: (date: Date) => void;
+ onToggle?: () => void;
+ schedule: Schedule | null;
+}) {
+ const { confirm } = useAppDialog();
+ const editable = schedule != null && schedule.status !== 'deleted';
+ const actionIsEdit = Boolean(onEdit && editable);
+ const canDelete = Boolean(onDelete && editable);
+ const canToggle = Boolean(onToggle && editable);
+ const statusLabel = schedule ? scheduleStatusLabel(schedule) : '';
+ const isDone = schedule?.status === 'done';
+ const itemDate = schedule ? scheduleDate(schedule) : null;
+ const displayDate = itemDate ?? new Date();
+ const dateLabel = schedule?.start_time ? formatFullDate(displayDate) : '按地点触发';
+ const rangeLabel = schedule ? scheduleRange(schedule) : '';
+
+ const handleDelete = async () => {
+ if (!canDelete) return;
+ const confirmed = await confirm({
+ title: '删除日程',
+ message: '确定删除这个日程吗?相关提醒也会一并取消。',
+ confirmLabel: '删除',
+ cancelLabel: '取消',
+ tone: 'danger',
+ });
+ if (!confirmed) return;
+ onDelete?.();
+ onClose();
+ };
+
+ return (
+
+
+
+ {schedule && (
+
+
+
+
+ 安排详情
+
+
+ {isDone && }
+
+ {statusLabel}
+
+
+
+
+
+ {scheduleSourceLabel(schedule)}
+ {schedule.title}
+
+ {isDone ? '已完成 · 可回顾这次安排' : '安排已加入你的时间轴'}
+
+
+
+
+
+
+
+ 日期与时间
+ {dateLabel}
+ {rangeLabel}
+
+ {scheduleDuration(schedule)}
+
+
+
+
+
+ 状态
+
+
+ {statusLabel}
+
+
+
+
+
+
+ {isDone ? (
+
+ ) : (
+
+ )}
+ {isDone ? '恢复' : '完成'}
+
+ void handleDelete()}
+ style={[
+ styles.scheduleModalSecondaryAction,
+ canDelete && styles.scheduleModalDeleteAction,
+ !canDelete && styles.scheduleModalDeleteDisabled,
+ ]}
+ >
+ {canDelete ? : null}
+
+ 删除
+
+
+ {
+ onClose();
+ if (actionIsEdit) onEdit?.();
+ else onOpenDay(displayDate);
+ }}
+ style={styles.scheduleModalPrimaryAction}
+ >
+ {actionIsEdit && }
+
+ {actionIsEdit ? '编辑日程' : '查看当天'}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/features/schedule/detail/detail.styles.ts b/frontend/src/features/schedule/detail/detail.styles.ts
new file mode 100644
index 0000000..8e7ea06
--- /dev/null
+++ b/frontend/src/features/schedule/detail/detail.styles.ts
@@ -0,0 +1,158 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const detailStyles = StyleSheet.create({
+ scheduleModalBackdrop: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(20, 40, 33, 0.34)',
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 14,
+ },
+ scheduleModalDismiss: {
+ bottom: 0,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ scheduleModalSheet: {
+ backgroundColor: '#F7F8F6',
+ borderRadius: 24,
+ maxHeight: '88%',
+ maxWidth: 520,
+ overflow: 'hidden',
+ width: '100%',
+ },
+ scheduleModalReferenceHeader: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(255, 255, 255, 0.94)',
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ gap: 10,
+ minHeight: 56,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ scheduleModalHeaderCopy: { flex: 1, minWidth: 0 },
+ scheduleModalHeaderTitle: {
+ color: colors.ink,
+ fontSize: 13,
+ fontWeight: '700',
+ lineHeight: 18,
+ marginTop: 2,
+ },
+ scheduleModalStatus: {
+ alignItems: 'center',
+ backgroundColor: '#EDF2DA',
+ borderRadius: 8,
+ flexDirection: 'row',
+ gap: 4,
+ paddingHorizontal: 8,
+ paddingVertical: 5,
+ },
+ scheduleModalStatusText: { color: '#5C7045', fontSize: 8, fontWeight: '700' },
+ scheduleModalStatusCompleted: { backgroundColor: '#E4EEE6' },
+ scheduleModalStatusTextCompleted: { color: '#63866E' },
+ scheduleModalScroll: { flexGrow: 0, flexShrink: 1 },
+ scheduleModalScrollContent: { paddingBottom: 16, paddingHorizontal: 20, paddingTop: 18 },
+ scheduleModalTitleBlock: {
+ paddingBottom: 16,
+ paddingRight: 2,
+ },
+ scheduleModalSource: { color: '#73806F', fontSize: 9, fontWeight: '600' },
+ scheduleModalTitle: {
+ color: colors.ink,
+ fontSize: 22,
+ fontWeight: '700',
+ lineHeight: 29,
+ marginTop: 7,
+ },
+ scheduleModalSubtitle: { color: '#7B877F', fontSize: 10, lineHeight: 15, marginTop: 7 },
+ scheduleModalTimeCard: {
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderColor: '#DFE3DD',
+ borderRadius: 8,
+ borderWidth: 1,
+ flexDirection: 'row',
+ gap: 12,
+ justifyContent: 'space-between',
+ padding: 15,
+ },
+ scheduleModalTimeIcon: {
+ alignItems: 'center',
+ borderRadius: 10,
+ flexShrink: 0,
+ height: 35,
+ justifyContent: 'center',
+ width: 35,
+ },
+ scheduleModalTimeCopy: { flex: 1, minWidth: 0 },
+ scheduleModalTimeEyebrow: { color: colors.sub, fontSize: 8 },
+ scheduleModalDate: { color: colors.ink, fontSize: 13, fontWeight: '700', marginTop: 5 },
+ scheduleModalTime: { color: '#64716A', fontSize: 11, marginTop: 4 },
+ scheduleModalDuration: {
+ backgroundColor: colors.deep,
+ borderRadius: 8,
+ color: colors.surface,
+ flexShrink: 0,
+ fontSize: 9,
+ fontWeight: '700',
+ paddingHorizontal: 8,
+ paddingVertical: 6,
+ },
+ scheduleModalMeta: { borderTopColor: colors.line, borderTopWidth: 1, marginTop: 14 },
+ scheduleModalMetaRow: {
+ alignItems: 'center',
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ minHeight: 43,
+ },
+ scheduleModalMetaLabelGroup: { alignItems: 'center', flexDirection: 'row', gap: 7 },
+ scheduleModalMetaLabel: { color: colors.sub, fontSize: 10 },
+ scheduleModalMetaValue: { color: colors.ink, fontSize: 11, fontWeight: '600' },
+ scheduleModalCompleted: { color: '#63866E' },
+ scheduleModalActions: {
+ backgroundColor: colors.surface,
+ borderTopColor: colors.line,
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ gap: 9,
+ minHeight: 66,
+ paddingHorizontal: 20,
+ paddingBottom: 12,
+ paddingTop: 10,
+ },
+ scheduleModalSecondaryAction: {
+ alignItems: 'center',
+ backgroundColor: '#E9ECE8',
+ borderRadius: 10,
+ flex: 1,
+ flexDirection: 'row',
+ gap: 6,
+ height: 44,
+ justifyContent: 'center',
+ },
+ scheduleModalSecondaryText: { color: colors.ink, fontSize: 10, fontWeight: '700' },
+ scheduleModalDeleteAction: {
+ backgroundColor: colors.peach,
+ },
+ scheduleModalDeleteText: { color: colors.coral },
+ scheduleModalDeleteDisabled: { opacity: 0.45 },
+ scheduleModalPrimaryAction: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 10,
+ flex: 1.35,
+ flexDirection: 'row',
+ gap: 6,
+ height: 44,
+ justifyContent: 'center',
+ },
+ scheduleModalPrimaryText: { color: colors.surface, fontSize: 10, fontWeight: '700' },
+});
diff --git a/frontend/src/features/schedule/domain/scheduleOrdering.ts b/frontend/src/features/schedule/domain/scheduleOrdering.ts
new file mode 100644
index 0000000..051592c
--- /dev/null
+++ b/frontend/src/features/schedule/domain/scheduleOrdering.ts
@@ -0,0 +1,10 @@
+import type { Schedule } from '@/contracts';
+
+export function compareSchedules(first: Schedule, second: Schedule) {
+ if (first.start_time && second.start_time) {
+ return new Date(first.start_time).getTime() - new Date(second.start_time).getTime();
+ }
+ if (first.start_time) return -1;
+ if (second.start_time) return 1;
+ return new Date(second.created_at).getTime() - new Date(first.created_at).getTime();
+}
diff --git a/frontend/src/features/schedule/domain/scheduleStatus.ts b/frontend/src/features/schedule/domain/scheduleStatus.ts
new file mode 100644
index 0000000..88259ec
--- /dev/null
+++ b/frontend/src/features/schedule/domain/scheduleStatus.ts
@@ -0,0 +1,28 @@
+import type { Schedule } from '@/contracts';
+
+export function nextStatusAfterToggle(status: Schedule['status']): Schedule['status'] | null {
+ if (status === 'deleted') return null;
+ return status === 'done' ? 'scheduled' : 'done';
+}
+
+export function markDeleted(schedule: Schedule, systemScheduleRefId: string | null): Schedule {
+ return {
+ ...schedule,
+ status: 'deleted',
+ system_schedule_ref_id: systemScheduleRefId,
+ updated_at: new Date().toISOString(),
+ };
+}
+
+export function withStatus(
+ schedule: Schedule,
+ status: Schedule['status'],
+ systemScheduleRefId: string | null,
+): Schedule {
+ return {
+ ...schedule,
+ status,
+ system_schedule_ref_id: systemScheduleRefId,
+ updated_at: new Date().toISOString(),
+ };
+}
diff --git a/frontend/src/features/schedule/editor/ClearFieldButton.tsx b/frontend/src/features/schedule/editor/ClearFieldButton.tsx
new file mode 100644
index 0000000..bbb3b24
--- /dev/null
+++ b/frontend/src/features/schedule/editor/ClearFieldButton.tsx
@@ -0,0 +1,23 @@
+import { Pressable, Text } from 'react-native';
+
+import { createSheetStyles as styles } from './createSheet.styles';
+
+export function ClearFieldButton({
+ accessibilityLabel,
+ onPress,
+}: {
+ accessibilityLabel: string;
+ onPress: () => void;
+}) {
+ return (
+
+ 清除
+
+ );
+}
diff --git a/frontend/src/features/schedule/editor/DateTimeField.tsx b/frontend/src/features/schedule/editor/DateTimeField.tsx
new file mode 100644
index 0000000..eda3ba5
--- /dev/null
+++ b/frontend/src/features/schedule/editor/DateTimeField.tsx
@@ -0,0 +1,56 @@
+import { useState } from 'react';
+import { Pressable, Text, View } from 'react-native';
+
+import { DatePickerSheet } from '@/shared/components/DatePickerSheet';
+import { TimePickerSheet } from '@/shared/components/TimePickerSheet';
+
+import { createSheetStyles as styles } from './createSheet.styles';
+import { formatDateValue, parsePickerValue, type PickerMode } from './datetime';
+
+/** 统一日期/时间字段:日期走 DatePickerSheet,时间走 TimePickerSheet。 */
+export function DateTimeField({
+ accessibilityLabel,
+ mode,
+ onChange,
+ placeholder,
+ value,
+}: {
+ accessibilityLabel: string;
+ mode: PickerMode;
+ onChange: (value: string) => void;
+ placeholder: string;
+ value: string;
+}) {
+ const [open, setOpen] = useState(false);
+ const selected = parsePickerValue(value, mode);
+
+ return (
+
+ setOpen(true)}
+ style={styles.pickerField}
+ >
+
+ {value || placeholder}
+
+
+ {mode === 'date' ? (
+ setOpen(false)}
+ onSelect={(date) => onChange(formatDateValue(date))}
+ selectedDate={selected}
+ visible={open}
+ />
+ ) : (
+ setOpen(false)}
+ onSelect={onChange}
+ selectedTime={selected}
+ visible={open}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/features/schedule/editor/StandardCreateModal.tsx b/frontend/src/features/schedule/editor/StandardCreateModal.tsx
new file mode 100644
index 0000000..2545ece
--- /dev/null
+++ b/frontend/src/features/schedule/editor/StandardCreateModal.tsx
@@ -0,0 +1,53 @@
+import { KeyboardAvoidingView, Modal, Platform, Pressable, View } from 'react-native';
+
+import type { ScheduleUpsertPayload as ScheduleDraft } from '@/contracts';
+
+import type { SavedLocation } from '../location';
+import { createSheetStyles as styles } from './createSheet.styles';
+import { StandardCreateSheet } from './StandardCreateSheet';
+
+export function StandardCreateModal({
+ initialDraft,
+ onClose,
+ onSave,
+ onUpsertLocation,
+ savedLocations,
+ visible,
+}: {
+ initialDraft: ScheduleDraft | null;
+ onClose: () => void;
+ onSave: (draft: ScheduleDraft) => void | Promise;
+ onUpsertLocation: (location: SavedLocation) => void;
+ savedLocations: SavedLocation[];
+ visible: boolean;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/schedule/editor/StandardCreateSheet.tsx b/frontend/src/features/schedule/editor/StandardCreateSheet.tsx
new file mode 100644
index 0000000..ec04947
--- /dev/null
+++ b/frontend/src/features/schedule/editor/StandardCreateSheet.tsx
@@ -0,0 +1,358 @@
+import { useState } from 'react';
+import { ChevronDown, ChevronUp, MapPin } from 'lucide-react-native';
+import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+import type { ScheduleType, ScheduleUpsertPayload as ScheduleDraft } from '@/contracts';
+import type { SavedLocation } from '../location';
+import { createSavedLocation, matchSavedLocation, LocationPickerSheet } from '../location';
+
+import {
+ currentTimezone,
+ dateAndTimeFromIso,
+ defaultCreateDateAndTime,
+ isoFromDateAndTime,
+ optionalNumber,
+} from './datetime';
+import { createSheetStyles as styles } from './createSheet.styles';
+import { ClearFieldButton } from './ClearFieldButton';
+import { DateTimeField } from './DateTimeField';
+
+export function StandardCreateSheet({
+ initialDraft,
+ onClose,
+ onSave,
+ onUpsertLocation,
+ savedLocations,
+}: {
+ initialDraft?: ScheduleDraft | null;
+ onClose: () => void;
+ onSave: (draft: ScheduleDraft) => void | Promise;
+ onUpsertLocation: (location: SavedLocation) => void;
+ savedLocations: SavedLocation[];
+}) {
+ const initialStart = initialDraft?.start_time
+ ? dateAndTimeFromIso(initialDraft.start_time)
+ : defaultCreateDateAndTime();
+ const initialEnd = dateAndTimeFromIso(initialDraft?.end_time);
+ const initialLocation =
+ matchSavedLocation(savedLocations, {
+ latitude: initialDraft?.latitude,
+ longitude: initialDraft?.longitude,
+ location_name: initialDraft?.location_name,
+ location_address: initialDraft?.location_address,
+ }) ??
+ (initialDraft?.latitude != null && initialDraft?.longitude != null
+ ? createSavedLocation({
+ address: initialDraft.location_address ?? '',
+ latitude: initialDraft.latitude,
+ longitude: initialDraft.longitude,
+ name: initialDraft.location_name ?? undefined,
+ })
+ : null);
+ const [title, setTitle] = useState(initialDraft?.title ?? '');
+ const [notes, setNotes] = useState(initialDraft?.notes ?? '');
+ const [date, setDate] = useState(initialStart.date);
+ const [start, setStart] = useState(initialStart.time);
+ const [end, setEnd] = useState(initialEnd.time);
+ const [selectedLocation, setSelectedLocation] = useState(initialLocation);
+ const [geofenceRadius, setGeofenceRadius] = useState(
+ String(initialDraft?.geofence_radius_meters ?? 100),
+ );
+ const [remindOffset, setRemindOffset] = useState(
+ String(initialDraft?.time_remind_offset_minutes ?? 0),
+ );
+ const [moreOpen, setMoreOpen] = useState(() =>
+ Boolean(
+ initialDraft?.notes ||
+ initialEnd.time ||
+ (initialDraft?.geofence_radius_meters != null &&
+ initialDraft.geofence_radius_meters !== 100) ||
+ (initialDraft?.time_remind_offset_minutes != null &&
+ initialDraft.time_remind_offset_minutes !== 0),
+ ),
+ );
+ const [locationPickerOpen, setLocationPickerOpen] = useState(false);
+ const [error, setError] = useState('');
+ const [saving, setSaving] = useState(false);
+ const editing = Boolean(initialDraft?.schedule_id);
+ const MoreIcon = moreOpen ? ChevronUp : ChevronDown;
+
+ const applyLocation = (location: SavedLocation | null) => {
+ setSelectedLocation(location);
+ };
+
+ const handleSave = async () => {
+ const normalizedTitle = title.trim();
+ const startTime = date && start ? isoFromDateAndTime(date, start) : null;
+ const endTime = startTime && end ? isoFromDateAndTime(date, end) : null;
+ const latitudeValue = selectedLocation?.latitude ?? null;
+ const longitudeValue = selectedLocation?.longitude ?? null;
+ const hasLocation = selectedLocation != null && latitudeValue != null && longitudeValue != null;
+ const radiusValue = optionalNumber(geofenceRadius);
+ const remindOffsetValue = optionalNumber(remindOffset);
+ // 有时间 → time;仅地点 → location;时间和地点都有仍按 time。
+ const resolvedType: ScheduleType = startTime ? 'time' : 'location';
+
+ if (!normalizedTitle) {
+ setError('请填写日程标题。');
+ return;
+ }
+ if (!startTime && !hasLocation) {
+ setError('请至少填写时间或地点。');
+ return;
+ }
+ if (date && !start) {
+ setError('已选日期时请一并选择开始时间。');
+ return;
+ }
+ if (start && !date) {
+ setError('已选时间时请一并选择日期。');
+ return;
+ }
+ if (startTime && new Date(startTime).getTime() <= Date.now()) {
+ setError('开始时间需晚于当前分钟,请选择下一分钟及以后。');
+ return;
+ }
+ if (endTime && startTime && new Date(endTime) < new Date(startTime)) {
+ setError('结束时间不能早于开始时间。');
+ return;
+ }
+ if (
+ hasLocation &&
+ (radiusValue === null || !Number.isInteger(radiusValue) || radiusValue <= 0)
+ ) {
+ setError('地理围栏半径必须是大于 0 的整数。');
+ return;
+ }
+ if (
+ remindOffsetValue === null ||
+ !Number.isInteger(remindOffsetValue) ||
+ remindOffsetValue < 0
+ ) {
+ setError('提前提醒分钟数必须是非负整数。');
+ return;
+ }
+
+ const nextDraft: ScheduleDraft = {
+ end_time: endTime,
+ geofence_armed: initialDraft?.geofence_armed ?? null,
+ geofence_radius_meters: hasLocation
+ ? radiusValue
+ : (initialDraft?.geofence_radius_meters ?? 100),
+ latitude: latitudeValue,
+ location_address: selectedLocation?.address ?? null,
+ location_name: selectedLocation?.name?.trim() || selectedLocation?.address || null,
+ longitude: longitudeValue,
+ notes: notes.trim() || null,
+ schedule_id: initialDraft?.schedule_id ?? null,
+ schedule_type: resolvedType,
+ source_mode: initialDraft?.source_mode ?? 'manual',
+ start_time: startTime,
+ time_remind_offset_minutes: remindOffsetValue,
+ timezone: startTime ? currentTimezone() : null,
+ title: normalizedTitle,
+ };
+ setError('');
+ setSaving(true);
+ try {
+ await onSave(nextDraft);
+ } catch (saveError) {
+ setError(saveError instanceof Error ? saveError.message : '保存失败,请稍后重试。');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {editing ? 'EDIT SCHEDULE' : 'STANDARD SCHEDULE'}
+
+ {editing ? '编辑日程' : '添加日程'}
+
+
+ ×
+
+
+
+ 标题(必填)
+
+
+ 日期
+
+
+
+
+ {date ? (
+ {
+ setDate('');
+ setStart('');
+ setEnd('');
+ }}
+ />
+ ) : null}
+
+
+ 开始时间
+
+
+
+
+ {start ? (
+ {
+ setStart('');
+ setEnd('');
+ }}
+ />
+ ) : null}
+
+
+ 地点
+
+ setLocationPickerOpen(true)}
+ style={styles.locationFieldMain}
+ >
+
+
+
+
+
+ {selectedLocation
+ ? (selectedLocation.name ?? selectedLocation.address)
+ : '从常用地点中选择'}
+
+ {selectedLocation ? (
+
+ {selectedLocation.address}
+
+ ) : (
+ 时间或地点至少填一项
+ )}
+
+
+ {selectedLocation ? (
+ applyLocation(null)} />
+ ) : null}
+
+
+ setMoreOpen((open) => !open)}
+ style={[styles.moreToggle, moreOpen && styles.moreToggleActive]}
+ >
+
+ 更多信息
+
+
+
+
+ {moreOpen ? (
+
+ 备注(可选)
+
+ 结束时间(可选)
+
+ 提前提醒(分钟)
+
+ {selectedLocation ? (
+ <>
+ 围栏半径(米)
+
+ >
+ ) : null}
+
+ ) : null}
+
+ {error ? {error} : null}
+
+
+ {saving ? '正在保存…' : editing ? '保存修改' : '添加日程'}
+
+
+
+
+ setLocationPickerOpen(false)}
+ onSelect={applyLocation}
+ onUpsertLocation={onUpsertLocation}
+ selectedId={selectedLocation?.id ?? null}
+ visible={locationPickerOpen}
+ />
+
+ );
+}
diff --git a/frontend/src/features/schedule/editor/createSheet.styles.ts b/frontend/src/features/schedule/editor/createSheet.styles.ts
new file mode 100644
index 0000000..1e5c632
--- /dev/null
+++ b/frontend/src/features/schedule/editor/createSheet.styles.ts
@@ -0,0 +1,164 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const createSheetStyles = StyleSheet.create({
+ modalBackdrop: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(22,32,28,0.52)',
+ flex: 1,
+ justifyContent: 'flex-end',
+ },
+ modalDismiss: { alignSelf: 'stretch', flex: 1 },
+ modalKeyboardAvoider: { flex: 1 },
+ editModalBackdrop: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(22,32,28,0.52)',
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 18,
+ },
+ editModalDismiss: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0 },
+ standardSheet: {
+ backgroundColor: colors.surface,
+ borderTopLeftRadius: 26,
+ borderTopRightRadius: 26,
+ padding: 18,
+ paddingBottom: 28,
+ maxWidth: 430,
+ maxHeight: '92%',
+ width: '100%',
+ },
+ standardDialog: {
+ borderRadius: 20,
+ maxHeight: '88%',
+ paddingBottom: 20,
+ paddingTop: 20,
+ },
+ sheetCloseText: { color: colors.ink, fontSize: 17 },
+ fieldLabel: { color: colors.ink, fontSize: 11, fontWeight: '700', marginBottom: 7, marginTop: 9 },
+ formInputControl: {
+ borderColor: colors.line,
+ borderRadius: 10,
+ borderWidth: 1,
+ color: colors.ink,
+ fontSize: 13,
+ height: 48,
+ paddingHorizontal: 13,
+ },
+ formInputMultiline: {
+ height: 72,
+ paddingTop: 12,
+ textAlignVertical: 'top',
+ },
+ pickerField: {
+ alignItems: 'center',
+ borderColor: colors.line,
+ borderRadius: 10,
+ borderWidth: 1,
+ flexDirection: 'row',
+ height: 48,
+ paddingHorizontal: 13,
+ },
+ pickerFieldText: { color: colors.ink, flex: 1, fontSize: 13, fontWeight: '600' },
+ pickerFieldPlaceholder: { color: colors.muted, fontWeight: '500' },
+ formError: { color: '#B66752', fontSize: 11, marginTop: 10 },
+ moreToggle: {
+ alignItems: 'center',
+ backgroundColor: colors.surfaceTint,
+ borderRadius: 12,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 14,
+ minHeight: 44,
+ paddingHorizontal: 12,
+ },
+ moreToggleActive: { backgroundColor: colors.limeSoft },
+ moreToggleText: { color: '#7D8982', fontSize: 13, fontWeight: '800' },
+ moreToggleTextActive: { color: '#52745D' },
+ moreSection: { marginTop: 2 },
+ fieldWithClear: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
+ },
+ fieldWithClearMain: {
+ flex: 1,
+ minWidth: 0,
+ },
+ locationField: {
+ alignItems: 'center',
+ borderColor: colors.line,
+ borderRadius: 10,
+ borderWidth: 1,
+ flexDirection: 'row',
+ minHeight: 52,
+ paddingHorizontal: 10,
+ paddingVertical: 8,
+ },
+ locationFieldMain: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ },
+ locationFieldIcon: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 9,
+ height: 32,
+ justifyContent: 'center',
+ width: 32,
+ },
+ locationFieldCopy: { flex: 1, marginLeft: 10 },
+ locationFieldTitle: { color: colors.ink, fontSize: 13, fontWeight: '700' },
+ locationFieldPlaceholder: { color: colors.muted, fontWeight: '500' },
+ locationFieldHint: { color: colors.sub, fontSize: 10, marginTop: 2 },
+ locationClear: {
+ backgroundColor: colors.surfaceTint,
+ borderRadius: 8,
+ paddingHorizontal: 8,
+ paddingVertical: 6,
+ },
+ locationClearText: { color: colors.sub, fontSize: 11, fontWeight: '700' },
+ standardPrimary: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 12,
+ justifyContent: 'center',
+ marginTop: 14,
+ minHeight: 50,
+ },
+ standardPrimaryText: { color: colors.surface, fontSize: 14, fontWeight: '800' },
+ standardFormScroll: { flexGrow: 0, flexShrink: 1 },
+ standardFormContent: { paddingBottom: 2 },
+ sheetHandle: {
+ alignSelf: 'center',
+ backgroundColor: '#D8D6CF',
+ borderRadius: 3,
+ height: 4,
+ marginBottom: 18,
+ width: 34,
+ },
+ sheetHeader: {
+ alignItems: 'flex-end',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 16,
+ },
+ sheetEyebrow: {
+ color: colors.muted,
+ fontSize: 10,
+ fontWeight: '700',
+ letterSpacing: 0,
+ marginBottom: 6,
+ },
+ sheetTitle: { color: colors.ink, fontSize: 23, fontWeight: '800' },
+ sheetClose: {
+ alignItems: 'center',
+ backgroundColor: colors.surfaceTint,
+ borderRadius: 11,
+ height: 36,
+ justifyContent: 'center',
+ width: 36,
+ },
+});
diff --git a/frontend/src/features/schedule/editor/datetime.ts b/frontend/src/features/schedule/editor/datetime.ts
new file mode 100644
index 0000000..0aa76f7
--- /dev/null
+++ b/frontend/src/features/schedule/editor/datetime.ts
@@ -0,0 +1,79 @@
+import { formatTimeValue } from '@/shared/utils/date';
+
+export type PickerMode = 'date' | 'time';
+
+export function parseDateValue(value: string) {
+ const parts = value
+ .split(/[^0-9]+/)
+ .filter(Boolean)
+ .map(Number);
+ const next = new Date();
+ next.setHours(0, 0, 0, 0);
+ if (parts.length === 3 && parts[0] >= 1 && parts[1] >= 1 && parts[1] <= 12 && parts[2] >= 1) {
+ next.setFullYear(parts[0], parts[1] - 1, parts[2]);
+ }
+ return next;
+}
+
+export function parseTimeValue(value: string) {
+ const parsed = value.match(/^(\d{1,2}):(\d{2})$/);
+ const next = new Date();
+ if (parsed) next.setHours(Number(parsed[1]), Number(parsed[2]), 0, 0);
+ else next.setSeconds(0, 0);
+ return next;
+}
+
+export function parsePickerValue(value: string, mode: PickerMode) {
+ return mode === 'date' ? parseDateValue(value) : parseTimeValue(value);
+}
+
+export function formatDateValue(value: Date) {
+ return `${value.getFullYear()} / ${String(value.getMonth() + 1).padStart(2, '0')} / ${String(value.getDate()).padStart(2, '0')}`;
+}
+
+export function dateAndTimeFromIso(value?: string | null) {
+ if (!value) return { date: '', time: '' };
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) return { date: '', time: '' };
+ return { date: formatDateValue(parsed), time: formatTimeValue(parsed) };
+}
+
+/** 新建日程默认选下一分钟(当前分钟已过去/不允许创建)。 */
+export function defaultCreateDateAndTime(now = new Date()) {
+ const next = new Date(now);
+ next.setSeconds(0, 0);
+ next.setMinutes(next.getMinutes() + 1);
+ return { date: formatDateValue(next), time: formatTimeValue(next) };
+}
+
+export function isoFromDateAndTime(dateValue: string, timeValue: string) {
+ const timeParts = timeValue.match(/^(\d{1,2}):(\d{2})$/);
+ if (!timeParts) return null;
+
+ const date = parseDateValue(dateValue);
+ // parseDateValue 在非法输入时回退到「今天」;需确认输入本身合法。
+ const parts = dateValue
+ .split(/[^0-9]+/)
+ .filter(Boolean)
+ .map(Number);
+ if (parts.length !== 3 || parts[0] < 1 || parts[1] < 1 || parts[1] > 12 || parts[2] < 1) {
+ return null;
+ }
+
+ date.setHours(Number(timeParts[1]), Number(timeParts[2]), 0, 0);
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
+}
+
+export function optionalNumber(value: string) {
+ if (!value.trim()) return null;
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : null;
+}
+
+export function currentTimezone() {
+ try {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
+ } catch {
+ return null;
+ }
+}
diff --git a/frontend/src/features/schedule/hooks/useScheduleCommands.tsx b/frontend/src/features/schedule/hooks/useScheduleCommands.tsx
new file mode 100644
index 0000000..016f196
--- /dev/null
+++ b/frontend/src/features/schedule/hooks/useScheduleCommands.tsx
@@ -0,0 +1,213 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ useSyncExternalStore,
+ type ReactNode,
+} from 'react';
+
+import type {
+ ConnectionStatus,
+ Schedule,
+ ScheduleUpsertPayload as ScheduleDraft,
+} from '@/contracts';
+
+import type { AlarmPort } from '../application/AlarmPort';
+import type { ScheduleConflictNotifier } from '../application/ScheduleNotificationPort';
+import { ScheduleService } from '../application/ScheduleService';
+import { ScheduleCache } from '../data/ScheduleCache';
+import type { ScheduleTransport } from '../data/ScheduleTransport';
+import { WsScheduleRepository } from '../data/WsScheduleRepository';
+
+type ScheduleMutationState = {
+ status: 'idle' | 'pending' | 'error';
+ error: string | null;
+ pendingId: string | null;
+};
+
+const IDLE_MUTATION: ScheduleMutationState = {
+ status: 'idle',
+ error: null,
+ pendingId: null,
+};
+
+const EMPTY_SCHEDULES: Schedule[] = [];
+
+type ReadySnapshot = {
+ service: ScheduleService;
+ sessionEpoch: number;
+ userId: string;
+};
+
+type ScheduleCommandsValue = {
+ items: Schedule[];
+ ready: boolean;
+ mutation: ScheduleMutationState;
+ saveDraft: (draft: ScheduleDraft) => Promise;
+ toggleScheduleDone: (schedule: Schedule) => Promise;
+ deleteSchedule: (schedule: Schedule) => Promise;
+ service: ScheduleService | null;
+};
+
+const ScheduleCommandsContext = createContext(null);
+
+export type ScheduleProviderProps = {
+ alarmAdapter: AlarmPort;
+ children: ReactNode;
+ /** 由 app 从 SessionProvider 注入,feature 不反向依赖 app。 */
+ client: ScheduleTransport | null;
+ /** 当前 session 的连接状态;断线期间禁止写操作。 */
+ connectionStatus: ConnectionStatus;
+ /** App-owned feedback for server-reported schedule conflicts. */
+ notifyConflicts?: ScheduleConflictNotifier;
+ userId: string | null;
+ /** 每次 session.ready 递增;用于重连后 resync。 */
+ sessionEpoch: number;
+};
+
+export function ScheduleProvider({
+ alarmAdapter,
+ children,
+ client,
+ connectionStatus,
+ notifyConflicts,
+ userId,
+ sessionEpoch,
+}: ScheduleProviderProps) {
+ const [readySnapshot, setReadySnapshot] = useState(null);
+ const [mutation, setMutation] = useState(IDLE_MUTATION);
+
+ const service = useMemo(() => {
+ if (!client) return null;
+ const cache = new ScheduleCache();
+ const repository = new WsScheduleRepository(client);
+ return new ScheduleService({
+ alarmAdapter,
+ repository,
+ cache,
+ getUserId: () => {
+ if (!userId) throw new Error('会话身份尚未就绪');
+ return userId;
+ },
+ notifyConflicts,
+ });
+ }, [alarmAdapter, client, notifyConflicts, userId]);
+
+ const subscribeToItems = useCallback(
+ (onStoreChange: () => void) => {
+ if (!service) return () => undefined;
+ return service.subscribe(() => onStoreChange());
+ },
+ [service],
+ );
+
+ const getItemsSnapshot = useCallback(() => service?.getItems() ?? EMPTY_SCHEDULES, [service]);
+
+ const items = useSyncExternalStore(subscribeToItems, getItemsSnapshot, getItemsSnapshot);
+
+ useEffect(() => {
+ return () => service?.dispose();
+ }, [service]);
+
+ useEffect(() => {
+ if (!service || !userId || sessionEpoch === 0) return;
+ let cancelled = false;
+ void (async () => {
+ try {
+ await service.resync();
+ if (!cancelled) setReadySnapshot({ service, sessionEpoch, userId });
+ } catch (error) {
+ if (!cancelled) {
+ setMutation({
+ status: 'error',
+ error: error instanceof Error ? error.message : '加载日程失败',
+ pendingId: null,
+ });
+ }
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [service, userId, sessionEpoch]);
+
+ const ready = Boolean(
+ service &&
+ connectionStatus === 'ready' &&
+ userId &&
+ readySnapshot?.service === service &&
+ readySnapshot.userId === userId &&
+ readySnapshot.sessionEpoch === sessionEpoch,
+ );
+
+ const runMutation = useCallback(
+ async (
+ pendingId: string,
+ fallbackError: string,
+ action: (activeService: ScheduleService) => Promise,
+ ): Promise => {
+ if (!service || !ready) {
+ const error = new Error('日程服务尚未连接,请稍后重试');
+ setMutation({ status: 'error', error: error.message, pendingId });
+ throw error;
+ }
+ setMutation({ status: 'pending', error: null, pendingId });
+ try {
+ const result = await action(service);
+ setMutation(IDLE_MUTATION);
+ return result;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : fallbackError;
+ setMutation({ status: 'error', error: message, pendingId });
+ throw error instanceof Error ? error : new Error(message);
+ }
+ },
+ [ready, service],
+ );
+
+ const saveDraft = useCallback(
+ (draft: ScheduleDraft) =>
+ runMutation(draft.schedule_id ?? 'new', '保存失败', (active) => active.saveDraft(draft)),
+ [runMutation],
+ );
+
+ const toggleScheduleDone = useCallback(
+ (schedule: Schedule) =>
+ runMutation(schedule.id, '更新失败', (active) => active.toggleDone(schedule)),
+ [runMutation],
+ );
+
+ const deleteSchedule = useCallback(
+ (schedule: Schedule) =>
+ runMutation(schedule.id, '删除失败', (active) => active.deleteSchedule(schedule)),
+ [runMutation],
+ );
+
+ const value = useMemo(
+ () => ({
+ items,
+ ready,
+ mutation,
+ saveDraft,
+ toggleScheduleDone,
+ deleteSchedule,
+ service,
+ }),
+ [deleteSchedule, items, mutation, ready, saveDraft, service, toggleScheduleDone],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useScheduleCommands(): ScheduleCommandsValue {
+ const value = useContext(ScheduleCommandsContext);
+ if (!value) {
+ throw new Error('useScheduleCommands must be used within ScheduleProvider');
+ }
+ return value;
+}
diff --git a/frontend/src/features/schedule/index.ts b/frontend/src/features/schedule/index.ts
new file mode 100644
index 0000000..b4763c9
--- /dev/null
+++ b/frontend/src/features/schedule/index.ts
@@ -0,0 +1,9 @@
+export { ScheduleScreen } from './screens/ScheduleScreen';
+export { StandardCreateModal } from './editor/StandardCreateModal';
+export { ScheduleProvider, useScheduleCommands } from './hooks/useScheduleCommands';
+export type { AlarmPort } from './application/AlarmPort';
+export type { ScheduleConflictNotifier } from './application/ScheduleNotificationPort';
+export { scheduleDraftFromVoiceParse, upsertDraftForSchedule } from './data/adapters';
+export type { Schedule, ScheduleUpsertPayload as ScheduleDraft } from '@/contracts';
+export type { SavedLocation } from './location';
+export { useSessionSavedLocations } from './location';
diff --git a/frontend/src/features/schedule/location/AddressEditorSheet.styles.ts b/frontend/src/features/schedule/location/AddressEditorSheet.styles.ts
new file mode 100644
index 0000000..7410647
--- /dev/null
+++ b/frontend/src/features/schedule/location/AddressEditorSheet.styles.ts
@@ -0,0 +1,57 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const addressEditorStyles = StyleSheet.create({
+ sheet: {
+ paddingHorizontal: 20,
+ },
+ fieldLabel: { color: colors.ink, fontSize: 12, fontWeight: '800', marginTop: 20 },
+ input: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#C7D0C9',
+ borderRadius: 14,
+ borderWidth: 1,
+ color: colors.ink,
+ fontSize: 14,
+ height: 52,
+ marginTop: 8,
+ outlineColor: 'transparent',
+ outlineStyle: 'solid',
+ outlineWidth: 0,
+ paddingHorizontal: 13,
+ },
+ error: { color: '#A85F4E', fontSize: 11, marginTop: 6 },
+ mapField: {
+ alignItems: 'center',
+ backgroundColor: '#F8FAF7',
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ flexDirection: 'row',
+ marginTop: 8,
+ minHeight: 66,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ },
+ mapIcon: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 10,
+ height: 36,
+ justifyContent: 'center',
+ width: 36,
+ },
+ mapCopy: { flex: 1, marginLeft: 10 },
+ mapTitle: { color: colors.ink, fontSize: 13, lineHeight: 18 },
+ mapHint: { color: colors.sub, fontSize: 10, marginTop: 3 },
+ primary: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 13,
+ height: 50,
+ justifyContent: 'center',
+ marginTop: 17,
+ },
+ primaryText: { color: colors.surface, fontSize: 13, fontWeight: '800' },
+});
diff --git a/frontend/src/features/schedule/location/AddressEditorSheet.tsx b/frontend/src/features/schedule/location/AddressEditorSheet.tsx
new file mode 100644
index 0000000..bdde1fd
--- /dev/null
+++ b/frontend/src/features/schedule/location/AddressEditorSheet.tsx
@@ -0,0 +1,129 @@
+import { useState } from 'react';
+import { MapPin } from 'lucide-react-native';
+import { Modal, Pressable, Text, TextInput, View } from 'react-native';
+
+import { BottomSheetFrame } from '@/shared/components/BottomSheetFrame';
+import { colors } from '@/shared/theme';
+
+import { addressEditorStyles as styles } from './AddressEditorSheet.styles';
+import { MapPicker, type MapLocation } from './MapPicker';
+
+type AddressEditorSheetProps = {
+ initialLocation?: MapLocation | null;
+ onClose: () => void;
+ onSave: (location: MapLocation) => void;
+ title: string;
+ visible: boolean;
+};
+
+export function AddressEditorSheet({
+ initialLocation = null,
+ onClose,
+ onSave,
+ title,
+ visible,
+}: AddressEditorSheetProps) {
+ const [mapOpen, setMapOpen] = useState(false);
+ const [pendingLocation, setPendingLocation] = useState(initialLocation);
+ const [locationName, setLocationName] = useState(initialLocation?.name ?? '');
+ const [formError, setFormError] = useState('');
+ const [syncedVisible, setSyncedVisible] = useState(visible);
+ const [syncedInitial, setSyncedInitial] = useState(initialLocation);
+
+ if (visible !== syncedVisible || initialLocation !== syncedInitial) {
+ setSyncedVisible(visible);
+ setSyncedInitial(initialLocation);
+ if (visible) {
+ setPendingLocation(initialLocation);
+ setLocationName(initialLocation?.name ?? '');
+ setFormError('');
+ setMapOpen(false);
+ }
+ }
+
+ const handleClose = () => {
+ setMapOpen(false);
+ setFormError('');
+ onClose();
+ };
+
+ const handleSave = () => {
+ if (!pendingLocation) {
+ setFormError('请选择一个地图位置');
+ return;
+ }
+ const nextName = locationName.trim();
+ onSave({ ...pendingLocation, name: nextName || undefined });
+ };
+
+ return (
+ <>
+
+ 地点名称
+
+ 地图位置
+ {
+ setFormError('');
+ setMapOpen(true);
+ }}
+ style={styles.mapField}
+ >
+
+
+
+
+
+ {pendingLocation?.address ?? '请选择地点'}
+
+ {pendingLocation ? '点击重新选择' : '点击打开地图'}
+
+
+ {formError ? {formError} : null}
+
+ 保存地址
+
+
+
+ setMapOpen(false)}
+ visible={visible && mapOpen}
+ >
+ setMapOpen(false)}
+ onConfirm={(location) => {
+ setMapOpen(false);
+ setPendingLocation(location);
+ setFormError('');
+ }}
+ />
+
+ >
+ );
+}
diff --git a/frontend/src/features/schedule/location/LocationPickerSheet.styles.ts b/frontend/src/features/schedule/location/LocationPickerSheet.styles.ts
new file mode 100644
index 0000000..4b2cde5
--- /dev/null
+++ b/frontend/src/features/schedule/location/LocationPickerSheet.styles.ts
@@ -0,0 +1,60 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const locationPickerStyles = StyleSheet.create({
+ sheet: {
+ maxHeight: '78%',
+ paddingHorizontal: 20,
+ },
+ list: { flexGrow: 0, flexShrink: 1 },
+ listContent: { gap: 8, paddingBottom: 8 },
+ empty: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderColor: '#D7E6B2',
+ borderRadius: 14,
+ borderWidth: 1,
+ paddingHorizontal: 16,
+ paddingVertical: 22,
+ },
+ emptyTitle: { color: colors.deep, fontSize: 14, fontWeight: '800' },
+ emptyHint: { color: colors.sub, fontSize: 11, marginTop: 6 },
+ item: {
+ alignItems: 'center',
+ backgroundColor: '#F8FAF7',
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ flexDirection: 'row',
+ minHeight: 68,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ },
+ itemSelected: {
+ backgroundColor: colors.limeSoft,
+ borderColor: '#C7D69A',
+ },
+ itemIcon: {
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderRadius: 10,
+ height: 36,
+ justifyContent: 'center',
+ width: 36,
+ },
+ itemCopy: { flex: 1, marginLeft: 10 },
+ itemName: { color: colors.ink, fontSize: 14, fontWeight: '800' },
+ itemAddress: { color: colors.sub, fontSize: 12, lineHeight: 17, marginTop: 3 },
+ addButton: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 13,
+ flexDirection: 'row',
+ gap: 8,
+ height: 50,
+ justifyContent: 'center',
+ marginTop: 12,
+ },
+ addButtonText: { color: colors.surface, fontSize: 13, fontWeight: '800' },
+});
diff --git a/frontend/src/features/schedule/location/LocationPickerSheet.tsx b/frontend/src/features/schedule/location/LocationPickerSheet.tsx
new file mode 100644
index 0000000..c472124
--- /dev/null
+++ b/frontend/src/features/schedule/location/LocationPickerSheet.tsx
@@ -0,0 +1,112 @@
+import { useState } from 'react';
+import { MapPin, Plus } from 'lucide-react-native';
+import { Pressable, ScrollView, Text, View } from 'react-native';
+
+import { BottomSheetFrame } from '@/shared/components/BottomSheetFrame';
+import { colors } from '@/shared/theme';
+
+import { AddressEditorSheet } from './AddressEditorSheet';
+import { locationPickerStyles as styles } from './LocationPickerSheet.styles';
+import type { SavedLocation } from './types';
+import { createSavedLocation } from './utils';
+
+type LocationPickerSheetProps = {
+ locations: SavedLocation[];
+ onClose: () => void;
+ onSelect: (location: SavedLocation) => void;
+ onUpsertLocation: (location: SavedLocation) => void;
+ selectedId?: string | null;
+ visible: boolean;
+};
+
+export function LocationPickerSheet({
+ locations,
+ onClose,
+ onSelect,
+ onUpsertLocation,
+ selectedId = null,
+ visible,
+}: LocationPickerSheetProps) {
+ const [editorOpen, setEditorOpen] = useState(false);
+
+ const handleClose = () => {
+ setEditorOpen(false);
+ onClose();
+ };
+
+ return (
+ <>
+
+
+ {locations.length === 0 ? (
+
+ 还没有常用地点
+ 先添加一个地点,再用于日程提醒
+
+ ) : (
+ locations.map((location) => {
+ const selected = location.id === selectedId;
+ return (
+ {
+ onSelect(location);
+ handleClose();
+ }}
+ style={[styles.item, selected && styles.itemSelected]}
+ >
+
+
+
+
+ {location.name ?? '未命名地点'}
+
+ {location.address}
+
+
+
+ );
+ })
+ )}
+
+
+ setEditorOpen(true)}
+ style={styles.addButton}
+ >
+
+ 添加地点
+
+
+
+ setEditorOpen(false)}
+ onSave={(location) => {
+ const saved = createSavedLocation(location);
+ onUpsertLocation(saved);
+ onSelect(saved);
+ setEditorOpen(false);
+ onClose();
+ }}
+ title="添加地点"
+ visible={visible && editorOpen}
+ />
+ >
+ );
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/MapPicker.native.tsx b/frontend/src/features/schedule/location/MapPicker/MapPicker.native.tsx
new file mode 100644
index 0000000..e333f33
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/MapPicker.native.tsx
@@ -0,0 +1,184 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { View } from 'react-native';
+import { WebView, WebViewMessageEvent } from 'react-native-webview';
+
+import {
+ BaiduMapBridgeMessage,
+ buildBaiduMapDocument,
+ BAIDU_MAP_AK,
+ createCoordinateLocation,
+} from './baidu';
+import { MapPickerOverlay } from './Overlay';
+import { mapPickerStyles as styles } from './styles';
+import type { MapLocation, MapPickerProps } from './types';
+
+const SEARCH_TIMEOUT_MS = 8000;
+
+type PendingSearch = {
+ reject: (error: Error) => void;
+ resolve: (locations: MapLocation[]) => void;
+ timeout: ReturnType;
+};
+
+export function MapPicker({ initialLocation, onCancel, onConfirm }: MapPickerProps) {
+ const webViewRef = useRef(null);
+ const pendingSearchRef = useRef(null);
+ const [selection, setSelection] = useState(initialLocation);
+ const [locating, setLocating] = useState(false);
+ const [locationError, setLocationError] = useState(null);
+ const [mapReady, setMapReady] = useState(false);
+ const [mapError, setMapError] = useState(
+ BAIDU_MAP_AK ? null : '缺少百度地图浏览器端密钥,请完成地图服务配置。',
+ );
+ const document = useMemo(
+ () => buildBaiduMapDocument(BAIDU_MAP_AK, initialLocation),
+ [initialLocation],
+ );
+
+ useEffect(() => {
+ return () => {
+ if (pendingSearchRef.current) {
+ clearTimeout(pendingSearchRef.current.timeout);
+ pendingSearchRef.current = null;
+ }
+ };
+ }, []);
+
+ const failPendingSearch = useCallback((message: string) => {
+ const pending = pendingSearchRef.current;
+ if (!pending) return;
+ clearTimeout(pending.timeout);
+ pending.reject(new Error(message));
+ pendingSearchRef.current = null;
+ }, []);
+
+ const handleMessage = useCallback(
+ ({ nativeEvent }: WebViewMessageEvent) => {
+ let message: BaiduMapBridgeMessage;
+ try {
+ message = JSON.parse(nativeEvent.data) as BaiduMapBridgeMessage;
+ } catch {
+ return;
+ }
+
+ if (message.type === 'map-ready') {
+ setMapReady(true);
+ setMapError(null);
+ if (!initialLocation) {
+ webViewRef.current?.injectJavaScript('window.__timeflowLocate(); true;');
+ }
+ return;
+ }
+ if (message.type === 'map-error') {
+ setMapReady(false);
+ setMapError(message.message);
+ failPendingSearch(message.message);
+ return;
+ }
+ if (message.type === 'selecting') {
+ setLocationError(null);
+ setSelection(createCoordinateLocation(message.latitude, message.longitude));
+ setLocating(true);
+ return;
+ }
+ if (message.type === 'selected') {
+ setSelection(message.location);
+ setLocating(false);
+ setLocationError(null);
+ return;
+ }
+ if (message.type === 'location-error') {
+ setLocating(false);
+ setLocationError(message.message);
+ return;
+ }
+ if (message.type === 'search-results') {
+ const pending = pendingSearchRef.current;
+ if (!pending) return;
+ clearTimeout(pending.timeout);
+ pending.resolve(message.results);
+ pendingSearchRef.current = null;
+ return;
+ }
+ if (message.type === 'search-error') {
+ failPendingSearch('Baidu place search failed');
+ }
+ },
+ [failPendingSearch, initialLocation],
+ );
+
+ const searchLocations = useCallback(
+ (query: string) => {
+ return new Promise((resolve, reject) => {
+ if (!mapReady || !webViewRef.current) {
+ reject(new Error('Baidu map is not ready'));
+ return;
+ }
+
+ failPendingSearch('A newer search replaced this request');
+ const timeout = setTimeout(() => {
+ failPendingSearch('Baidu place search timed out');
+ }, SEARCH_TIMEOUT_MS);
+ pendingSearchRef.current = { reject, resolve, timeout };
+ webViewRef.current.injectJavaScript(
+ `window.__timeflowSearch(${JSON.stringify(query)}); true;`,
+ );
+ });
+ },
+ [failPendingSearch, mapReady],
+ );
+
+ const selectSearchResult = (location: MapLocation) => {
+ setLocationError(null);
+ setLocating(false);
+ setSelection(location);
+ webViewRef.current?.injectJavaScript(
+ `window.__timeflowSelect(${location.longitude}, ${location.latitude}); true;`,
+ );
+ };
+
+ const locateCurrentPosition = () => {
+ setLocationError(null);
+ setLocating(true);
+ webViewRef.current?.injectJavaScript('window.__timeflowLocate(); true;');
+ };
+
+ return (
+
+ {BAIDU_MAP_AK ? (
+ {
+ setMapReady(false);
+ setMapError('地图加载失败,请检查网络或百度地图密钥配置。');
+ }}
+ onMessage={handleMessage}
+ originWhitelist={['https://*']}
+ ref={webViewRef}
+ scrollEnabled={false}
+ setSupportMultipleWindows={false}
+ source={{ baseUrl: 'https://timeflow.local/', html: document }}
+ style={styles.mapCanvas}
+ />
+ ) : (
+
+ )}
+ selection && onConfirm(selection)}
+ onSearch={searchLocations}
+ onSelectSearchResult={selectSearchResult}
+ selection={selection}
+ />
+
+ );
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/MapPicker.tsx b/frontend/src/features/schedule/location/MapPicker/MapPicker.tsx
new file mode 100644
index 0000000..4e817d2
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/MapPicker.tsx
@@ -0,0 +1,284 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { load as loadBaiduMap } from '@baidumap/jsapi-loader';
+import { View } from 'react-native';
+
+import {
+ BAIDU_MAP_AK,
+ createCoordinateLocation,
+ createReverseGeocodeGate,
+ readablePoiAddress,
+ SHANGHAI_CENTER,
+} from './baidu';
+import { MapPickerOverlay } from './Overlay';
+import { mapPickerStyles as styles } from './styles';
+import type { MapLocation, MapPickerProps } from './types';
+
+const MAP_REQUEST_TIMEOUT_MS = 6000;
+const MAP_LOAD_TIMEOUT_MS = 5000;
+const MARKER_SVG = encodeURIComponent(
+ '',
+);
+
+export function MapPicker({ initialLocation, onCancel, onConfirm }: MapPickerProps) {
+ const mapHostRef = useRef(null);
+ const bmapRef = useRef(null);
+ const mapRef = useRef(null);
+ const markerRef = useRef(null);
+ const requestRef = useRef(0);
+ const reverseGeocodeGateRef = useRef(createReverseGeocodeGate());
+ const [selection, setSelection] = useState(initialLocation);
+ const [locating, setLocating] = useState(false);
+ const [locationError, setLocationError] = useState(null);
+ const [mapReady, setMapReady] = useState(false);
+ const [mapError, setMapError] = useState(
+ BAIDU_MAP_AK ? null : '缺少百度地图浏览器端密钥,请完成地图服务配置。',
+ );
+
+ const moveMarker = useCallback((location: MapLocation) => {
+ const BMapApi = bmapRef.current;
+ const map = mapRef.current;
+ if (!BMapApi || !map) return;
+
+ const point = new BMapApi.Point(location.longitude, location.latitude);
+ if (markerRef.current) {
+ markerRef.current.setPosition(point);
+ return;
+ }
+
+ const icon = new BMapApi.Icon(
+ `data:image/svg+xml;charset=utf-8,${MARKER_SVG}`,
+ new BMapApi.Size(28, 28),
+ { anchor: new BMapApi.Size(14, 14) },
+ );
+ markerRef.current = new BMapApi.Marker(point, { icon });
+ map.addOverlay(markerRef.current);
+ }, []);
+
+ const selectCoordinates = useCallback(
+ (latitude: number, longitude: number) => {
+ const BMapApi = bmapRef.current;
+ if (!BMapApi) return;
+
+ const requestId = requestRef.current + 1;
+ requestRef.current = requestId;
+ const pendingLocation = createCoordinateLocation(latitude, longitude);
+
+ setLocationError(null);
+ setSelection(pendingLocation);
+ moveMarker(pendingLocation);
+ setLocating(true);
+
+ reverseGeocodeGateRef.current.schedule({ latitude, longitude, requestId }, (job) => {
+ if (requestRef.current !== job.requestId) return;
+
+ return new Promise((resolve) => {
+ let completed = false;
+ const finish = (address?: string) => {
+ if (completed) {
+ resolve();
+ return;
+ }
+ completed = true;
+ window.clearTimeout(timeout);
+ if (requestRef.current === job.requestId) {
+ if (address?.trim()) {
+ setSelection({
+ ...createCoordinateLocation(job.latitude, job.longitude),
+ address: address.trim(),
+ });
+ }
+ setLocating(false);
+ }
+ resolve();
+ };
+
+ const timeout = window.setTimeout(() => finish(), MAP_REQUEST_TIMEOUT_MS);
+ const geocoder = new BMapApi.Geocoder({ language: 'zh-CN' });
+ geocoder.getLocation(
+ new BMapApi.Point(job.longitude, job.latitude),
+ (result: BMap.GeocoderResult | null) => finish(result?.address),
+ );
+ });
+ });
+ },
+ [moveMarker],
+ );
+
+ const locateCurrentPosition = useCallback(() => {
+ const BMapApi = bmapRef.current;
+ const map = mapRef.current;
+ if (!BMapApi || !map) return;
+
+ setLocationError(null);
+ setLocating(true);
+ const geolocation = new BMapApi.Geolocation();
+ geolocation.getCurrentPosition(
+ (result) => {
+ if (geolocation.getStatus() !== 0 || !result?.point) {
+ setLocating(false);
+ setLocationError('无法获取当前位置,请允许定位权限后重试。');
+ return;
+ }
+
+ map.setCenter(result.point, { noAnimation: false });
+ map.setZoom(17, { noAnimation: false });
+ selectCoordinates(result.point.lat, result.point.lng);
+ },
+ { enableHighAccuracy: true },
+ );
+ }, [selectCoordinates]);
+
+ useEffect(() => {
+ const host = mapHostRef.current as unknown as HTMLElement | null;
+ if (!host) return;
+
+ if (!BAIDU_MAP_AK) return;
+
+ const reverseGeocodeGate = reverseGeocodeGateRef.current;
+ let disposed = false;
+ const loadTimeout = window.setTimeout(() => {
+ if (!disposed) {
+ setMapError('请在百度地图控制台为此 Key 开通 JavaScript API 服务后重试。');
+ }
+ }, MAP_LOAD_TIMEOUT_MS);
+
+ void loadBaiduMap({
+ ak: BAIDU_MAP_AK,
+ globalConfig: { coordType: 'bd09ll' },
+ timeout: 10000,
+ version: '4.0',
+ })
+ .then((namespace: typeof BMap) => {
+ if (disposed) return;
+ window.clearTimeout(loadTimeout);
+
+ const center = initialLocation ?? SHANGHAI_CENTER;
+ const point = new namespace.Point(center.longitude, center.latitude);
+ const map = new namespace.Map(host, {
+ center: point,
+ enablePinchZoom: true,
+ enableWheelZoom: true,
+ fixCenterWhenResize: true,
+ zoom: initialLocation ? 17 : 14,
+ });
+
+ bmapRef.current = namespace;
+ mapRef.current = map;
+ if (initialLocation) moveMarker(initialLocation);
+ map.addEventListener('click', (event) => {
+ selectCoordinates(event.point.lat, event.point.lng);
+ });
+ setMapError(null);
+ setMapReady(true);
+ if (!initialLocation) locateCurrentPosition();
+ })
+ .catch(() => {
+ if (!disposed) {
+ window.clearTimeout(loadTimeout);
+ setMapError('地图加载失败,请检查网络或百度地图密钥配置。');
+ }
+ });
+
+ return () => {
+ disposed = true;
+ window.clearTimeout(loadTimeout);
+ reverseGeocodeGate.clear();
+ requestRef.current += 1;
+ markerRef.current = null;
+ const map = mapRef.current;
+ if (map) {
+ try {
+ const destroy = (map as unknown as { destroy?: () => void }).destroy;
+ if (typeof destroy === 'function') {
+ destroy.call(map);
+ } else {
+ (map as unknown as { clearOverlays?: () => void }).clearOverlays?.();
+ }
+ } catch {
+ // Baidu may have already torn down the map while the overlay closes.
+ }
+ }
+ mapRef.current = null;
+ bmapRef.current = null;
+ };
+ }, [initialLocation, locateCurrentPosition, moveMarker, selectCoordinates]);
+
+ const searchLocations = useCallback((query: string) => {
+ return new Promise((resolve, reject) => {
+ const BMapApi = bmapRef.current;
+ const map = mapRef.current;
+ if (!BMapApi || !map) {
+ reject(new Error('Baidu map is not ready'));
+ return;
+ }
+
+ let completed = false;
+ const finish = (locations?: MapLocation[]) => {
+ if (completed) return;
+ completed = true;
+ window.clearTimeout(timeout);
+ if (locations) resolve(locations);
+ else reject(new Error('Baidu place search failed'));
+ };
+ const timeout = window.setTimeout(() => finish(), MAP_REQUEST_TIMEOUT_MS);
+ const localSearch = new BMapApi.LocalSearch(map, {
+ onSearchComplete: (rawResults) => {
+ const result = Array.isArray(rawResults) ? rawResults[0] : rawResults;
+ if (!result) {
+ finish([]);
+ return;
+ }
+
+ const locations: MapLocation[] = [];
+ const count = Math.min(result.getCurrentNumPois(), 5);
+ for (let index = 0; index < count; index += 1) {
+ const poi = result.getPoi(index);
+ if (!poi?.point) continue;
+ locations.push({
+ address: readablePoiAddress(poi.title, poi.address),
+ latitude: poi.point.lat,
+ longitude: poi.point.lng,
+ });
+ }
+ finish(locations);
+ },
+ pageCapacity: 5,
+ renderOptions: { autoViewport: false },
+ });
+ localSearch.search(query);
+ });
+ }, []);
+
+ const selectSearchResult = (location: MapLocation) => {
+ const BMapApi = bmapRef.current;
+ const map = mapRef.current;
+ if (!BMapApi || !map) return;
+
+ setLocationError(null);
+ reverseGeocodeGateRef.current.clear();
+ requestRef.current += 1;
+ setLocating(false);
+ setSelection(location);
+ moveMarker(location);
+ map.setCenter(new BMapApi.Point(location.longitude, location.latitude), { noAnimation: false });
+ map.setZoom(17, { noAnimation: false });
+ };
+
+ return (
+
+
+ selection && onConfirm(selection)}
+ onSearch={searchLocations}
+ onSelectSearchResult={selectSearchResult}
+ selection={selection}
+ />
+
+ );
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/Overlay.tsx b/frontend/src/features/schedule/location/MapPicker/Overlay.tsx
new file mode 100644
index 0000000..92f805b
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/Overlay.tsx
@@ -0,0 +1,216 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { LocateFixed, MapPin, Search } from 'lucide-react-native';
+import { Pressable, Text, TextInput, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+import { BackButton } from '@/shared/components/BackButton';
+import { mapPickerStyles as styles } from './styles';
+import type { MapLocation } from './types';
+
+type MapPickerOverlayProps = {
+ mapError: string | null;
+ mapReady: boolean;
+ locating: boolean;
+ locationError: string | null;
+ onCancel: () => void;
+ onLocate: () => void;
+ onConfirm: () => void;
+ onSearch: (query: string) => Promise;
+ onSelectSearchResult: (location: MapLocation) => void;
+ selection: MapLocation | null;
+};
+
+export function MapPickerOverlay({
+ mapError,
+ mapReady,
+ locating,
+ locationError,
+ onCancel,
+ onLocate,
+ onConfirm,
+ onSearch,
+ onSelectSearchResult,
+ selection,
+}: MapPickerOverlayProps) {
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+ const [searching, setSearching] = useState(false);
+ const [searched, setSearched] = useState(false);
+ const [searchFailed, setSearchFailed] = useState(false);
+ const searchRequestRef = useRef(0);
+
+ const search = useCallback(
+ async (value: string) => {
+ const nextQuery = value.trim();
+ if (!nextQuery || !mapReady) return;
+
+ const requestId = searchRequestRef.current + 1;
+ searchRequestRef.current = requestId;
+ setSearching(true);
+ setSearched(false);
+ setSearchFailed(false);
+ try {
+ const nextResults = await onSearch(nextQuery);
+ if (requestId !== searchRequestRef.current) return;
+ setResults(nextResults);
+ setSearched(true);
+ } catch {
+ if (requestId !== searchRequestRef.current) return;
+ setResults([]);
+ setSearchFailed(true);
+ } finally {
+ if (requestId === searchRequestRef.current) setSearching(false);
+ }
+ },
+ [mapReady, onSearch],
+ );
+
+ useEffect(() => {
+ const nextQuery = query.trim();
+ if (!nextQuery || !mapReady) return;
+
+ const timer = setTimeout(() => {
+ void search(nextQuery);
+ }, 320);
+ return () => clearTimeout(timer);
+ }, [mapReady, query, search]);
+
+ const chooseResult = (location: MapLocation) => {
+ searchRequestRef.current += 1;
+ setSearching(false);
+ setQuery('');
+ setResults([]);
+ setSearched(false);
+ onSelectSearchResult(location);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {
+ setQuery(value);
+ searchRequestRef.current += 1;
+ setSearching(false);
+ setSearchFailed(false);
+ setSearched(false);
+ if (!value.trim()) {
+ setResults([]);
+ }
+ }}
+ onSubmitEditing={() => void search(query)}
+ placeholder="搜索地点或地址"
+ placeholderTextColor="#909892"
+ returnKeyType="search"
+ style={styles.searchInput}
+ value={query}
+ />
+ void search(query)}
+ style={styles.searchButton}
+ >
+
+
+
+
+
+
+
+
+ {(searching || searchFailed || searched) && (
+
+ {searching ? (
+ 正在搜索...
+ ) : searchFailed ? (
+ 搜索暂时不可用,请直接在地图上选点
+ ) : results.length === 0 ? (
+ 没有找到相关地点
+ ) : (
+ results.map((result, index) => (
+ chooseResult(result)}
+ style={[
+ styles.searchResult,
+ index === results.length - 1 && styles.searchResultLast,
+ ]}
+ >
+
+
+ {result.address}
+
+
+ ))
+ )}
+
+ )}
+ {locationError ? (
+
+ {locationError}
+
+ ) : null}
+
+
+ {mapError && (
+
+
+ 百度地图暂时不可用
+ {mapError}
+
+ )}
+
+
+
+
+
+
+
+ 选中的地点
+ {selection ? (
+
+ {locating ? '正在获取详细地址...' : selection.address}
+
+ ) : locating ? (
+ 正在获取当前位置...
+ ) : (
+ 点击地图,或搜索后选择一个地点
+ )}
+
+
+ {selection && (
+
+ {selection.latitude.toFixed(5)}, {selection.longitude.toFixed(5)} · 百度地图 · BD-09
+
+ )}
+
+ 确认这个地点
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/baidu/baiduMapWebView.ts b/frontend/src/features/schedule/location/MapPicker/baidu/baiduMapWebView.ts
new file mode 100644
index 0000000..0fe20a2
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/baidu/baiduMapWebView.ts
@@ -0,0 +1,245 @@
+import type { MapLocation } from './types';
+
+export type BaiduMapBridgeMessage =
+ | { type: 'map-ready' }
+ | { message: string; type: 'map-error' }
+ | { latitude: number; longitude: number; type: 'selecting' }
+ | { location: MapLocation; type: 'selected' }
+ | { message: string; type: 'location-error' }
+ | { results: MapLocation[]; type: 'search-results' }
+ | { type: 'search-error' };
+
+export function buildBaiduMapDocument(ak: string, initialLocation: MapLocation | null) {
+ const center = initialLocation ?? {
+ address: '上海市 · 默认地图中心',
+ latitude: 31.236305,
+ longitude: 121.480237,
+ };
+ const initialJson = JSON.stringify(initialLocation);
+ const centerJson = JSON.stringify(center);
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+`;
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/baidu/index.ts b/frontend/src/features/schedule/location/MapPicker/baidu/index.ts
new file mode 100644
index 0000000..091cfbb
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/baidu/index.ts
@@ -0,0 +1,8 @@
+export {
+ BAIDU_MAP_AK,
+ SHANGHAI_CENTER,
+ createCoordinateLocation,
+ readablePoiAddress,
+} from './services';
+export { createReverseGeocodeGate } from './reverseGeocodeGate';
+export { buildBaiduMapDocument, type BaiduMapBridgeMessage } from './baiduMapWebView';
diff --git a/frontend/src/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.ts b/frontend/src/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.ts
new file mode 100644
index 0000000..7c0ef2c
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/baidu/reverseGeocodeGate.ts
@@ -0,0 +1,82 @@
+/**
+ * 逆地理编码调度:防抖 + 串行,避免连点/拖图触发超免费 QPS。
+ * 同一时间只保留最新待解析坐标;上一请求结束后再发下一请求。
+ */
+export type ReverseGeocodeJob = {
+ latitude: number;
+ longitude: number;
+ requestId: number;
+};
+
+export type ReverseGeocodeRunner = (job: ReverseGeocodeJob) => Promise | void;
+
+export type ReverseGeocodeGate = {
+ schedule: (job: ReverseGeocodeJob, run: ReverseGeocodeRunner) => void;
+ clear: () => void;
+};
+
+export function createReverseGeocodeGate(options?: {
+ debounceMs?: number;
+ minIntervalMs?: number;
+}): ReverseGeocodeGate {
+ const debounceMs = options?.debounceMs ?? 450;
+ const minIntervalMs = options?.minIntervalMs ?? 350;
+
+ let debounceTimer: ReturnType | null = null;
+ let intervalTimer: ReturnType | null = null;
+ let pending: { job: ReverseGeocodeJob; run: ReverseGeocodeRunner } | null = null;
+ let inFlight = false;
+ let lastStartedAt = 0;
+
+ const clearTimers = () => {
+ if (debounceTimer != null) {
+ clearTimeout(debounceTimer);
+ debounceTimer = null;
+ }
+ if (intervalTimer != null) {
+ clearTimeout(intervalTimer);
+ intervalTimer = null;
+ }
+ };
+
+ const flush = async () => {
+ if (inFlight || !pending) return;
+
+ const elapsed = Date.now() - lastStartedAt;
+ const wait = lastStartedAt === 0 ? 0 : Math.max(0, minIntervalMs - elapsed);
+ if (wait > 0) {
+ intervalTimer = setTimeout(() => {
+ intervalTimer = null;
+ void flush();
+ }, wait);
+ return;
+ }
+
+ const current = pending;
+ pending = null;
+ inFlight = true;
+ lastStartedAt = Date.now();
+
+ try {
+ await current.run(current.job);
+ } finally {
+ inFlight = false;
+ if (pending) void flush();
+ }
+ };
+
+ return {
+ schedule(job, run) {
+ pending = { job, run };
+ if (debounceTimer != null) clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => {
+ debounceTimer = null;
+ void flush();
+ }, debounceMs);
+ },
+ clear() {
+ clearTimers();
+ pending = null;
+ },
+ };
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/baidu/services.ts b/frontend/src/features/schedule/location/MapPicker/baidu/services.ts
new file mode 100644
index 0000000..060eadb
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/baidu/services.ts
@@ -0,0 +1,26 @@
+import type { MapLocation } from '@/shared/types/geo';
+
+export const BAIDU_MAP_AK = process.env.EXPO_PUBLIC_BAIDU_MAP_AK?.trim() ?? '';
+export const BAIDU_COORDINATE_SYSTEM = 'bd09ll' as const;
+
+export const SHANGHAI_CENTER: MapLocation = {
+ address: '上海市 · 默认地图中心',
+ latitude: 31.236305,
+ longitude: 121.480237,
+};
+
+export function coordinateAddress(latitude: number, longitude: number) {
+ return `百度地图选点 · ${latitude.toFixed(5)}, ${longitude.toFixed(5)}`;
+}
+
+export function createCoordinateLocation(latitude: number, longitude: number): MapLocation {
+ return {
+ address: coordinateAddress(latitude, longitude),
+ latitude,
+ longitude,
+ };
+}
+
+export function readablePoiAddress(title: string, address?: string) {
+ return address?.trim() ? `${title} · ${address.trim()}` : title;
+}
diff --git a/frontend/src/features/schedule/location/MapPicker/baidu/types.ts b/frontend/src/features/schedule/location/MapPicker/baidu/types.ts
new file mode 100644
index 0000000..2b533c6
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/baidu/types.ts
@@ -0,0 +1,3 @@
+import type { MapLocation } from '@/shared/types/geo';
+
+export type { MapLocation };
diff --git a/frontend/src/features/schedule/location/MapPicker/index.ts b/frontend/src/features/schedule/location/MapPicker/index.ts
new file mode 100644
index 0000000..bfcef56
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/index.ts
@@ -0,0 +1,2 @@
+export { MapPicker } from './MapPicker';
+export type { MapLocation, MapPickerProps } from './types';
diff --git a/frontend/src/features/schedule/location/MapPicker/styles.ts b/frontend/src/features/schedule/location/MapPicker/styles.ts
new file mode 100644
index 0000000..63a020f
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/styles.ts
@@ -0,0 +1,150 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const mapPickerStyles = StyleSheet.create({
+ screen: { backgroundColor: '#D8E4DE', flex: 1 },
+ mapCanvas: { flex: 1 },
+ mapError: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(247, 248, 245, 0.96)',
+ borderColor: colors.line,
+ borderRadius: 16,
+ borderWidth: 1,
+ left: 44,
+ paddingHorizontal: 20,
+ paddingVertical: 18,
+ position: 'absolute',
+ right: 44,
+ top: '38%',
+ zIndex: 900,
+ },
+ mapErrorTitle: { color: colors.ink, fontSize: 14, fontWeight: '800', marginTop: 10 },
+ mapErrorText: {
+ color: colors.sub,
+ fontSize: 11,
+ lineHeight: 17,
+ marginTop: 5,
+ textAlign: 'center',
+ },
+ topArea: {
+ left: 14,
+ position: 'absolute',
+ right: 14,
+ top: 14,
+ zIndex: 1000,
+ },
+ toolbar: { alignItems: 'center', flexDirection: 'row', gap: 8 },
+ searchBox: {
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ elevation: 4,
+ flex: 1,
+ flexDirection: 'row',
+ height: 48,
+ paddingHorizontal: 12,
+ },
+ searchInput: {
+ borderWidth: 0,
+ color: colors.ink,
+ flex: 1,
+ fontSize: 13,
+ height: 46,
+ marginHorizontal: 8,
+ outlineColor: 'transparent',
+ outlineStyle: 'solid',
+ outlineWidth: 0,
+ paddingVertical: 0,
+ },
+ searchButton: {
+ alignItems: 'center',
+ height: 34,
+ justifyContent: 'center',
+ width: 30,
+ },
+ locateButton: {
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ elevation: 4,
+ height: 48,
+ justifyContent: 'center',
+ width: 42,
+ },
+ locateButtonActive: { backgroundColor: colors.limeSoft },
+ searchResults: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ marginLeft: 50,
+ marginTop: 7,
+ overflow: 'hidden',
+ },
+ searchResult: {
+ alignItems: 'center',
+ borderBottomColor: colors.line,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ minHeight: 48,
+ paddingHorizontal: 12,
+ paddingVertical: 9,
+ },
+ searchResultLast: { borderBottomWidth: 0 },
+ searchResultText: { color: colors.ink, flex: 1, fontSize: 12, lineHeight: 17, marginLeft: 9 },
+ searchMessage: { color: colors.sub, fontSize: 11, padding: 14, textAlign: 'center' },
+ locationError: {
+ alignSelf: 'center',
+ backgroundColor: 'rgba(247, 248, 245, 0.96)',
+ borderColor: colors.line,
+ borderRadius: 11,
+ borderWidth: 1,
+ marginTop: 7,
+ paddingHorizontal: 10,
+ paddingVertical: 7,
+ },
+ locationErrorText: { color: colors.sub, fontSize: 11 },
+ selectionCard: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 18,
+ borderWidth: 1,
+ bottom: 16,
+ elevation: 6,
+ left: 14,
+ padding: 14,
+ position: 'absolute',
+ right: 14,
+ zIndex: 1000,
+ },
+ selectionHeading: { alignItems: 'center', flexDirection: 'row' },
+ selectionIcon: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 12,
+ height: 38,
+ justifyContent: 'center',
+ marginRight: 10,
+ width: 38,
+ },
+ selectionCopy: { flex: 1 },
+ selectionKicker: { color: '#728456', fontSize: 10, fontWeight: '800' },
+ selectionAddress: { color: colors.ink, fontSize: 13, lineHeight: 19, marginTop: 4 },
+ selectionHint: { color: colors.sub, fontSize: 11, lineHeight: 17, marginTop: 4 },
+ selectionMeta: { color: colors.muted, fontSize: 10, marginTop: 8 },
+ confirmButton: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 12,
+ height: 48,
+ justifyContent: 'center',
+ marginTop: 12,
+ },
+ confirmButtonDisabled: { opacity: 0.38 },
+ confirmButtonText: { color: colors.surface, fontSize: 13, fontWeight: '800' },
+});
diff --git a/frontend/src/features/schedule/location/MapPicker/types.ts b/frontend/src/features/schedule/location/MapPicker/types.ts
new file mode 100644
index 0000000..c6e4128
--- /dev/null
+++ b/frontend/src/features/schedule/location/MapPicker/types.ts
@@ -0,0 +1,9 @@
+import type { MapLocation } from '../types';
+
+export type { MapLocation };
+
+export type MapPickerProps = {
+ initialLocation: MapLocation | null;
+ onCancel: () => void;
+ onConfirm: (location: MapLocation) => void;
+};
diff --git a/frontend/src/features/schedule/location/index.ts b/frontend/src/features/schedule/location/index.ts
new file mode 100644
index 0000000..421840d
--- /dev/null
+++ b/frontend/src/features/schedule/location/index.ts
@@ -0,0 +1,4 @@
+export { LocationPickerSheet } from './LocationPickerSheet';
+export type { SavedLocation } from './types';
+export { createSavedLocation, matchSavedLocation, upsertSavedLocation } from './utils';
+export { useSessionSavedLocations } from './useSessionSavedLocations';
diff --git a/frontend/src/features/schedule/location/types.ts b/frontend/src/features/schedule/location/types.ts
new file mode 100644
index 0000000..661083e
--- /dev/null
+++ b/frontend/src/features/schedule/location/types.ts
@@ -0,0 +1,7 @@
+import type { MapLocation } from '@/shared/types/geo';
+
+export type { MapLocation };
+
+export type SavedLocation = MapLocation & {
+ id: string;
+};
diff --git a/frontend/src/features/schedule/location/useSessionSavedLocations.ts b/frontend/src/features/schedule/location/useSessionSavedLocations.ts
new file mode 100644
index 0000000..0ca552b
--- /dev/null
+++ b/frontend/src/features/schedule/location/useSessionSavedLocations.ts
@@ -0,0 +1,21 @@
+import { useCallback, useMemo, useState } from 'react';
+
+import type { SavedLocation } from './types';
+import { upsertSavedLocation } from './utils';
+
+/**
+ * App-session scoped saved locations.
+ *
+ * Persistence is intentionally not implied: a host can replace this hook with
+ * a storage-backed provider once a cross-platform storage adapter is part of
+ * the composition root.
+ */
+export function useSessionSavedLocations() {
+ const [locations, setLocations] = useState([]);
+
+ const upsert = useCallback((location: SavedLocation) => {
+ setLocations((current) => upsertSavedLocation(current, location));
+ }, []);
+
+ return useMemo(() => ({ locations, upsert }), [locations, upsert]);
+}
diff --git a/frontend/src/features/schedule/location/utils.ts b/frontend/src/features/schedule/location/utils.ts
new file mode 100644
index 0000000..519c313
--- /dev/null
+++ b/frontend/src/features/schedule/location/utils.ts
@@ -0,0 +1,60 @@
+import type { MapLocation, SavedLocation } from './types';
+
+export function createSavedLocation(location: MapLocation, id?: string): SavedLocation {
+ return {
+ ...location,
+ id: id ?? `loc_${Date.now()}`,
+ };
+}
+
+export function upsertSavedLocation(
+ locations: SavedLocation[],
+ location: SavedLocation,
+): SavedLocation[] {
+ const index = locations.findIndex((item) => item.id === location.id);
+ if (index < 0) {
+ return [...locations, location];
+ }
+ const next = [...locations];
+ next[index] = location;
+ return next;
+}
+
+export function matchSavedLocation(
+ locations: SavedLocation[],
+ candidate: {
+ latitude?: number | null;
+ longitude?: number | null;
+ location_name?: string | null;
+ location_address?: string | null;
+ },
+): SavedLocation | null {
+ if (candidate.latitude != null && candidate.longitude != null) {
+ const byCoords = locations.find(
+ (item) => item.latitude === candidate.latitude && item.longitude === candidate.longitude,
+ );
+ if (byCoords) {
+ return byCoords;
+ }
+ }
+
+ const name = candidate.location_name?.trim();
+ const address = candidate.location_address?.trim();
+ if (!name && !address) {
+ return null;
+ }
+
+ return (
+ locations.find((item) => {
+ const itemName = item.name?.trim() ?? '';
+ const itemAddress = item.address.trim();
+ if (name && address) {
+ return itemName === name && itemAddress === address;
+ }
+ if (name) {
+ return itemName === name;
+ }
+ return itemAddress === address;
+ }) ?? null
+ );
+}
diff --git a/frontend/src/features/schedule/presentation/scheduleFormat.ts b/frontend/src/features/schedule/presentation/scheduleFormat.ts
new file mode 100644
index 0000000..f8ed665
--- /dev/null
+++ b/frontend/src/features/schedule/presentation/scheduleFormat.ts
@@ -0,0 +1,53 @@
+import type { Schedule } from '@/contracts';
+import { formatTimeValue } from '@/shared/utils/date';
+
+export function timeToMinutes(value: string) {
+ const [hours, minutes] = value.split(':').map(Number);
+ return hours * 60 + minutes;
+}
+
+export function scheduleDate(item: Schedule) {
+ if (!item.start_time) return null;
+ const value = new Date(item.start_time);
+ return Number.isNaN(value.getTime()) ? null : value;
+}
+
+export function scheduleTime(item: Schedule) {
+ const date = scheduleDate(item);
+ return date ? formatTimeValue(date) : '地点';
+}
+
+export function scheduleRange(item: Schedule) {
+ const start = scheduleDate(item);
+ if (!start) return item.location_name ?? item.location_address ?? '地点提醒';
+ const startLabel = scheduleTime(item);
+ if (!item.end_time) return startLabel;
+ const end = new Date(item.end_time);
+ if (Number.isNaN(end.getTime())) return startLabel;
+ return `${startLabel}–${formatTimeValue(end)}`;
+}
+
+export function scheduleDuration(item: Schedule) {
+ const start = scheduleDate(item);
+ const end = item.end_time ? new Date(item.end_time) : null;
+ if (!start || !end || Number.isNaN(end.getTime())) return '未设置时长';
+ const minutes = Math.round((end.getTime() - start.getTime()) / 60_000);
+ return minutes > 0 ? `${minutes} 分钟` : '未设置时长';
+}
+
+export function scheduleColor(item: Schedule) {
+ if (item.status === 'done') return '#A8C7B5';
+ if (item.schedule_type === 'location') return '#E79472';
+ return item.source_mode === 'voice' ? '#AEC46B' : '#7DA6B8';
+}
+
+export function scheduleSourceLabel(item: Schedule) {
+ return item.source_mode === 'voice' ? '语音创建' : '手动创建';
+}
+
+/** 只映射契约里的三种 status,不做「已过期」等过程态。 */
+export function scheduleStatusLabel(item: Schedule) {
+ if (item.status === 'done') return '已完成';
+ if (item.status === 'deleted') return '已删除';
+ return '待完成';
+}
diff --git a/frontend/src/features/schedule/screens/ScheduleScreen.tsx b/frontend/src/features/schedule/screens/ScheduleScreen.tsx
new file mode 100644
index 0000000..37fb44a
--- /dev/null
+++ b/frontend/src/features/schedule/screens/ScheduleScreen.tsx
@@ -0,0 +1,122 @@
+import { useMemo, useState } from 'react';
+import { ChevronDown, Plus } from 'lucide-react-native';
+import { Pressable, Text, View } from 'react-native';
+
+import { DatePickerSheet } from '@/shared/components/DatePickerSheet';
+import type { Schedule } from '@/contracts';
+import { useCurrentDate } from '@/shared/hooks/useCurrentDate';
+import { colors } from '@/shared/theme';
+import { startOfMonth } from '@/shared/utils/date';
+
+import { MonthView } from '../calendar/MonthView';
+import { buildScheduleIndex } from '../calendar/scheduleIndex';
+import { ScheduleDetailSheet } from '../detail/ScheduleDetailSheet';
+import { scheduleScreenStyles as styles } from './scheduleScreen.styles';
+
+export function ScheduleScreen({
+ canMutate = true,
+ onCreate,
+ onDeleteSchedule,
+ onEditSchedule,
+ onToggleSchedule,
+ scheduleItems,
+}: {
+ canMutate?: boolean;
+ onCreate: () => void;
+ onDeleteSchedule: (item: Schedule) => void;
+ onEditSchedule: (item: Schedule) => void;
+ onToggleSchedule?: (item: Schedule) => void;
+ scheduleItems: Schedule[];
+}) {
+ const now = useCurrentDate();
+ const [visibleMonth, setVisibleMonth] = useState(() => startOfMonth(new Date()));
+ const [selectedDate, setSelectedDate] = useState(() => new Date());
+ const [selectedScheduleId, setSelectedScheduleId] = useState(null);
+ const [datePickerOpen, setDatePickerOpen] = useState(false);
+
+ // Derive the selected entity from the cache. When a push removes it, the
+ // detail sheet naturally closes without mutating state during render.
+ const selectedSchedule = selectedScheduleId
+ ? scheduleItems.find((item) => item.id === selectedScheduleId)
+ : undefined;
+ const scheduleIndex = useMemo(() => buildScheduleIndex(scheduleItems), [scheduleItems]);
+
+ const selectDate = (date: Date) => {
+ setSelectedDate(date);
+ setVisibleMonth(startOfMonth(date));
+ };
+
+ return (
+
+
+ setDatePickerOpen(true)}
+ style={styles.headerButton}
+ >
+
+ {visibleMonth.getFullYear()}年{visibleMonth.getMonth() + 1}月
+
+
+ {visibleMonth.getMonth() + 1}月
+
+
+
+
+
+
+
+
+
+
+ setDatePickerOpen(false)}
+ onSelect={selectDate}
+ selectedDate={selectedDate}
+ visible={datePickerOpen}
+ />
+ onDeleteSchedule(selectedSchedule)
+ : undefined
+ }
+ onEdit={
+ canMutate && selectedSchedule && selectedSchedule.status !== 'deleted'
+ ? () => onEditSchedule(selectedSchedule)
+ : undefined
+ }
+ onToggle={
+ canMutate && selectedSchedule && selectedSchedule.status !== 'deleted' && onToggleSchedule
+ ? () => onToggleSchedule(selectedSchedule)
+ : undefined
+ }
+ onClose={() => setSelectedScheduleId(null)}
+ onOpenDay={(date) => {
+ selectDate(date);
+ }}
+ />
+
+ );
+}
diff --git a/frontend/src/features/schedule/screens/scheduleScreen.styles.ts b/frontend/src/features/schedule/screens/scheduleScreen.styles.ts
new file mode 100644
index 0000000..696e772
--- /dev/null
+++ b/frontend/src/features/schedule/screens/scheduleScreen.styles.ts
@@ -0,0 +1,41 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const scheduleScreenStyles = StyleSheet.create({
+ screen: { flex: 1, paddingBottom: 76, paddingHorizontal: 20, paddingTop: 20 },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 14,
+ minHeight: 50,
+ },
+ headerButton: { flex: 1, marginRight: 12 },
+ headerEyebrow: {
+ color: colors.muted,
+ fontSize: 10,
+ fontWeight: '700',
+ letterSpacing: 0,
+ marginBottom: 4,
+ },
+ headerTitleRow: { alignItems: 'center', flexDirection: 'row', gap: 4 },
+ headerTitle: {
+ color: colors.ink,
+ fontSize: 28,
+ fontWeight: '800',
+ letterSpacing: 0,
+ lineHeight: 29,
+ },
+ addButton: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderColor: '#D4E3B1',
+ borderRadius: 13,
+ borderWidth: 1,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ addButtonDisabled: { opacity: 0.45 },
+});
diff --git a/frontend/src/infrastructure/audio/VoiceRecorder.ts b/frontend/src/infrastructure/audio/VoiceRecorder.ts
new file mode 100644
index 0000000..51b1397
--- /dev/null
+++ b/frontend/src/infrastructure/audio/VoiceRecorder.ts
@@ -0,0 +1,310 @@
+import {
+ NativeEventEmitter,
+ NativeModules,
+ PermissionsAndroid,
+ Platform,
+ type EmitterSubscription,
+} from 'react-native';
+
+export type AudioChunkHandler = (chunk: ArrayBuffer) => void;
+
+export type VoiceRecorder = {
+ start(onChunk: AudioChunkHandler): Promise;
+ stop(): Promise;
+ cancel(): Promise;
+};
+
+export class VoiceRecordingUnavailableError extends Error {
+ constructor(message = '当前构建未提供录音适配器') {
+ super(message);
+ this.name = 'VoiceRecordingUnavailableError';
+ }
+}
+
+type BrowserMediaStreamTrack = { stop: () => void };
+type BrowserMediaStream = { getTracks: () => BrowserMediaStreamTrack[] };
+type BrowserAudioBuffer = {
+ length: number;
+ numberOfChannels: number;
+ sampleRate: number;
+ getChannelData: (channel: number) => Float32Array;
+};
+type BrowserAudioProcessEvent = { inputBuffer: BrowserAudioBuffer };
+type BrowserAudioNode = {
+ connect: (node: BrowserAudioNode) => void;
+ disconnect: () => void;
+};
+type BrowserScriptProcessor = BrowserAudioNode & {
+ onaudioprocess: ((event: BrowserAudioProcessEvent) => void) | null;
+};
+type BrowserGainNode = BrowserAudioNode & { gain: { value: number } };
+type BrowserAudioContext = {
+ sampleRate: number;
+ destination: BrowserAudioNode;
+ state?: string;
+ createMediaStreamSource: (stream: BrowserMediaStream) => BrowserAudioNode;
+ createScriptProcessor: (
+ bufferSize: number,
+ inputChannels: number,
+ outputChannels: number,
+ ) => BrowserScriptProcessor;
+ createGain: () => BrowserGainNode;
+ resume?: () => Promise;
+ close?: () => Promise;
+};
+type BrowserAudioContextConstructor = new () => BrowserAudioContext;
+type BrowserNavigator = {
+ mediaDevices?: {
+ getUserMedia: (constraints: { audio: boolean }) => Promise;
+ };
+};
+
+function pcm16Chunk(input: BrowserAudioBuffer, targetRate: number): ArrayBuffer | null {
+ if (input.length === 0) return null;
+ const channels = Array.from({ length: Math.max(1, input.numberOfChannels) }, (_, index) =>
+ input.getChannelData(index),
+ );
+ const ratio = input.sampleRate / targetRate;
+ const outputLength = Math.max(1, Math.floor(input.length / ratio));
+ const output = new ArrayBuffer(outputLength * 2);
+ const view = new DataView(output);
+
+ for (let index = 0; index < outputLength; index += 1) {
+ const sourceIndex = Math.min(input.length - 1, Math.floor(index * ratio));
+ let sample = 0;
+ for (const channel of channels) sample += channel[sourceIndex] ?? 0;
+ sample /= channels.length;
+ const clamped = Math.max(-1, Math.min(1, sample));
+ view.setInt16(index * 2, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);
+ }
+ return output;
+}
+
+/**
+ * Browser recorder that emits mono 16 kHz PCM16 frames, matching the voice
+ * stream contract. It intentionally does not use MediaRecorder because its
+ * WebM/Opus output cannot be labelled as PCM without corrupting the protocol.
+ */
+export class BrowserPcmVoiceRecorder implements VoiceRecorder {
+ private stream: BrowserMediaStream | null = null;
+ private context: BrowserAudioContext | null = null;
+ private source: BrowserAudioNode | null = null;
+ private processor: BrowserScriptProcessor | null = null;
+ private gain: BrowserGainNode | null = null;
+
+ async start(onChunk: AudioChunkHandler): Promise {
+ if (this.context) {
+ throw new Error('录音已经开始');
+ }
+ const browserNavigator = (globalThis as { navigator?: BrowserNavigator }).navigator;
+ const mediaDevices = browserNavigator?.mediaDevices;
+ const audioGlobals = globalThis as unknown as {
+ AudioContext?: BrowserAudioContextConstructor;
+ webkitAudioContext?: BrowserAudioContextConstructor;
+ };
+ const contextConstructor = audioGlobals.AudioContext;
+ const WebkitAudioContext = audioGlobals.webkitAudioContext;
+ if (!mediaDevices?.getUserMedia || (!contextConstructor && !WebkitAudioContext)) {
+ throw new VoiceRecordingUnavailableError('当前浏览器不支持 PCM 麦克风采集');
+ }
+
+ let stream: BrowserMediaStream;
+ try {
+ stream = await mediaDevices.getUserMedia({ audio: true });
+ } catch (error) {
+ throw new VoiceRecordingUnavailableError(
+ `麦克风权限未授予: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+
+ const context = new (contextConstructor ?? WebkitAudioContext!)();
+ try {
+ await context.resume?.();
+ const source = context.createMediaStreamSource(stream);
+ const processor = context.createScriptProcessor(4096, 1, 1);
+ const gain = context.createGain();
+ // Keep the processor in the audio graph without feeding microphone audio
+ // back to the speakers.
+ gain.gain.value = 0;
+ processor.onaudioprocess = (event) => {
+ const chunk = pcm16Chunk(event.inputBuffer, 16_000);
+ if (chunk) onChunk(chunk);
+ };
+ source.connect(processor);
+ processor.connect(gain);
+ gain.connect(context.destination);
+ this.stream = stream;
+ this.context = context;
+ this.source = source;
+ this.processor = processor;
+ this.gain = gain;
+ } catch (error) {
+ stream.getTracks().forEach((track) => track.stop());
+ await context.close?.();
+ throw new VoiceRecordingUnavailableError(
+ `初始化 PCM 录音失败: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ }
+
+ async stop(): Promise {
+ await this.release();
+ }
+
+ async cancel(): Promise {
+ await this.release();
+ }
+
+ private async release(): Promise {
+ this.processor?.disconnect();
+ this.source?.disconnect();
+ this.gain?.disconnect();
+ this.stream?.getTracks().forEach((track) => track.stop());
+ await this.context?.close?.();
+ this.stream = null;
+ this.context = null;
+ this.source = null;
+ this.processor = null;
+ this.gain = null;
+ }
+}
+
+type TimeflowVoiceRecorderNative = {
+ start: () => Promise;
+ stop: () => Promise;
+ cancel: () => Promise;
+ addListener: (eventName: string) => void;
+ removeListeners: (count: number) => void;
+};
+
+type NativeRecorderErrorEvent = { message?: string };
+
+const AUDIO_CHUNK_EVENT = 'TimeflowVoiceRecorderChunk';
+const ERROR_EVENT = 'TimeflowVoiceRecorderError';
+
+function base64PcmToArrayBuffer(data: string): ArrayBuffer {
+ const atob = (globalThis as { atob?: (value: string) => string }).atob;
+ if (!atob) throw new VoiceRecordingUnavailableError('当前 JavaScript 引擎不支持音频解码');
+ const decoded = atob(data);
+ const bytes = new Uint8Array(decoded.length);
+ for (let index = 0; index < decoded.length; index += 1) {
+ bytes[index] = decoded.charCodeAt(index);
+ }
+ return bytes.buffer;
+}
+
+export class AndroidPcmVoiceRecorder implements VoiceRecorder {
+ private readonly nativeRecorder: TimeflowVoiceRecorderNative | undefined;
+ private readonly eventEmitter: NativeEventEmitter | null;
+ private subscriptions: EmitterSubscription[] = [];
+ private recording = false;
+ private runtimeError: Error | null = null;
+
+ constructor(
+ nativeRecorder:
+ TimeflowVoiceRecorderNative | null | undefined = NativeModules.TimeflowVoiceRecorder,
+ ) {
+ this.nativeRecorder = nativeRecorder ?? undefined;
+ this.eventEmitter = nativeRecorder ? new NativeEventEmitter(nativeRecorder) : null;
+ }
+
+ async start(onChunk: AudioChunkHandler): Promise {
+ if (!this.nativeRecorder || !this.eventEmitter) {
+ throw new VoiceRecordingUnavailableError('原生录音模块未链接,请重新安装最新 APK');
+ }
+ if (this.recording) throw new Error('录音已经开始');
+
+ const permission = PermissionsAndroid.PERMISSIONS.RECORD_AUDIO;
+ const granted =
+ (await PermissionsAndroid.check(permission)) ||
+ (await PermissionsAndroid.request(permission, {
+ title: '麦克风权限',
+ message: 'Timeflow 需要使用麦克风,将语音整理成日程。',
+ buttonPositive: '允许',
+ buttonNegative: '取消',
+ })) === PermissionsAndroid.RESULTS.GRANTED;
+ if (!granted) {
+ throw new VoiceRecordingUnavailableError('麦克风权限未授予');
+ }
+
+ this.runtimeError = null;
+ this.subscriptions = [
+ this.eventEmitter.addListener(AUDIO_CHUNK_EVENT, (data: string) => {
+ if (!this.recording) return;
+ try {
+ onChunk(base64PcmToArrayBuffer(data));
+ } catch (error) {
+ this.runtimeError = error instanceof Error ? error : new Error(String(error));
+ }
+ }),
+ this.eventEmitter.addListener(ERROR_EVENT, (event: NativeRecorderErrorEvent) => {
+ this.runtimeError = new Error(event.message ?? '原生录音失败');
+ }),
+ ];
+ this.recording = true;
+
+ try {
+ await this.nativeRecorder.start();
+ } catch (error) {
+ this.releaseListeners();
+ this.recording = false;
+ throw new VoiceRecordingUnavailableError(
+ `启动原生录音失败: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ }
+
+ async stop(): Promise {
+ if (!this.recording) return;
+ try {
+ await this.nativeRecorder?.stop();
+ } finally {
+ this.recording = false;
+ this.releaseListeners();
+ }
+ if (this.runtimeError) {
+ const error = this.runtimeError;
+ this.runtimeError = null;
+ throw error;
+ }
+ }
+
+ async cancel(): Promise {
+ if (!this.recording) return;
+ try {
+ await this.nativeRecorder?.cancel();
+ } finally {
+ this.recording = false;
+ this.runtimeError = null;
+ this.releaseListeners();
+ }
+ }
+
+ private releaseListeners(): void {
+ this.subscriptions.forEach((subscription) => subscription.remove());
+ this.subscriptions = [];
+ }
+}
+
+/** Fallback for native platforms that do not have a PCM recorder implementation. */
+export class UnavailableVoiceRecorder implements VoiceRecorder {
+ constructor(private readonly reason = '原生录音模块未链接') {}
+
+ async start(_onChunk: AudioChunkHandler): Promise {
+ throw new VoiceRecordingUnavailableError(this.reason);
+ }
+
+ async stop(): Promise {
+ // No stream was started.
+ }
+
+ async cancel(): Promise {
+ // No stream was started.
+ }
+}
+
+export function createVoiceRecorder(): VoiceRecorder {
+ if (Platform.OS === 'web') return new BrowserPcmVoiceRecorder();
+ if (Platform.OS === 'android') return new AndroidPcmVoiceRecorder();
+ return new UnavailableVoiceRecorder('当前平台尚未提供 PCM/流式录音适配器');
+}
diff --git a/frontend/src/infrastructure/location/LocationReporter.ts b/frontend/src/infrastructure/location/LocationReporter.ts
new file mode 100644
index 0000000..61cd804
--- /dev/null
+++ b/frontend/src/infrastructure/location/LocationReporter.ts
@@ -0,0 +1,252 @@
+import { requireOptionalNativeModule } from 'expo';
+import { Platform } from 'react-native';
+
+import type { LocationReport, LocationReportAck, Schedule, WsJsonMessage } from '@/contracts';
+import { nextRequestId } from '@/shared/utils/requestId';
+
+export type LocationSample = {
+ latitude: number;
+ longitude: number;
+ accuracy: number;
+ timestamp?: string;
+};
+
+export type LocationProvider = {
+ getCurrentSample(): Promise;
+};
+
+export type LocationTransport = {
+ request(
+ message: WsJsonMessage & { request_id: string },
+ isMatch?: (response: WsJsonMessage) => boolean,
+ ): Promise;
+};
+
+export class LocationUnavailableError extends Error {
+ constructor(message = '当前位置服务不可用') {
+ super(message);
+ this.name = 'LocationUnavailableError';
+ }
+}
+
+type ExpoLocationPosition = {
+ coords?: {
+ latitude?: number;
+ longitude?: number;
+ accuracy?: number | null;
+ };
+ timestamp?: number;
+};
+
+type ExpoLocationPermission = {
+ status?: string;
+ granted?: boolean;
+};
+
+type ExpoLocationModule = {
+ getForegroundPermissionsAsync?: () => Promise;
+ requestForegroundPermissionsAsync?: () => Promise;
+ getCurrentPositionAsync?: (options?: Record) => Promise;
+};
+
+// Expo Location's public enum maps Balanced accuracy to 3. Keep the numeric
+// value at this adapter boundary so the feature does not depend on the SDK.
+const EXPO_BALANCED_ACCURACY = 3;
+
+function assertSample(sample: LocationSample): LocationSample {
+ if (
+ !Number.isFinite(sample.latitude) ||
+ sample.latitude < -90 ||
+ sample.latitude > 90 ||
+ !Number.isFinite(sample.longitude) ||
+ sample.longitude < -180 ||
+ sample.longitude > 180 ||
+ !Number.isFinite(sample.accuracy) ||
+ sample.accuracy < 0
+ ) {
+ throw new LocationUnavailableError('定位 SDK 返回了无效坐标');
+ }
+ return sample;
+}
+
+function isGranted(permission: ExpoLocationPermission | null | undefined): boolean {
+ return permission?.granted === true || permission?.status === 'granted';
+}
+
+/** Native Expo location provider. The module must be linked by the host build. */
+export class ExpoLocationProvider implements LocationProvider {
+ constructor(
+ private readonly module: ExpoLocationModule | null = requireOptionalNativeModule(
+ 'ExpoLocation',
+ ),
+ ) {}
+
+ async getCurrentSample(): Promise {
+ const location = this.module;
+ if (!location?.getCurrentPositionAsync) {
+ throw new LocationUnavailableError(
+ 'ExpoLocation 原生模块未链接;请在宿主中注入 LocationProvider',
+ );
+ }
+
+ const currentPermission = await location.getForegroundPermissionsAsync?.();
+ const permission = isGranted(currentPermission)
+ ? currentPermission
+ : await location.requestForegroundPermissionsAsync?.();
+ if (!isGranted(permission)) {
+ throw new LocationUnavailableError('未授予前台定位权限');
+ }
+
+ const position = await location.getCurrentPositionAsync({
+ accuracy: EXPO_BALANCED_ACCURACY,
+ });
+ const latitude = position.coords?.latitude;
+ const longitude = position.coords?.longitude;
+ if (latitude == null || longitude == null) {
+ throw new LocationUnavailableError('定位 SDK 未返回坐标');
+ }
+ return assertSample({
+ latitude,
+ longitude,
+ accuracy: Math.max(0, position.coords?.accuracy ?? 0),
+ timestamp:
+ position.timestamp != null
+ ? new Date(position.timestamp).toISOString()
+ : new Date().toISOString(),
+ });
+ }
+}
+
+/** Browser provider used by RN Web and desktop development. */
+export class BrowserLocationProvider implements LocationProvider {
+ async getCurrentSample(): Promise {
+ const geolocation = globalThis.navigator?.geolocation;
+ if (!geolocation) {
+ throw new LocationUnavailableError('当前浏览器不支持定位');
+ }
+ return new Promise((resolve, reject) => {
+ geolocation.getCurrentPosition(
+ (position) => {
+ try {
+ resolve(
+ assertSample({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ accuracy: Math.max(0, position.coords.accuracy ?? 0),
+ timestamp: new Date(position.timestamp).toISOString(),
+ }),
+ );
+ } catch (error) {
+ reject(error);
+ }
+ },
+ (error) => reject(new LocationUnavailableError(`定位失败: ${error.message}`)),
+ { enableHighAccuracy: true, maximumAge: 15_000, timeout: 10_000 },
+ );
+ });
+ }
+}
+
+/**
+ * Resolve the platform provider at the composition boundary. No fixed
+ * coordinates are ever returned: an unavailable native module is surfaced as
+ * an error and the reporter skips that tick.
+ */
+export function createLocationProvider(): LocationProvider {
+ return Platform.OS === 'web' ? new BrowserLocationProvider() : new ExpoLocationProvider();
+}
+
+type LocationSampleSource = LocationProvider | (() => Promise);
+
+function readSample(source: LocationSampleSource): Promise {
+ return typeof source === 'function' ? source() : source.getCurrentSample();
+}
+
+/**
+ * 地点提醒位置上报器:客户端只上报位置,触发判定留给服务端。
+ * 当前 timer 是前台轮询;后台 task/围栏应由宿主注入更合适的 provider。
+ */
+export class LocationReporter {
+ private timer: ReturnType | null = null;
+ private armed = false;
+ private tickInFlight = false;
+ private lastError: Error | null = null;
+
+ constructor(
+ private readonly client: LocationTransport,
+ private readonly source: LocationSampleSource,
+ private readonly onError?: (error: Error) => void,
+ ) {}
+
+ getLastError(): Error | null {
+ return this.lastError;
+ }
+
+ syncArmedSchedules(schedules: Schedule[]): void {
+ this.armed = schedules.some(
+ (item) =>
+ item.status === 'scheduled' && item.schedule_type === 'location' && item.geofence_armed,
+ );
+ if (this.armed) {
+ this.start(30_000);
+ } else {
+ this.stop();
+ }
+ }
+
+ start(intervalMs = 30_000): void {
+ if (this.timer) return;
+ this.armed = true;
+ void this.tick();
+ this.timer = setInterval(() => {
+ void this.tick();
+ }, intervalMs);
+ }
+
+ stop(): void {
+ this.armed = false;
+ if (this.timer) {
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+ }
+
+ async report(sample: LocationSample): Promise {
+ const validated = assertSample(sample);
+ const message: LocationReport = {
+ type: 'location.report',
+ request_id: nextRequestId('req_location'),
+ payload: {
+ schedule_scope: 'current',
+ latitude: validated.latitude,
+ longitude: validated.longitude,
+ accuracy: validated.accuracy,
+ timestamp: validated.timestamp ?? new Date().toISOString(),
+ },
+ };
+ const isMatch = (incoming: WsJsonMessage) =>
+ incoming.type === 'location.report.ack' && incoming.request_id === message.request_id;
+
+ const ack = await this.client.request(message, isMatch);
+ if (!ack.ok) {
+ throw new Error(ack.error.message);
+ }
+ return ack;
+ }
+
+ private async tick(): Promise {
+ if (!this.armed || this.tickInFlight) return;
+ this.tickInFlight = true;
+ try {
+ const sample = await readSample(this.source);
+ if (!sample || !this.armed) return;
+ await this.report(sample);
+ this.lastError = null;
+ } catch (error) {
+ this.lastError = error instanceof Error ? error : new Error(String(error));
+ this.onError?.(this.lastError);
+ } finally {
+ this.tickInFlight = false;
+ }
+ }
+}
diff --git a/frontend/src/infrastructure/storage/deviceIdStore.ts b/frontend/src/infrastructure/storage/deviceIdStore.ts
new file mode 100644
index 0000000..21d998e
--- /dev/null
+++ b/frontend/src/infrastructure/storage/deviceIdStore.ts
@@ -0,0 +1,150 @@
+import { Platform } from 'react-native';
+import { requireOptionalNativeModule } from 'expo';
+
+const DEVICE_ID_KEY = 'timeflow.device_id';
+
+export type DeviceIdStore = {
+ get(): Promise;
+ set(value: string): Promise;
+};
+
+type ExpoFileSystemLike = {
+ documentDirectory: string | null;
+ getInfoAsync: (path: string, options: Record) => Promise<{ exists: boolean }>;
+ readAsStringAsync: (path: string, options: Record) => Promise;
+ writeAsStringAsync: (
+ path: string,
+ contents: string,
+ options: Record,
+ ) => Promise;
+};
+
+/**
+ * Native storage is deliberately a hard dependency at runtime. Falling back
+ * to an in-process map makes the device identity change on every cold start,
+ * which is worse than refusing to establish a session.
+ */
+export class DeviceIdPersistenceUnavailableError extends Error {
+ constructor(message = '原生设备存储不可用,无法持久化 device_id') {
+ super(message);
+ this.name = 'DeviceIdPersistenceUnavailableError';
+ }
+}
+
+function webStore(): DeviceIdStore {
+ return {
+ async get() {
+ if (typeof localStorage === 'undefined') {
+ throw new DeviceIdPersistenceUnavailableError('浏览器 localStorage 不可用');
+ }
+ return localStorage.getItem(DEVICE_ID_KEY);
+ },
+ async set(value) {
+ if (typeof localStorage === 'undefined') {
+ throw new DeviceIdPersistenceUnavailableError('浏览器 localStorage 不可用');
+ }
+ localStorage.setItem(DEVICE_ID_KEY, value);
+ },
+ };
+}
+
+/** Test/host adapter. Production code must inject a persistent implementation. */
+export function memoryStore(seed: Map = new Map()): DeviceIdStore {
+ return {
+ async get() {
+ return seed.get(DEVICE_ID_KEY) ?? null;
+ },
+ async set(value) {
+ seed.set(DEVICE_ID_KEY, value);
+ },
+ };
+}
+
+let nativeFileStore: DeviceIdStore | null = null;
+let nativeFileStorePromise: Promise | null = null;
+
+function loadExpoFileSystem(): ExpoFileSystemLike | null {
+ // Expo Go and a custom Expo runtime expose the legacy module under this
+ // name. The lookup is static and Metro-visible; there is no hidden import
+ // or optional JS package that can silently disappear from a release bundle.
+ return requireOptionalNativeModule('ExponentFileSystem');
+}
+
+function createNativeFileStore(FileSystem = loadExpoFileSystem()): DeviceIdStore {
+ const base = FileSystem?.documentDirectory;
+ if (
+ !FileSystem ||
+ !base ||
+ typeof FileSystem.getInfoAsync !== 'function' ||
+ typeof FileSystem.readAsStringAsync !== 'function' ||
+ typeof FileSystem.writeAsStringAsync !== 'function'
+ ) {
+ throw new DeviceIdPersistenceUnavailableError(
+ 'ExpoFileSystem 原生模块未链接;请在构建中声明 expo-file-system 或注入 DeviceIdStore',
+ );
+ }
+ const path = `${base}.timeflow-device-id`;
+ return {
+ async get() {
+ let info: { exists: boolean };
+ try {
+ info = await FileSystem.getInfoAsync(path, {});
+ } catch (error) {
+ throw new DeviceIdPersistenceUnavailableError(
+ `读取 device_id 失败: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ if (!info.exists) return null;
+ try {
+ const value = await FileSystem.readAsStringAsync(path, {});
+ return value.trim() || null;
+ } catch (error) {
+ throw new DeviceIdPersistenceUnavailableError(
+ `读取 device_id 失败: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ },
+ async set(value) {
+ try {
+ await FileSystem.writeAsStringAsync(path, value, {});
+ } catch (error) {
+ throw new DeviceIdPersistenceUnavailableError(
+ `写入 device_id 失败: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ },
+ };
+}
+
+export async function createDeviceIdStore(): Promise {
+ if (Platform.OS === 'web') {
+ return webStore();
+ }
+ if (!nativeFileStore) {
+ nativeFileStorePromise ??= Promise.resolve()
+ .then(() => createNativeFileStore())
+ .catch((error) => {
+ // A transient host/module setup failure should not poison all later
+ // attempts during the same app lifetime.
+ nativeFileStorePromise = null;
+ throw error;
+ });
+ nativeFileStore = await nativeFileStorePromise;
+ }
+ return nativeFileStore;
+}
+
+export async function getOrCreateDeviceId(store?: DeviceIdStore): Promise {
+ const backend = store ?? (await createDeviceIdStore());
+ const existing = await backend.get();
+ if (existing) return existing;
+ const next = `device_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+ await backend.set(next);
+ // Read back once so a best-effort native implementation cannot report a
+ // successful write while losing the value (for example, a denied file URI).
+ const persisted = await backend.get();
+ if (persisted !== next) {
+ throw new DeviceIdPersistenceUnavailableError('device_id 写入后校验失败');
+ }
+ return next;
+}
diff --git a/frontend/src/infrastructure/ws/WsClient.ts b/frontend/src/infrastructure/ws/WsClient.ts
new file mode 100644
index 0000000..45c9b88
--- /dev/null
+++ b/frontend/src/infrastructure/ws/WsClient.ts
@@ -0,0 +1,250 @@
+import type { ConnectionStatus, WsJsonMessage } from '@/contracts';
+
+export type { ConnectionStatus, WsJsonMessage } from '@/contracts';
+
+export type WsClientOptions = {
+ /** 真实后端地址;为空则走 fakeHandler 进程内通道。 */
+ url?: string | null;
+ /** 无 URL 时的进程内消息处理器(由 SessionProvider 在显式 Fake 模式下注入)。 */
+ fakeHandler?: (message: WsJsonMessage | ArrayBuffer) => void | Promise;
+ requestTimeoutMs?: number;
+};
+
+type PendingRequest = {
+ resolve: (value: WsJsonMessage) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+ isMatch: (response: WsJsonMessage) => boolean;
+};
+
+/**
+ * 契约对齐的 WS 客户端:按 request_id 等待响应,支持订阅推送与二进制帧。
+ * 无 URL 时走进程内 Fake 通道,便于本地与单测。
+ */
+export class WsClient {
+ private socket: WebSocket | null = null;
+ private readonly pending = new Map();
+ private readonly listeners = new Set<(message: WsJsonMessage | ArrayBuffer) => void>();
+ private readonly statusListeners = new Set<(status: ConnectionStatus) => void>();
+ private status: ConnectionStatus = 'idle';
+ private readonly requestTimeoutMs: number;
+ private readonly url: string | null;
+ private readonly fakeHandler?: (message: WsJsonMessage | ArrayBuffer) => void | Promise;
+ private fakeReply: ((message: WsJsonMessage | ArrayBuffer) => void) | null = null;
+ private intentionallyClosed = false;
+ /** Invalidates callbacks belonging to a socket that has been replaced. */
+ private socketGeneration = 0;
+ private connecting: { generation: number; reject: (error: Error) => void } | null = null;
+
+ constructor(options: WsClientOptions = {}) {
+ this.url = options.url?.trim() || null;
+ this.fakeHandler = options.fakeHandler;
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
+ }
+
+ getConnectionStatus(): ConnectionStatus {
+ return this.status;
+ }
+
+ onStatus(listener: (status: ConnectionStatus) => void): () => void {
+ this.statusListeners.add(listener);
+ listener(this.status);
+ return () => this.statusListeners.delete(listener);
+ }
+
+ onMessage(listener: (message: WsJsonMessage | ArrayBuffer) => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ async connect(): Promise {
+ this.intentionallyClosed = false;
+ if (!this.url) {
+ this.setStatus('connecting');
+ this.fakeReply = (message) => this.dispatch(message);
+ this.setStatus('ready');
+ return;
+ }
+
+ this.setStatus(this.status === 'ready' ? 'reconnecting' : 'connecting');
+ await new Promise((resolve, reject) => {
+ const generation = ++this.socketGeneration;
+ const socket = new WebSocket(this.url!);
+ this.socket = socket;
+ this.connecting = { generation, reject };
+ socket.binaryType = 'arraybuffer';
+ socket.onopen = () => {
+ if (this.socket !== socket || this.socketGeneration !== generation) return;
+ this.connecting = null;
+ this.setStatus('ready');
+ resolve();
+ };
+ socket.onerror = () => {
+ if (this.socket !== socket || this.socketGeneration !== generation) return;
+ const error = new Error('WebSocket connection failed');
+ this.setStatus('error');
+ this.rejectPending(error);
+ this.rejectConnecting(generation, error);
+ };
+ socket.onclose = () => {
+ const isCurrent = this.socket === socket && this.socketGeneration === generation;
+ if (!isCurrent) return;
+ this.socket = null;
+ if (this.intentionallyClosed) return;
+ const error = new Error('WebSocket closed unexpectedly');
+ this.rejectPending(error);
+ this.setStatus('closed');
+ this.rejectConnecting(generation, new Error('WebSocket closed before becoming ready'));
+ };
+ socket.onmessage = (event) => {
+ if (this.socket !== socket || this.socketGeneration !== generation) return;
+ if (typeof event.data === 'string') {
+ try {
+ this.dispatch(JSON.parse(event.data) as WsJsonMessage);
+ } catch {
+ // ignore malformed JSON
+ }
+ return;
+ }
+ if (event.data instanceof ArrayBuffer) {
+ this.dispatch(event.data);
+ }
+ };
+ });
+ }
+
+ close(): void {
+ this.intentionallyClosed = true;
+ this.rejectConnecting(this.socketGeneration, new Error('WebSocket closed'));
+ this.socketGeneration += 1;
+ const socket = this.socket;
+ this.socket = null;
+ socket?.close();
+ this.fakeReply = null;
+ this.rejectPending(new Error('WebSocket closed'));
+ this.setStatus('closed');
+ }
+
+ sendJson(message: WsJsonMessage): void {
+ if (!this.url) {
+ void Promise.resolve(this.fakeHandler?.(message)).catch((error) => {
+ if (message.request_id) {
+ this.rejectRequest(
+ message.request_id,
+ error instanceof Error ? error : new Error(String(error)),
+ );
+ }
+ });
+ return;
+ }
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
+ throw new Error('WebSocket is not open');
+ }
+ this.socket.send(JSON.stringify(message));
+ }
+
+ sendBinary(data: ArrayBuffer): void {
+ if (!this.url) {
+ void Promise.resolve(this.fakeHandler?.(data)).catch(() => undefined);
+ return;
+ }
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
+ throw new Error('WebSocket is not open');
+ }
+ this.socket.send(data);
+ }
+
+ /** Fake 服务端向客户端推送。 */
+ emitFromServer(message: WsJsonMessage | ArrayBuffer): void {
+ this.fakeReply?.(message);
+ }
+
+ request(
+ message: WsJsonMessage & { request_id: string },
+ isMatch: (response: WsJsonMessage) => boolean = (response) =>
+ response.request_id === message.request_id,
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ if (!message.request_id) {
+ reject(new Error(`Request id is required: ${message.type}`));
+ return;
+ }
+ if (this.pending.has(message.request_id)) {
+ reject(new Error(`Duplicate request id: ${message.request_id}`));
+ return;
+ }
+ const timer = setTimeout(() => {
+ this.pending.delete(message.request_id);
+ reject(new Error(`Request timed out: ${message.type}`));
+ }, this.requestTimeoutMs);
+
+ this.pending.set(message.request_id, {
+ isMatch,
+ resolve: (value) => {
+ clearTimeout(timer);
+ this.pending.delete(message.request_id);
+ resolve(value as T);
+ },
+ reject: (error) => {
+ clearTimeout(timer);
+ this.pending.delete(message.request_id);
+ reject(error);
+ },
+ timer,
+ });
+
+ try {
+ this.sendJson(message);
+ } catch (error) {
+ clearTimeout(timer);
+ this.pending.delete(message.request_id);
+ reject(error instanceof Error ? error : new Error(String(error)));
+ }
+ });
+ }
+
+ private dispatch(message: WsJsonMessage | ArrayBuffer): void {
+ if (!(message instanceof ArrayBuffer)) {
+ for (const pending of this.pending.values()) {
+ if (pending.isMatch(message)) {
+ pending.resolve(message);
+ break;
+ }
+ }
+ }
+ for (const listener of this.listeners) {
+ listener(message);
+ }
+ }
+
+ private rejectPending(error: Error): void {
+ const pending = [...this.pending.values()];
+ this.pending.clear();
+ for (const request of pending) {
+ clearTimeout(request.timer);
+ request.reject(error);
+ }
+ }
+
+ private rejectRequest(requestId: string, error: Error): void {
+ const request = this.pending.get(requestId);
+ if (!request) return;
+ clearTimeout(request.timer);
+ this.pending.delete(requestId);
+ request.reject(error);
+ }
+
+ private rejectConnecting(generation: number, error: Error): void {
+ const connecting = this.connecting;
+ if (!connecting || connecting.generation !== generation) return;
+ this.connecting = null;
+ connecting.reject(error);
+ }
+
+ private setStatus(status: ConnectionStatus): void {
+ this.status = status;
+ for (const listener of this.statusListeners) {
+ listener(status);
+ }
+ }
+}
diff --git a/frontend/src/mocks/schedules.ts b/frontend/src/mocks/schedules.ts
deleted file mode 100644
index ff24159..0000000
--- a/frontend/src/mocks/schedules.ts
+++ /dev/null
@@ -1,349 +0,0 @@
-import type {
- Schedule,
- ScheduleConflict,
- ScheduleListQuery,
- ScheduleListResult,
- ScheduleUpsertCommand,
- ScheduleUpsertError,
- ScheduleUpsertResult,
-} from '../types/home';
-
-export const scheduleListMock: ScheduleListResult = {
- type: 'schedule.list.result',
- request_id: 'req_schedule_list_mock',
- ok: true,
- payload: {
- schedules: [
- {
- id: 'schedule_001',
- user_id: 'default_user',
- source_mode: 'voice',
- schedule_type: 'time',
- status: 'done',
- title: '复习 Java 集合基础',
- notes: '复习 List、Set 与 Map 的常见实现。',
- start_time: '2026-07-27T08:40:00+08:00',
- end_time: '2026-07-27T09:15:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: null,
- location_address: null,
- latitude: null,
- longitude: null,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: '2026-07-27T08:25:00+08:00',
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_001',
- system_alarm_ref_id: null,
- created_at: '2026-07-26T20:10:00+08:00',
- updated_at: '2026-07-27T09:15:00+08:00',
- },
- {
- id: 'schedule_002',
- user_id: 'default_user',
- source_mode: 'manual',
- schedule_type: 'time',
- status: 'scheduled',
- title: '需求分析讨论',
- notes: null,
- start_time: '2026-07-28T14:00:00+08:00',
- end_time: '2026-07-28T14:45:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: '张江办公室',
- location_address: '上海市浦东新区张江路 88 号',
- latitude: 31.2015,
- longitude: 121.5871,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_002',
- system_alarm_ref_id: null,
- created_at: '2026-07-27T18:30:00+08:00',
- updated_at: '2026-07-27T18:30:00+08:00',
- },
- {
- id: 'schedule_003',
- user_id: 'default_user',
- source_mode: 'voice',
- schedule_type: 'time',
- status: 'done',
- title: '完成需求分析初稿',
- notes: null,
- start_time: '2026-07-29T09:25:00+08:00',
- end_time: '2026-07-29T09:50:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: null,
- location_address: null,
- latitude: null,
- longitude: null,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: '2026-07-29T09:10:00+08:00',
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_003',
- system_alarm_ref_id: null,
- created_at: '2026-07-28T21:30:00+08:00',
- updated_at: '2026-07-29T09:50:00+08:00',
- },
- {
- id: 'schedule_004',
- user_id: 'default_user',
- source_mode: 'manual',
- schedule_type: 'time',
- status: 'scheduled',
- title: '项目周会',
- notes: '同步本周进展与风险。',
- start_time: '2026-07-29T10:30:00+08:00',
- end_time: '2026-07-29T11:20:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: '3 号会议室',
- location_address: '上海市浦东新区张江路 88 号 5 楼',
- latitude: 31.2016,
- longitude: 121.587,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_004',
- system_alarm_ref_id: null,
- created_at: '2026-07-25T10:00:00+08:00',
- updated_at: '2026-07-25T10:00:00+08:00',
- },
- {
- id: 'schedule_005',
- user_id: 'default_user',
- source_mode: 'voice',
- schedule_type: 'time',
- status: 'scheduled',
- title: '复习 JVM 内存模型',
- notes: null,
- start_time: '2026-07-29T19:30:00+08:00',
- end_time: '2026-07-29T20:05:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: null,
- location_address: null,
- latitude: null,
- longitude: null,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_005',
- system_alarm_ref_id: null,
- created_at: '2026-07-28T22:00:00+08:00',
- updated_at: '2026-07-28T22:00:00+08:00',
- },
- {
- id: 'schedule_006',
- user_id: 'default_user',
- source_mode: 'manual',
- schedule_type: 'time',
- status: 'scheduled',
- title: '整理项目介绍提纲',
- notes: null,
- start_time: '2026-07-29T20:15:00+08:00',
- end_time: '2026-07-29T20:35:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: null,
- location_address: null,
- latitude: null,
- longitude: null,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_006',
- system_alarm_ref_id: null,
- created_at: '2026-07-28T22:10:00+08:00',
- updated_at: '2026-07-28T22:10:00+08:00',
- },
- {
- id: 'schedule_007',
- user_id: 'default_user',
- source_mode: 'voice',
- schedule_type: 'time',
- status: 'scheduled',
- title: '整理面试问题清单',
- notes: '优先整理并发与数据库问题。',
- start_time: '2026-07-31T19:30:00+08:00',
- end_time: '2026-07-31T20:20:00+08:00',
- timezone: 'Asia/Shanghai',
- location_name: null,
- location_address: null,
- latitude: null,
- longitude: null,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_007',
- system_alarm_ref_id: null,
- created_at: '2026-07-28T22:20:00+08:00',
- updated_at: '2026-07-28T22:20:00+08:00',
- },
- {
- id: 'schedule_008',
- user_id: 'default_user',
- source_mode: 'voice',
- schedule_type: 'location',
- status: 'scheduled',
- title: '到公司后提交实习简历',
- notes: '提交前再次检查附件命名。',
- start_time: null,
- end_time: null,
- timezone: null,
- location_name: '张江办公室',
- location_address: '上海市浦东新区张江路 88 号',
- latitude: 31.2015,
- longitude: 121.5871,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- time_triggered_at: null,
- geo_triggered_at: null,
- system_schedule_ref_id: 'system_schedule_008',
- system_alarm_ref_id: null,
- created_at: '2026-07-29T08:00:00+08:00',
- updated_at: '2026-07-29T08:00:00+08:00',
- },
- ],
- },
-};
-
-export const scheduleListQueryMock: ScheduleListQuery = {
- type: 'schedule.list.query',
- request_id: 'req_schedule_list_001',
- payload: {
- status: null,
- include_deleted: false,
- },
-};
-
-export const scheduleUpsertMock: ScheduleUpsertCommand = {
- type: 'schedule.upsert.command',
- request_id: 'req_schedule_001',
- payload: {
- schedule_id: null,
- source_mode: 'voice',
- schedule_type: 'time',
- title: '开会',
- notes: null,
- start_time: '2026-07-29T15:00:00+08:00',
- end_time: null,
- timezone: 'Asia/Shanghai',
- location_name: '陆家嘴',
- location_address: null,
- latitude: 31.2451,
- longitude: 121.5067,
- geofence_radius_meters: 100,
- geofence_armed: true,
- time_remind_offset_minutes: 15,
- },
-};
-
-export const scheduleUpsertResultMock: ScheduleUpsertResult = {
- type: 'schedule.upsert.result',
- request_id: 'req_schedule_001',
- ok: true,
- payload: {
- schedule_id: 'schedule_001',
- schedule_type: 'time',
- status: 'scheduled',
- conflicts: [],
- geofence_armed: true,
- },
-};
-
-function findScheduleConflicts(
- command: ScheduleUpsertCommand,
- schedules: Schedule[],
- currentScheduleId: string,
-): ScheduleConflict[] {
- const { end_time: endTime, start_time: startTime } = command.payload;
- if (!startTime) return [];
-
- const start = new Date(startTime).getTime();
- const end = endTime ? new Date(endTime).getTime() : start;
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
-
- return schedules
- .filter((item) => item.id !== currentScheduleId && item.status !== 'deleted' && item.start_time)
- .filter((item) => {
- const itemStart = new Date(item.start_time!).getTime();
- const itemEnd = item.end_time ? new Date(item.end_time).getTime() : itemStart;
- return (
- Number.isFinite(itemStart) &&
- Number.isFinite(itemEnd) &&
- start <= itemEnd &&
- itemStart <= end
- );
- })
- .map((item) => ({
- schedule_id: item.id,
- title: item.title,
- start_time: item.start_time!,
- end_time: item.end_time,
- }));
-}
-
-export function createScheduleUpsertResultMock(
- command: ScheduleUpsertCommand,
- schedules: Schedule[],
- scheduleId: string,
-): ScheduleUpsertResult {
- const existingSchedule = schedules.find((item) => item.id === scheduleId);
-
- return {
- ...scheduleUpsertResultMock,
- request_id: command.request_id,
- payload: {
- ...scheduleUpsertResultMock.payload,
- schedule_id: scheduleId,
- schedule_type: command.payload.schedule_type,
- conflicts: findScheduleConflicts(command, schedules, scheduleId),
- geofence_armed: command.payload.geofence_armed ?? existingSchedule?.geofence_armed ?? true,
- },
- };
-}
-
-export const scheduleUpsertConflictMock: ScheduleUpsertResult = {
- type: 'schedule.upsert.result',
- request_id: 'req_schedule_001',
- ok: true,
- payload: {
- schedule_id: 'schedule_001',
- schedule_type: 'time',
- status: 'scheduled',
- conflicts: [
- {
- schedule_id: 'schedule_older',
- title: '已有日程',
- start_time: '2026-07-28T15:00:00+08:00',
- end_time: '2026-07-28T16:00:00+08:00',
- },
- ],
- geofence_armed: true,
- },
-};
-
-export const scheduleUpsertErrorMock: ScheduleUpsertError = {
- type: 'schedule.upsert.error',
- request_id: 'req_schedule_001',
- ok: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: '请求参数不合法',
- details: {
- field: 'schedule_type',
- reason: 'schedule_type 为 time 时 start_time 必填;为 location 时 latitude 和 longitude 必填',
- },
- },
-};
diff --git a/frontend/src/screens/AssistantScreen.styles.ts b/frontend/src/screens/AssistantScreen.styles.ts
deleted file mode 100644
index b00f15d..0000000
--- a/frontend/src/screens/AssistantScreen.styles.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { StyleSheet } from 'react-native';
-
-import { colors } from '../constants/theme';
-
-export const assistantStyles = StyleSheet.create({
- assistantScreen: { backgroundColor: colors.background, flex: 1, padding: 20 },
- assistantHeader: { paddingTop: 8 },
- assistantNavRow: {
- alignItems: 'center',
- flexDirection: 'row',
- },
- assistantEyebrow: {
- color: colors.muted,
- fontSize: 10,
- fontWeight: '700',
- letterSpacing: 0,
- marginLeft: 11,
- },
- assistantTitle: { color: colors.ink, fontSize: 28, fontWeight: '800', marginTop: 18 },
- assistantSubtitle: { color: colors.sub, fontSize: 13, lineHeight: 20, marginTop: 8 },
- assistantMessageRow: { alignItems: 'flex-start', flexDirection: 'row', marginTop: 28 },
- assistantMark: {
- backgroundColor: colors.deep,
- borderColor: 'rgba(215,243,106,0.34)',
- borderRadius: 12,
- borderWidth: 1,
- elevation: 4,
- height: 38,
- alignItems: 'center',
- justifyContent: 'center',
- width: 38,
- },
- assistantBubble: {
- backgroundColor: colors.surface,
- borderColor: '#ECE9E2',
- borderRadius: 12,
- borderWidth: 1,
- flex: 1,
- marginLeft: 10,
- padding: 16,
- },
- assistantBubbleText: { color: colors.ink, fontSize: 14, lineHeight: 21 },
- assistantComposer: { flexShrink: 0, marginTop: 'auto' },
- selectedImageRow: {
- alignItems: 'center',
- alignSelf: 'flex-start',
- backgroundColor: '#FFFFFF',
- borderColor: 'rgba(20,40,33,0.09)',
- borderRadius: 12,
- borderWidth: 1,
- elevation: 2,
- flexDirection: 'row',
- marginBottom: 8,
- maxWidth: '78%',
- padding: 5,
- },
- selectedImageThumbnail: { borderRadius: 8, height: 34, width: 34 },
- selectedImageName: {
- color: colors.ink,
- flexShrink: 1,
- fontSize: 12,
- fontWeight: '600',
- marginHorizontal: 8,
- },
- selectedImageRemove: {
- alignItems: 'center',
- borderRadius: 9,
- height: 28,
- justifyContent: 'center',
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- width: 28,
- },
- commandInputRow: {
- alignItems: 'center',
- backgroundColor: 'rgba(255,255,255,0.96)',
- borderColor: 'rgba(20,40,33,0.09)',
- borderRadius: 18,
- borderWidth: 1,
- elevation: 4,
- flexDirection: 'row',
- gap: 5,
- minHeight: 60,
- padding: 8,
- },
- commandInputRowCompact: { gap: 3, paddingHorizontal: 6, paddingVertical: 7 },
- commandMedia: {
- alignItems: 'center',
- backgroundColor: '#F5F6F2',
- borderRadius: 14,
- height: 40,
- justifyContent: 'center',
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- width: 40,
- },
- commandHold: {
- alignItems: 'center',
- backgroundColor: '#FFFFFF',
- borderRadius: 12,
- flex: 1,
- height: 44,
- justifyContent: 'center',
- minWidth: 0,
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- },
- commandHoldActive: {
- backgroundColor: colors.limeSoft,
- transform: [{ scale: 0.985 }],
- },
- commandHoldCompact: { height: 42 },
- commandHoldContent: {
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- },
- commandHoldText: {
- color: '#1C2420',
- fontSize: 15,
- fontWeight: '700',
- letterSpacing: 0,
- },
- commandHoldTextCompact: { fontSize: 14 },
- commandWave: {
- alignItems: 'center',
- flexDirection: 'row',
- gap: 3,
- height: 24,
- left: 14,
- position: 'absolute',
- },
- commandWaveBar: {
- backgroundColor: '#142E26',
- borderRadius: 2,
- width: 3,
- },
- commandTextPanel: {
- alignItems: 'center',
- flex: 1,
- flexDirection: 'row',
- gap: 5,
- minWidth: 0,
- },
- commandInput: {
- backgroundColor: '#F0F3EF',
- borderRadius: 12,
- borderWidth: 0,
- color: colors.ink,
- flex: 1,
- fontSize: 14,
- height: 44,
- minWidth: 0,
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- paddingHorizontal: 13,
- },
- commandInputCompact: { height: 42 },
- commandInputFocused: {
- backgroundColor: '#FFFFFF',
- borderColor: '#AEBBB4',
- borderWidth: 1,
- },
- commandSend: {
- alignItems: 'center',
- backgroundColor: colors.deep,
- borderRadius: 12,
- height: 40,
- justifyContent: 'center',
- minWidth: 48,
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- paddingHorizontal: 11,
- },
- commandSendCompact: { minWidth: 44, paddingHorizontal: 9 },
- commandSendText: { color: colors.surface, fontSize: 12, fontWeight: '700' },
- commandMode: {
- alignItems: 'center',
- backgroundColor: 'transparent',
- borderRadius: 14,
- height: 40,
- justifyContent: 'center',
- outlineColor: 'transparent',
- outlineStyle: 'solid',
- outlineWidth: 0,
- width: 40,
- },
- commandModeCompact: { height: 38, width: 36 },
- commandControlPressed: {
- backgroundColor: '#E7EBE4',
- transform: [{ scale: 0.94 }],
- },
- assistantDock: {
- alignItems: 'center',
- backgroundColor: 'rgba(255,254,250,0.94)',
- borderTopColor: 'rgba(30,40,35,0.08)',
- borderTopWidth: 1,
- bottom: 0,
- height: 64,
- justifyContent: 'center',
- left: 0,
- paddingBottom: 8,
- position: 'absolute',
- right: 0,
- zIndex: 20,
- },
- assistantDockButton: {
- alignItems: 'center',
- backgroundColor: '#142821',
- borderRadius: 15,
- boxShadow: '0 7px 18px rgba(20,40,33,0.25)',
- elevation: 6,
- height: 46,
- justifyContent: 'center',
- width: 46,
- },
- assistantDockButtonPressed: { opacity: 0.9, transform: [{ scale: 0.95 }] },
-});
diff --git a/frontend/src/screens/HomeScreen.tsx b/frontend/src/screens/HomeScreen.tsx
deleted file mode 100644
index b86246e..0000000
--- a/frontend/src/screens/HomeScreen.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, Text, View } from 'react-native';
-
-import { colors, spacing } from '../constants/theme';
-
-export function HomeScreen() {
- return (
-
- Timeflow
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- backgroundColor: colors.background,
- flex: 1,
- justifyContent: 'center',
- },
- title: {
- color: colors.text,
- fontSize: 24,
- padding: spacing.md,
- },
-});
diff --git a/frontend/src/shared/components/AppDialogProvider.styles.ts b/frontend/src/shared/components/AppDialogProvider.styles.ts
new file mode 100644
index 0000000..ff4aa31
--- /dev/null
+++ b/frontend/src/shared/components/AppDialogProvider.styles.ts
@@ -0,0 +1,86 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const appDialogStyles = StyleSheet.create({
+ backdrop: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(14, 23, 19, 0.46)',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 24,
+ },
+ dismiss: {
+ bottom: 0,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ dialog: {
+ backgroundColor: colors.surface,
+ borderColor: colors.line,
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 18,
+ width: '100%',
+ maxWidth: 360,
+ },
+ icon: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderRadius: 8,
+ height: 36,
+ justifyContent: 'center',
+ marginBottom: 13,
+ width: 36,
+ },
+ iconDanger: {
+ backgroundColor: colors.peach,
+ },
+ title: {
+ color: colors.ink,
+ fontSize: 20,
+ fontWeight: '800',
+ lineHeight: 26,
+ },
+ message: {
+ color: colors.sub,
+ fontSize: 14,
+ lineHeight: 21,
+ marginTop: 9,
+ },
+ actions: {
+ flexDirection: 'row',
+ gap: 10,
+ justifyContent: 'flex-end',
+ marginTop: 22,
+ },
+ action: {
+ alignItems: 'center',
+ borderRadius: 8,
+ minHeight: 42,
+ minWidth: 84,
+ justifyContent: 'center',
+ paddingHorizontal: 15,
+ },
+ cancelAction: {
+ backgroundColor: colors.surfaceTint,
+ },
+ primaryAction: {
+ backgroundColor: colors.deep,
+ },
+ dangerAction: {
+ backgroundColor: colors.coral,
+ },
+ cancelText: {
+ color: colors.ink,
+ fontSize: 14,
+ fontWeight: '800',
+ },
+ primaryText: {
+ color: colors.surface,
+ fontSize: 14,
+ fontWeight: '800',
+ },
+});
diff --git a/frontend/src/shared/components/AppDialogProvider.tsx b/frontend/src/shared/components/AppDialogProvider.tsx
new file mode 100644
index 0000000..692b44a
--- /dev/null
+++ b/frontend/src/shared/components/AppDialogProvider.tsx
@@ -0,0 +1,162 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
+import { CircleAlert } from 'lucide-react-native';
+import { Modal, Pressable, Text, View } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+import { appDialogStyles as styles } from './AppDialogProvider.styles';
+
+export type AppDialogTone = 'default' | 'danger';
+
+export type AppDialogOptions = {
+ title: string;
+ message: string;
+ tone?: AppDialogTone;
+ confirmLabel?: string;
+ cancelLabel?: string;
+};
+
+type DialogRequest = Required> &
+ Pick & {
+ id: string;
+ mode: 'notice' | 'confirm';
+ resolve: (confirmed: boolean) => void;
+ };
+
+type AppDialogContextValue = {
+ showNotice: (options: AppDialogOptions) => Promise;
+ confirm: (options: AppDialogOptions) => Promise;
+};
+
+const AppDialogContext = createContext(null);
+
+let dialogSequence = 0;
+
+export function AppDialogProvider({ children }: { children: ReactNode }) {
+ const queueRef = useRef([]);
+ const [active, setActive] = useState(null);
+
+ const enqueue = useCallback(
+ (mode: DialogRequest['mode'], options: AppDialogOptions): Promise => {
+ return new Promise((resolve) => {
+ const request: DialogRequest = {
+ id: `dialog_${++dialogSequence}`,
+ mode,
+ title: options.title,
+ message: options.message,
+ tone: options.tone ?? 'default',
+ confirmLabel: options.confirmLabel,
+ cancelLabel: options.cancelLabel,
+ resolve,
+ };
+ queueRef.current = [...queueRef.current, request];
+ if (queueRef.current.length === 1) setActive(request);
+ });
+ },
+ [],
+ );
+
+ const settle = useCallback((confirmed: boolean) => {
+ const [current, ...rest] = queueRef.current;
+ if (!current) return;
+ queueRef.current = rest;
+ setActive(rest[0] ?? null);
+ current.resolve(confirmed);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ for (const request of queueRef.current) request.resolve(false);
+ queueRef.current = [];
+ };
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ showNotice: async (options) => {
+ await enqueue('notice', options);
+ },
+ confirm: (options) => enqueue('confirm', options),
+ }),
+ [enqueue],
+ );
+
+ const isDanger = active?.tone === 'danger';
+ const confirmLabel = active?.confirmLabel ?? (active?.mode === 'confirm' ? '确定' : '知道了');
+ const cancelLabel = active?.cancelLabel ?? '取消';
+
+ return (
+
+ {children}
+ settle(false)}
+ transparent
+ visible={Boolean(active)}
+ >
+
+ settle(false)}
+ style={styles.dismiss}
+ />
+ {active ? (
+
+
+
+
+ {active.title}
+ {active.message}
+
+ {active.mode === 'confirm' ? (
+ settle(false)}
+ style={[styles.action, styles.cancelAction]}
+ >
+ {cancelLabel}
+
+ ) : null}
+ settle(true)}
+ style={[
+ styles.action,
+ styles.primaryAction,
+ active.mode === 'confirm' && isDanger && styles.dangerAction,
+ ]}
+ >
+ {confirmLabel}
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+export function useAppDialog(): AppDialogContextValue {
+ const value = useContext(AppDialogContext);
+ if (!value) {
+ throw new Error('useAppDialog must be used within AppDialogProvider');
+ }
+ return value;
+}
diff --git a/frontend/src/components/BackButton.tsx b/frontend/src/shared/components/BackButton.tsx
similarity index 95%
rename from frontend/src/components/BackButton.tsx
rename to frontend/src/shared/components/BackButton.tsx
index e57c644..e228824 100644
--- a/frontend/src/components/BackButton.tsx
+++ b/frontend/src/shared/components/BackButton.tsx
@@ -1,7 +1,7 @@
import { ChevronLeft } from 'lucide-react-native';
import { Pressable, StyleSheet } from 'react-native';
-import { colors } from '../constants/theme';
+import { colors } from '@/shared/theme';
type BackButtonProps = {
accessibilityLabel?: string;
diff --git a/frontend/src/shared/components/BottomSheetFrame.styles.ts b/frontend/src/shared/components/BottomSheetFrame.styles.ts
new file mode 100644
index 0000000..49094af
--- /dev/null
+++ b/frontend/src/shared/components/BottomSheetFrame.styles.ts
@@ -0,0 +1,34 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+import { sheetChromeStyles } from './sheetChrome.styles';
+
+const localStyles = StyleSheet.create({
+ keyboardAvoider: { flex: 1 },
+ sheet: {
+ backgroundColor: colors.surface,
+ borderTopLeftRadius: 26,
+ borderTopRightRadius: 26,
+ paddingBottom: 28,
+ paddingHorizontal: 16,
+ paddingTop: 10,
+ },
+ header: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ paddingHorizontal: 4,
+ },
+ eyebrow: { color: colors.muted, fontSize: 10, fontWeight: '700' },
+ title: { color: colors.ink, fontSize: 22, fontWeight: '800', marginTop: 5 },
+});
+
+export const bottomSheetFrameStyles = {
+ backdrop: sheetChromeStyles.backdrop,
+ dismiss: sheetChromeStyles.dismiss,
+ handle: sheetChromeStyles.handle,
+ close: sheetChromeStyles.close,
+ ...localStyles,
+};
diff --git a/frontend/src/shared/components/BottomSheetFrame.tsx b/frontend/src/shared/components/BottomSheetFrame.tsx
new file mode 100644
index 0000000..2b848dc
--- /dev/null
+++ b/frontend/src/shared/components/BottomSheetFrame.tsx
@@ -0,0 +1,106 @@
+import type { ReactNode } from 'react';
+import { X } from 'lucide-react-native';
+import {
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ Text,
+ View,
+ type StyleProp,
+ type ViewStyle,
+} from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+import { bottomSheetFrameStyles as styles } from './BottomSheetFrame.styles';
+
+type BottomSheetFrameProps = {
+ visible: boolean;
+ onClose: () => void;
+ closeAccessibilityLabel: string;
+ children: ReactNode;
+ /** 标准 eyebrow + title 头;与 header 二选一。 */
+ eyebrow?: string;
+ title?: string;
+ /** 自定义头部左侧内容(覆盖 eyebrow/title)。 */
+ header?: ReactNode;
+ showClose?: boolean;
+ showHandle?: boolean;
+ sheetStyle?: StyleProp;
+ headerStyle?: StyleProp;
+ keyboardAvoiding?: boolean;
+ animationType?: 'slide' | 'fade' | 'none';
+};
+
+/**
+ * 底部 Sheet 共用外壳:Modal → backdrop → dismiss → sheet → handle → header → content。
+ */
+export function BottomSheetFrame({
+ visible,
+ onClose,
+ closeAccessibilityLabel,
+ children,
+ eyebrow,
+ title,
+ header,
+ showClose = true,
+ showHandle = true,
+ sheetStyle,
+ headerStyle,
+ keyboardAvoiding = false,
+ animationType = 'slide',
+}: BottomSheetFrameProps) {
+ const hasHeader = Boolean(header || title || eyebrow);
+ const headerLeft = header ?? (
+
+ {eyebrow ? {eyebrow} : null}
+ {title ? {title} : null}
+
+ );
+
+ const body = (
+
+
+
+ {showHandle ? : null}
+ {hasHeader ? (
+
+ {headerLeft}
+ {showClose ? (
+
+
+
+ ) : null}
+
+ ) : null}
+ {children}
+
+
+ );
+
+ return (
+
+ {keyboardAvoiding ? (
+
+ {body}
+
+ ) : (
+ body
+ )}
+
+ );
+}
diff --git a/frontend/src/shared/components/DatePickerSheet.styles.ts b/frontend/src/shared/components/DatePickerSheet.styles.ts
new file mode 100644
index 0000000..08bf810
--- /dev/null
+++ b/frontend/src/shared/components/DatePickerSheet.styles.ts
@@ -0,0 +1,22 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const datePickerSheetStyles = StyleSheet.create({
+ calendar: {
+ borderRadius: 14,
+ overflow: 'hidden',
+ },
+ todayButton: {
+ alignItems: 'center',
+ backgroundColor: colors.limeSoft,
+ borderColor: '#D7E6B2',
+ borderRadius: 13,
+ borderWidth: 1,
+ height: 48,
+ justifyContent: 'center',
+ marginHorizontal: 4,
+ marginTop: 12,
+ },
+ todayButtonText: { color: colors.deep, fontSize: 13, fontWeight: '800' },
+});
diff --git a/frontend/src/shared/components/DatePickerSheet.tsx b/frontend/src/shared/components/DatePickerSheet.tsx
new file mode 100644
index 0000000..8435237
--- /dev/null
+++ b/frontend/src/shared/components/DatePickerSheet.tsx
@@ -0,0 +1,145 @@
+import { useMemo } from 'react';
+import { Pressable, Text } from 'react-native';
+import { Calendar, LocaleConfig, type DateData } from 'react-native-calendars';
+
+import { BottomSheetFrame } from '@/shared/components/BottomSheetFrame';
+import { colors } from '@/shared/theme';
+import { dateKey } from '@/shared/utils/date';
+
+import { datePickerSheetStyles as styles } from './DatePickerSheet.styles';
+
+type DayMarking = {
+ dotColor?: string;
+ marked?: boolean;
+ selected?: boolean;
+ selectedColor?: string;
+};
+
+type MarkedDates = Record;
+
+LocaleConfig.locales.zh = {
+ monthNames: [
+ '一月',
+ '二月',
+ '三月',
+ '四月',
+ '五月',
+ '六月',
+ '七月',
+ '八月',
+ '九月',
+ '十月',
+ '十一月',
+ '十二月',
+ ],
+ monthNamesShort: [
+ '1月',
+ '2月',
+ '3月',
+ '4月',
+ '5月',
+ '6月',
+ '7月',
+ '8月',
+ '9月',
+ '10月',
+ '11月',
+ '12月',
+ ],
+ dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
+ dayNamesShort: ['日', '一', '二', '三', '四', '五', '六'],
+ today: '今天',
+};
+LocaleConfig.defaultLocale = 'zh';
+
+const calendarTheme = {
+ arrowColor: colors.deep,
+ backgroundColor: colors.surface,
+ calendarBackground: colors.surface,
+ dayTextColor: colors.ink,
+ dotColor: colors.coral,
+ monthTextColor: colors.ink,
+ selectedDayBackgroundColor: colors.deep,
+ selectedDayTextColor: colors.surface,
+ selectedDotColor: colors.surface,
+ textDayFontSize: 14,
+ textDayFontWeight: '600' as const,
+ textDayHeaderFontSize: 12,
+ textDayHeaderFontWeight: '700' as const,
+ textMonthFontSize: 16,
+ textMonthFontWeight: '800' as const,
+ textSectionTitleColor: colors.sub,
+ todayTextColor: colors.deep,
+};
+
+function parseDateKey(value: string) {
+ const [year, month, day] = value.split('-').map(Number);
+ return new Date(year, month - 1, day);
+}
+
+type DatePickerSheetProps = {
+ markedDateKeys?: string[];
+ onClose: () => void;
+ onSelect: (date: Date) => void;
+ selectedDate: Date;
+ visible: boolean;
+};
+
+export function DatePickerSheet({
+ markedDateKeys = [],
+ onClose,
+ onSelect,
+ selectedDate,
+ visible,
+}: DatePickerSheetProps) {
+ const selectedKey = dateKey(selectedDate);
+ const markedDates = useMemo(() => {
+ const next: MarkedDates = {};
+ for (const key of markedDateKeys) {
+ next[key] = { marked: true, dotColor: colors.coral };
+ }
+ next[selectedKey] = {
+ ...(next[selectedKey] ?? {}),
+ selected: true,
+ selectedColor: colors.deep,
+ };
+ return next;
+ }, [markedDateKeys, selectedKey]);
+
+ const handleDayPress = (day: DateData) => {
+ onSelect(parseDateKey(day.dateString));
+ onClose();
+ };
+
+ return (
+
+
+ {
+ onSelect(new Date());
+ onClose();
+ }}
+ style={styles.todayButton}
+ >
+ 回到今天
+
+
+ );
+}
diff --git a/frontend/src/shared/components/TimePickerSheet.styles.ts b/frontend/src/shared/components/TimePickerSheet.styles.ts
new file mode 100644
index 0000000..b38c1cd
--- /dev/null
+++ b/frontend/src/shared/components/TimePickerSheet.styles.ts
@@ -0,0 +1,65 @@
+import { StyleSheet } from 'react-native';
+
+import { colors } from '@/shared/theme';
+
+export const timePickerSheetStyles = StyleSheet.create({
+ preview: {
+ color: colors.ink,
+ fontSize: 28,
+ fontWeight: '800',
+ marginBottom: 12,
+ marginTop: 4,
+ textAlign: 'center',
+ },
+ columns: {
+ flexDirection: 'row',
+ gap: 10,
+ paddingHorizontal: 4,
+ },
+ column: { flex: 1 },
+ columnLabel: {
+ color: colors.sub,
+ fontSize: 12,
+ fontWeight: '800',
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ list: {
+ backgroundColor: '#F8FAF7',
+ borderColor: colors.line,
+ borderRadius: 14,
+ borderWidth: 1,
+ maxHeight: 220,
+ },
+ item: {
+ alignItems: 'center',
+ height: 44,
+ justifyContent: 'center',
+ },
+ itemSelected: {
+ backgroundColor: colors.limeSoft,
+ },
+ itemText: {
+ color: colors.ink,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ itemTextSelected: {
+ color: colors.deep,
+ fontWeight: '800',
+ },
+ confirm: {
+ alignItems: 'center',
+ backgroundColor: colors.deep,
+ borderRadius: 13,
+ height: 48,
+ justifyContent: 'center',
+ marginHorizontal: 4,
+ marginTop: 14,
+ },
+ confirmText: {
+ color: colors.surface,
+ fontSize: 13,
+ fontWeight: '800',
+ },
+});
diff --git a/frontend/src/shared/components/TimePickerSheet.tsx b/frontend/src/shared/components/TimePickerSheet.tsx
new file mode 100644
index 0000000..4e12239
--- /dev/null
+++ b/frontend/src/shared/components/TimePickerSheet.tsx
@@ -0,0 +1,127 @@
+import { useEffect, useMemo, useRef, useState, type RefObject } from 'react';
+import { Pressable, ScrollView, Text, View } from 'react-native';
+
+import { BottomSheetFrame } from '@/shared/components/BottomSheetFrame';
+
+import { timePickerSheetStyles as styles } from './TimePickerSheet.styles';
+
+const HOURS = Array.from({ length: 24 }, (_, index) => String(index).padStart(2, '0'));
+const MINUTES = Array.from({ length: 60 }, (_, index) => String(index).padStart(2, '0'));
+const ITEM_HEIGHT = 44;
+
+type TimePickerSheetProps = {
+ onClose: () => void;
+ onSelect: (time: string) => void;
+ selectedTime: Date;
+ visible: boolean;
+};
+
+function TimeWheelColumn({
+ accessibilityUnit,
+ label,
+ listRef,
+ onSelect,
+ selected,
+ values,
+}: {
+ accessibilityUnit: string;
+ label: string;
+ listRef: RefObject;
+ onSelect: (value: string) => void;
+ selected: string;
+ values: string[];
+}) {
+ return (
+
+ {label}
+
+ {values.map((value) => {
+ const isSelected = value === selected;
+ return (
+ onSelect(value)}
+ style={[styles.item, isSelected && styles.itemSelected]}
+ >
+ {value}
+
+ );
+ })}
+
+
+ );
+}
+
+export function TimePickerSheet({
+ onClose,
+ onSelect,
+ selectedTime,
+ visible,
+}: TimePickerSheetProps) {
+ const [hour, setHour] = useState(() => String(selectedTime.getHours()).padStart(2, '0'));
+ const [minute, setMinute] = useState(() => String(selectedTime.getMinutes()).padStart(2, '0'));
+ const hourListRef = useRef(null);
+ const minuteListRef = useRef(null);
+ const selectedTimeMs = selectedTime.getTime();
+
+ useEffect(() => {
+ if (!visible) return;
+ const nextHour = String(new Date(selectedTimeMs).getHours()).padStart(2, '0');
+ const nextMinute = String(new Date(selectedTimeMs).getMinutes()).padStart(2, '0');
+ // 打开或外部时间变化时同步滚轮;属受控 sheet 的合法同步。
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- sync picker when sheet opens
+ setHour(nextHour);
+ setMinute(nextMinute);
+ const frame = requestAnimationFrame(() => {
+ hourListRef.current?.scrollTo({ y: Number(nextHour) * ITEM_HEIGHT, animated: false });
+ minuteListRef.current?.scrollTo({ y: Number(nextMinute) * ITEM_HEIGHT, animated: false });
+ });
+ return () => cancelAnimationFrame(frame);
+ }, [selectedTimeMs, visible]);
+
+ const preview = useMemo(() => `${hour}:${minute}`, [hour, minute]);
+
+ const confirm = () => {
+ onSelect(`${hour}:${minute}`);
+ onClose();
+ };
+
+ return (
+
+ {preview}
+
+