diff --git a/AGENTS.md b/AGENTS.md index 0e6bc80..171015e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,72 @@ -# Expo HAS CHANGED +# SwimBuddy -Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code. +Swimming workout tracker with Apple Watch companion app. Expo 57 + React Native 0.86 + NativeWind v5 + Drizzle ORM + Swift Watch target. + +## Commands + +```bash +npx expo start # Dev server +npx expo run:ios # Build & run on iOS +npx expo prebuild -p ios # Regenerate ios/ dir (safe — targets/ persists) +npx drizzle-kit generate # Regenerate SQL migrations after schema changes +``` + +No `npm test` or lint scripts exist yet. No `babel.config.js` — NativeWind v5 uses CSS-first config. + +## Project Structure + +``` +src/ + features//screens/ # Screen components (registered in navigators) + features//components/# Feature-specific UI + features//hooks/ # Feature-specific hooks + components/ # Shared UI primitives (flat) + hooks/ # Shared hooks + navigation/ # React Navigation (RootNav, TabNav, feature stacks) + db/ # Drizzle schema, client, relations, operations + providers/ # DatabaseProvider, SettingsProvider (MMKV) + utils/ # formatTime, calculateSwolf, etc. + constants/ # strokes, poolLengths, colors + types/ # workout.ts, common.ts + modules/watch-connectivity/# Expo native module bridging WCSession +targets/watch/ # Apple Watch app (Swift/SwiftUI) — survives prebuild +``` + +## Key Conventions + +- **Path aliases**: `@/`, `@features/`, `@components/`, `@db/`, etc. (tsconfig.json) +- **Feature-based** folder structure with **type-based** shared layer +- **NativeWind v5**: `className` prop, no babel config, CSS-first. Use `className` not `StyleSheet` +- **`className` types**: `nativewind-env.d.ts` at root — don't delete +- **lightningcss pinned** to 1.30.1 in package.json overrides — required by NativeWind v5 + +## Watch Target + +- Lives in `targets/watch/`, NOT in `ios/` — survives `expo prebuild --clean` +- Config: `targets/watch/expo-target.config.js` +- Build: open `ios/SwimBuddy.xcworkspace` in Xcode, select Watch scheme +- Uses App Groups (`group.nl.guido-it.swimbuddy`) for data sharing via UserDefaults +- WCSession for real-time messaging, `transferUserInfo` for reliable offline sync +- HealthKit for swim workout tracking (indoor swimming) +- Swift types `StoredWorkout`, `StoredSet`, `CompletedLap` in `WorkoutManager.swift` + +## Database + +- **expo-sqlite** + **Drizzle ORM** (SQLite dialect, expo driver) +- Schema: `src/db/schema.ts` — 4 tables: templates, workouts, sets, laps +- Relations: `src/db/relations.ts` +- CRUD: `src/db/operations.ts` +- Client: `src/db/client.ts` — exports `db` and `DATABASE_NAME` +- Migrations in `drizzle/` — generated by `npx drizzle-kit generate` + +## Gotchas + +- `metro.config.js` has `.sql` in `sourceExts` — required for Drizzle migration imports +- `expo prebuild` clears and regenerates `ios/` by default. Use `--no-clean` to preserve +- Watch target compiles as separate Xcode target — not a RN module +- `react-native-css` is a NativeWind v5 peer dependency — don't remove +- `@bacons/apple-targets` plugin must be in app.json plugins array + +## Expo Docs + +Expo SDK 57: https://docs.expo.dev/versions/v57.0.0/ diff --git a/App.tsx b/App.tsx index 0329d0c..3558171 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,19 @@ -import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import "./global.css"; +import { StatusBar } from "expo-status-bar"; +import { NavigationContainer } from "@react-navigation/native"; +import { RootNavigator } from "./src/navigation/RootNavigator"; +import { DatabaseProvider } from "./src/providers/DatabaseProvider"; +import { SettingsProvider } from "./src/providers/WatchProvider"; export default function App() { return ( - - Open up App.tsx to start working on your app! - - + + + + + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/app.json b/app.json index 575eef1..36a4ea7 100644 --- a/app.json +++ b/app.json @@ -1,25 +1,36 @@ { - "expo": { - "name": "SwimBuddy", - "slug": "SwimBuddy", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "light", - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/android-icon-foreground.png", - "backgroundImage": "./assets/android-icon-background.png", - "monochromeImage": "./assets/android-icon-monochrome.png" - }, - "predictiveBackGestureEnabled": false - }, - "web": { - "favicon": "./assets/favicon.png" - } - } + "expo": { + "name": "SwimBuddy", + "slug": "swimbuddy", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "ios": { + "supportsTablet": true, + "bundleIdentifier": "nl.guido-it.swimbuddy", + "entitlements": { + "com.apple.security.application-groups": [ + "group.nl.guido-it.swimbuddy" + ], + "com.apple.developer.healthkit": true + }, + "appleTeamId": "7TABQZ5458" + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/android-icon-foreground.png", + "backgroundImage": "./assets/android-icon-background.png", + "monochromeImage": "./assets/android-icon-monochrome.png" + }, + "predictiveBackGestureEnabled": false, + "package": "nl.guidoit.swimbuddy" + }, + "web": { + "favicon": "./assets/favicon.png", + "bundler": "metro" + }, + "plugins": ["expo-sqlite", "@bacons/apple-targets"] + } } diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..279bc7c --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "sqlite", + driver: "expo", +}); diff --git a/drizzle/0000_mushy_groot.sql b/drizzle/0000_mushy_groot.sql new file mode 100644 index 0000000..6ccb0f4 --- /dev/null +++ b/drizzle/0000_mushy_groot.sql @@ -0,0 +1,49 @@ +CREATE TABLE `laps` ( + `id` text PRIMARY KEY NOT NULL, + `workout_id` text NOT NULL, + `set_position` integer NOT NULL, + `rep_number` integer NOT NULL, + `distance` integer NOT NULL, + `duration` real NOT NULL, + `stroke_count` integer, + `heart_rate` integer, + `swolf` real, + FOREIGN KEY (`workout_id`) REFERENCES `workouts`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `sets` ( + `id` text PRIMARY KEY NOT NULL, + `workout_id` text NOT NULL, + `position` integer NOT NULL, + `stroke` text NOT NULL, + `distance_per_rep` integer NOT NULL, + `rep_count` integer NOT NULL, + `rest_type` text DEFAULT 'fixed_rest' NOT NULL, + `rest_duration` real DEFAULT 0 NOT NULL, + `target_time` real, + FOREIGN KEY (`workout_id`) REFERENCES `workouts`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `templates` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text, + `pool_length` real DEFAULT 25 NOT NULL, + `json_data` text NOT NULL, + `is_favorite` integer DEFAULT false NOT NULL, + `created_at` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `workouts` ( + `id` text PRIMARY KEY NOT NULL, + `name` text, + `template_id` text, + `pool_length` real DEFAULT 25 NOT NULL, + `started_at` text NOT NULL, + `finished_at` text, + `total_distance` integer, + `total_duration` real, + `calories` real, + `avg_heart_rate` integer, + `source` text DEFAULT 'manual' NOT NULL +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..c3dd745 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,344 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a9688cc9-9314-4d59-9b68-b7e202f3e3ff", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "laps": { + "name": "laps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workout_id": { + "name": "workout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "set_position": { + "name": "set_position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rep_number": { + "name": "rep_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance": { + "name": "distance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stroke_count": { + "name": "stroke_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "swolf": { + "name": "swolf", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "laps_workout_id_workouts_id_fk": { + "name": "laps_workout_id_workouts_id_fk", + "tableFrom": "laps", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sets": { + "name": "sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workout_id": { + "name": "workout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stroke": { + "name": "stroke", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance_per_rep": { + "name": "distance_per_rep", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rep_count": { + "name": "rep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rest_type": { + "name": "rest_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed_rest'" + }, + "rest_duration": { + "name": "rest_duration", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "target_time": { + "name": "target_time", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sets_workout_id_workouts_id_fk": { + "name": "sets_workout_id_workouts_id_fk", + "tableFrom": "sets", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "templates": { + "name": "templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pool_length": { + "name": "pool_length", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 25 + }, + "json_data": { + "name": "json_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workouts": { + "name": "workouts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pool_length": { + "name": "pool_length", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 25 + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_distance": { + "name": "total_distance", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_duration": { + "name": "total_duration", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calories": { + "name": "calories", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avg_heart_rate": { + "name": "avg_heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..ef960b2 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1785854381700, + "tag": "0000_mushy_groot", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/migrations.js b/drizzle/migrations.js new file mode 100644 index 0000000..664de05 --- /dev/null +++ b/drizzle/migrations.js @@ -0,0 +1,12 @@ +// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo + +import journal from './meta/_journal.json'; +import m0000 from './0000_mushy_groot.sql'; + + export default { + journal, + migrations: { + m0000 + } + } + \ No newline at end of file diff --git a/global.css b/global.css new file mode 100644 index 0000000..9431a57 --- /dev/null +++ b/global.css @@ -0,0 +1,5 @@ +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/preflight.css" layer(base); +@import "tailwindcss/utilities.css"; + +@import "nativewind/theme"; diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..b26eb61 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,9 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativewind } = require("nativewind/metro"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +config.resolver.sourceExts.push("sql"); + +module.exports = withNativewind(config); diff --git a/nativewind-env.d.ts b/nativewind-env.d.ts new file mode 100644 index 0000000..2939089 --- /dev/null +++ b/nativewind-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/package-lock.json b/package-lock.json index 27849f0..020c990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,29 @@ "name": "swimbuddy", "version": "1.0.0", "dependencies": { + "@bacons/apple-targets": "^5.0.0", + "@react-navigation/bottom-tabs": "^7.18.14", + "@react-navigation/native": "^7.3.14", + "@react-navigation/native-stack": "^7.18.6", + "drizzle-orm": "^0.45.2", "expo": "~57.0.10", + "expo-sqlite": "~57.0.1", "expo-status-bar": "~57.0.1", + "expo-system-ui": "~57.0.2", + "nativewind": "^5.0.0-preview.4", "react": "19.2.3", - "react-native": "0.86.2" + "react-native": "0.86.2", + "react-native-css": "^3.0.7", + "react-native-mmkv": "^4.3.2", + "react-native-reanimated": "4.5.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0" }, "devDependencies": { "@types/react": "~19.2.2", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.31.10", + "tailwindcss": "^4", "typescript": "~6.0.3" } }, @@ -379,6 +395,92 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", @@ -537,6 +639,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", @@ -910,6 +1028,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -926,6 +1076,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", @@ -955,6 +1121,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", @@ -1063,6 +1261,1217 @@ "node": ">=6.9.0" } }, + "node_modules/@bacons/apple-targets": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@bacons/apple-targets/-/apple-targets-5.0.0.tgz", + "integrity": "sha512-03LEidnuAAccH5ueL03sOaauQMnpJ7ZGwrVUlzNc5K3BrlfGE9SDUJs1ITkXC7YLKfFPJyiB7VzoZYiRxJmUdA==", + "license": "MIT", + "dependencies": { + "@bacons/xcode": "1.0.0-alpha.32", + "@expo/prebuild-config": "~55.0.6", + "@react-native/normalize-colors": "^0.79.2", + "debug": "^4.3.4", + "glob": "^10.4.2" + }, + "peerDependencies": { + "expo": ">=52" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config": { + "version": "55.0.19", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.19.tgz", + "integrity": "sha512-MahrCR6LsElK/1r/C/zfEZ5Pdgmo88Gtd4CCnASv/rC2pUIB+ENt2oKRHAPIWi7McTeoqDPb9KwCkQBpNMOjrg==", + "license": "MIT", + "dependencies": { + "@expo/config-plugins": "~55.0.11", + "@expo/config-types": "^55.0.6", + "@expo/json-file": "^10.0.15", + "@expo/require-utils": "^55.0.6", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config-plugins": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.11.tgz", + "integrity": "sha512-85ZSmIK8rMfvYbG/2IHtwnykVdb6LI0ZavduM3ZwUMThyyVegYjVsO5Zek2ec8xzPhCmYX0ar5onnFEcwUDUlw==", + "license": "MIT", + "dependencies": { + "@expo/config-types": "^55.0.6", + "@expo/json-file": "~10.0.15", + "@expo/plist": "^0.5.4", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config-plugins/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config-plugins/node_modules/@expo/json-file": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", + "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config-plugins/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_modules/@bacons/apple-targets/node_modules/@expo/config-types": { + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.6.tgz", + "integrity": "sha512-S+GJKYoIjnWlert/9vXuTohaTsMbyOLSVxdIgPgoq3P4N1p4CWrfyZLnz6qRug8wYSO5fcYcS9mFleyEP8wRLg==", + "license": "MIT" + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/config/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_modules/@bacons/apple-targets/node_modules/@expo/image-utils": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.15.tgz", + "integrity": "sha512-3f5CgKJnJ8m4mp8VTcAFQqLrxof99RDr77Aa+7hgzDPg3ZotjLnofl77hf+COQRYi/kuqRYdYFgPIqOIOIdWJA==", + "license": "MIT", + "dependencies": { + "@expo/require-utils": "^55.0.6", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "semver": "^7.6.0" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/json-file": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", + "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/plist": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.4.tgz", + "integrity": "sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/prebuild-config": { + "version": "55.0.20", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.20.tgz", + "integrity": "sha512-Cy9xnwK0f3aG0TbKkBFuVQCtd6rj22muYfj7o1iCAE0NOUy5ks1k2vcmlKD1g48wzcWw0hFySiwtaGsB9qeVRQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~55.0.19", + "@expo/config-plugins": "~55.0.11", + "@expo/config-types": "^55.0.6", + "@expo/image-utils": "^0.8.15", + "@expo/json-file": "^10.0.15", + "@react-native/normalize-colors": "0.83.10", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": { + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.10.tgz", + "integrity": "sha512-dWgqcxaBy27oLx9tndcTF0917vbLOOstyNjYD58J6Z7YxkEZSaKG6+A+3I1KV6O1Xg2D69QThp4t6ezoC3NiGA==", + "license": "MIT" + }, + "node_modules/@bacons/apple-targets/node_modules/@expo/require-utils": { + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.6.tgz", + "integrity": "sha512-IqsRGPdGbF0lAIrg3XQ+GzcITYcsskvIRmFAjw2kBnr8RgSrRhFAJY1bKksukEUsuZy0knhTCVIIUce7hocqeA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@bacons/apple-targets/node_modules/@react-native/normalize-colors": { + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.7.tgz", + "integrity": "sha512-RrvewhdanEWhlyrHNWGXGZCc6MY0JGpNgRzA8y6OomDz0JmlnlIsbBHbNpPnIrt9Jh2KaV10KTscD1Ry8xU9gQ==", + "license": "MIT" + }, + "node_modules/@bacons/apple-targets/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/@bacons/apple-targets/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@bacons/apple-targets/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bacons/apple-targets/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bacons/apple-targets/node_modules/glob/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bacons/apple-targets/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/@bacons/xcode": { + "version": "1.0.0-alpha.32", + "resolved": "https://registry.npmjs.org/@bacons/xcode/-/xcode-1.0.0-alpha.32.tgz", + "integrity": "sha512-OGpH7+yMbWC2cgYZon5B+VVadH9HsB2V/abtEiplA65XnSuV4GAYAVixOCDc5k182WkfoakfdM0zW6U9cbcsbw==", + "license": "MIT", + "dependencies": { + "@expo/plist": "^0.0.18", + "debug": "^4.3.4", + "uuid": "^8.3.2" + } + }, + "node_modules/@bacons/xcode/node_modules/@expo/plist": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.0.18.tgz", + "integrity": "sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "~0.7.0", + "base64-js": "^1.2.3", + "xmlbuilder": "^14.0.0" + } + }, + "node_modules/@bacons/xcode/node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version has critical issues, please update to the latest version", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@bacons/xcode/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@bacons/xcode/node_modules/xmlbuilder": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", + "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1265,6 +2674,72 @@ "metro-transform-worker": "0.84.4" } }, + "node_modules/@expo/metro-config": { + "version": "57.0.7", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.7.tgz", + "integrity": "sha512-bVfEkg4zF1cA62OqAdYXmFOooJ6TB/I+REi7Se6Ct+PbSC+89TwSqWXnYx34L08eIs4z+1ilgbATakTZpgefmQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~57.0.6", + "@expo/env": "~2.4.2", + "@expo/json-file": "~11.0.1", + "@expo/metro": "~56.0.0", + "@expo/require-utils": "^57.0.4", + "@expo/spawn-async": "^1.8.0", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.36.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/hermes-estree": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz", + "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==", + "license": "MIT" + }, + "node_modules/@expo/metro-config/node_modules/hermes-parser": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz", + "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.36.1" + } + }, + "node_modules/@expo/metro-config/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": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@expo/metro-file-map": { "version": "57.0.1", "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.1.tgz", @@ -1397,6 +2872,102 @@ "excpretty": "build/cli.js" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -1490,6 +3061,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.86.2", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.2.tgz", @@ -1512,6 +3093,54 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/babel-preset": { + "version": "0.86.2", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.2.tgz", + "integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@react-native/babel-plugin-codegen": "0.86.2", + "babel-plugin-syntax-hermes-parser": "0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/@react-native/codegen": { "version": "0.86.2", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.2.tgz", @@ -1642,6 +3271,58 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.86.2", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz", + "integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.86.2", + "hermes-parser": "0.36.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz", + "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-native/metro-babel-transformer/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", + "peer": true, + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.86.2", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.2.tgz", + "integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/js-polyfills": "0.86.2", + "@react-native/metro-babel-transformer": "0.86.2", + "metro-config": "^0.84.3", + "metro-runtime": "^0.84.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, "node_modules/@react-native/normalize-colors": { "version": "0.86.2", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", @@ -1671,12 +3352,133 @@ } } }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.18.14", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.14.tgz", + "integrity": "sha512-A3V9rDSut459TBPtkD7rb0npUUBlJBfMunyRT5nOGKqPguhuXWk1h91NfQMuqyW2DCUvvFZChzsbLbj42rXTdQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.36", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.3.14", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.21.11.tgz", + "integrity": "sha512-bCW1PsLA/eOXDOukcJFEzlcL3Zpy8DJuDCfkDDwAQlAgoSZ/J9+ZeDRUMmCUi6xbnFgvFEEIMertaLeErOFP0Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.6.4", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/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==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.36", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.36.tgz", + "integrity": "sha512-+10x9s5v2Q7FwAYdSmPMgILtxZyC5e4hWJQu8g5o3u4p8DUToTBmGvys/UvmEr+h9xmm0Go42qw9Ff2ape53kQ==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.3.14", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.3.14", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.14.tgz", + "integrity": "sha512-hcKTDNBuuAA1/xW6QeKYmMPVhk5W9dKGQpPmn5dQeeePwMpu5OZ14NOgwKH0w9D3tg2jupojTcVL0tsx5DTFXg==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.21.11", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "standard-navigation": "^0.0.8", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.18.6.tgz", + "integrity": "sha512-KuvvSBddHrbKC4c6yKz+UCFey2xgTuPQHgjf2wJQAvA7JM8jCQa3G1+QzhO6d44nYNKir3y6EounXZvQE/BX6w==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.36", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.3.14", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.4.tgz", + "integrity": "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, "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/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "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", @@ -1701,6 +3503,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", @@ -1877,12 +3685,34 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "license": "MIT" + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, + "node_modules/babel-plugin-inline-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-inline-import/-/babel-plugin-inline-import-3.0.0.tgz", + "integrity": "sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-resolve": "0.0.2" + } + }, "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", @@ -2263,6 +4093,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2281,6 +4124,26 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorjs.io": { + "version": "0.6.0-alpha.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.0-alpha.1.tgz", + "integrity": "sha512-c/h/8uAmPydQcriRdX8UTAFHj6SpSHFHBA8LvMikvYWAVApPTwg/pyOXNsGmaCBd6L/EeDlRHSNhTtnIFp/qsg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -2290,6 +4153,19 @@ "node": ">= 10" } }, + "node_modules/comment-json": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2431,6 +4307,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2486,6 +4371,153 @@ "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2531,6 +4563,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2558,6 +4632,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2692,6 +4779,20 @@ "node": ">=20.16.0" } }, + "node_modules/expo-sqlite": { + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-57.0.1.tgz", + "integrity": "sha512-I6KoUvfGIiROTKxr5D3H+jRIGA/iEvWEtqHK4XMukAA7tVTinz/YdS8zOz6/DdG6vgrNmvF7gyOcbhLinlfxzQ==", + "license": "MIT", + "dependencies": { + "await-lock": "^2.2.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-status-bar": { "version": "57.0.1", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-57.0.1.tgz", @@ -2703,6 +4804,26 @@ "react-native": "*" } }, + "node_modules/expo-system-ui": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.2.tgz", + "integrity": "sha512-zABCRqFSioDBAo/RtmS0dQiGgDtDPZUVk01Y3Ti4ducobNM4HTM2sNAtq+YPpEUhaVW3hccPVsEUH4LK/ADVhA==", + "license": "MIT", + "dependencies": { + "@react-native/normalize-colors": "0.86.2", + "debug": "^4.3.2" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "57.0.12", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.12.tgz", @@ -2847,45 +4968,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.7.tgz", - "integrity": "sha512-bVfEkg4zF1cA62OqAdYXmFOooJ6TB/I+REi7Se6Ct+PbSC+89TwSqWXnYx34L08eIs4z+1ilgbATakTZpgefmQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~57.0.6", - "@expo/env": "~2.4.2", - "@expo/json-file": "~11.0.1", - "@expo/metro": "~56.0.0", - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.36.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": 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", @@ -3052,21 +5134,6 @@ "react": "*" } }, - "node_modules/expo/node_modules/hermes-estree": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz", - "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==", - "license": "MIT" - }, - "node_modules/expo/node_modules/hermes-parser": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz", - "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.1" - } - }, "node_modules/expo/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3136,6 +5203,12 @@ "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==", + "license": "MIT" + }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -3175,6 +5248,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -3220,6 +5302,34 @@ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3229,6 +5339,21 @@ "node": ">= 0.6" } }, + "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", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3256,6 +5381,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-tsconfig": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", + "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getenv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", @@ -3429,6 +5567,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -3495,6 +5639,21 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -3700,9 +5859,9 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -3715,43 +5874,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "cpu": [ "arm64" ], @@ -3769,9 +5907,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "cpu": [ "x64" ], @@ -3789,9 +5927,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "cpu": [ "x64" ], @@ -3809,9 +5947,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "cpu": [ "arm" ], @@ -3829,9 +5967,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "cpu": [ "arm64" ], @@ -3852,9 +5990,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "cpu": [ "arm64" ], @@ -3875,9 +6013,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "cpu": [ "x64" ], @@ -3898,9 +6036,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "cpu": [ "x64" ], @@ -3921,9 +6059,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "cpu": [ "arm64" ], @@ -3941,9 +6079,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "cpu": [ "x64" ], @@ -4496,6 +6634,25 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nativewind": { + "version": "5.0.0-preview.4", + "resolved": "https://registry.npmjs.org/nativewind/-/nativewind-5.0.0-preview.4.tgz", + "integrity": "sha512-eLsFGczxIsWADoD18EbEyt01aWgw4uP1/t/O7JIsp/OEbSgVYVobW97e2DDTOxxiiqSu0RexC1KSo5d7tes9uQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "tailwindcss-safe-area": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "react-native-css": "^3.0.1", + "tailwindcss": ">4.1.11" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -4720,6 +6877,12 @@ "node": ">=4" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parse-png": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", @@ -4741,6 +6904,13 @@ "node": ">= 0.8" } }, + "node_modules/path-extra": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/path-extra/-/path-extra-1.0.3.tgz", + "integrity": "sha512-vYm3+GCkjUlT1rDvZnDVhNLXIRvwFPaN8ebHAFcuMJM/H0RBOPD7JrcldiNLd9AS3dhAyUHLa4Hny5wp1A+Ffw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4925,6 +7095,24 @@ "node": ">= 6" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -4962,6 +7150,18 @@ "ws": "^7" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5027,6 +7227,137 @@ } } }, + "node_modules/react-native-css": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/react-native-css/-/react-native-css-3.0.7.tgz", + "integrity": "sha512-xsU9T5MleHI/DDmY72A7ZikbkDfDNkMrAEichOJW2bBUMxPzAJPrFk9ys4cXYk4gLFaR11y23EccOJ+JT6Iusw==", + "license": "MIT", + "workspaces": [ + "example", + "configs", + "." + ], + "dependencies": { + "@types/debug": "^4.1.12", + "babel-plugin-react-compiler": "^19.1.0-rc.2", + "colorjs.io": "0.6.0-alpha.1", + "comment-json": "^4.2.5", + "debug": "^4.4.1" + }, + "peerDependencies": { + "@expo/metro-config": ">=54", + "lightningcss": ">=1.27.0", + "react": ">=19", + "react-native": ">=0.81" + } + }, + "node_modules/react-native-css/node_modules/babel-plugin-react-compiler": { + "version": "19.1.0-rc.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.3.tgz", + "integrity": "sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", + "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-mmkv": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz", + "integrity": "sha512-49OAyfkg0/TMWiWELZN6VuVQPZPhizwL4DTmp8b7B1md3dB/s3LH3mGfC3T+lp9W0y/rqxZMEnotLFTIbOAenQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "*" + } + }, + "node_modules/react-native-nitro-modules": { + "version": "0.36.5", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.36.5.tgz", + "integrity": "sha512-SJby84+hovD70JmQ1fq+56tw3oapJeChehUqXA4Gqjm+ktJhMAcj8LGwoAjStSOUwRaK7czbzBPB0Widj+EDoQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-reanimated": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz", + "integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.3.1", + "semver": "^7.7.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "0.83 - 0.86", + "react-native-worklets": "0.10.x" + } + }, + "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", + "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.26.2.tgz", + "integrity": "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-worklets": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.3.tgz", + "integrity": "sha512-NaDBNXc2l6p8ggNYJ2iMfD1Yov/PI1CTwL+d0Lczih18C0Tw37mY0sJd9DXi1zZNBeltAsWyZSSr1nOEdren3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/preset-typescript": "^7.28.5", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "semver": "^7.7.4" + }, + "peerDependencies": { + "@babel/core": "*", + "@react-native/metro-config": "*", + "react": "*", + "react-native": "0.83 - 0.86" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -5113,6 +7444,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-resolve": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/require-resolve/-/require-resolve-0.0.2.tgz", + "integrity": "sha512-eafQVaxdQsWUB8HybwognkdcIdKdQdQBwTxH48FuE6WI0owZGKp63QYr1MRp73PoX0AcyB7MDapZThYUY8FD0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "x-path": "^0.0.2" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -5143,6 +7484,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-workspace-root": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", @@ -5317,6 +7668,15 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5367,6 +7727,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5419,6 +7788,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -5437,6 +7815,15 @@ "node": ">=6" } }, + "node_modules/standard-navigation": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/standard-navigation/-/standard-navigation-0.0.8.tgz", + "integrity": "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -5455,6 +7842,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5469,6 +7865,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5481,6 +7892,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/structured-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", @@ -5524,6 +7948,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tailwindcss-safe-area": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss-safe-area/-/tailwindcss-safe-area-1.3.0.tgz", + "integrity": "sha512-RoxnW1zAjBWC3XK+row7Qj5toRMRlKNN/p3FLXb6fTGKxDGWT6JP/mcNX1yf09xRficQ308hbwiedgniepSp1Q==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/mvllow" + }, + "peerDependencies": { + "tailwindcss": "^4.0.0" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -5648,6 +8093,509 @@ "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", "license": "MIT" }, + "node_modules/tsx": { + "version": "4.23.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz", + "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/type-fest": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", @@ -5756,6 +8704,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5808,6 +8774,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -5861,6 +8833,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "7.5.13", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", @@ -5882,6 +8872,16 @@ } } }, + "node_modules/x-path": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/x-path/-/x-path-0.0.2.tgz", + "integrity": "sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-extra": "^1.0.2" + } + }, "node_modules/xcode": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", diff --git a/package.json b/package.json index ee10c60..20c2c1f 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,40 @@ "version": "1.0.0", "main": "index.ts", "dependencies": { + "@bacons/apple-targets": "^5.0.0", + "@react-navigation/bottom-tabs": "^7.18.14", + "@react-navigation/native": "^7.3.14", + "@react-navigation/native-stack": "^7.18.6", + "drizzle-orm": "^0.45.2", "expo": "~57.0.10", + "expo-sqlite": "~57.0.1", "expo-status-bar": "~57.0.1", + "expo-system-ui": "~57.0.2", + "nativewind": "^5.0.0-preview.4", "react": "19.2.3", - "react-native": "0.86.2" + "react-native": "0.86.2", + "react-native-css": "^3.0.7", + "react-native-mmkv": "^4.3.2", + "react-native-reanimated": "4.5.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0" }, "devDependencies": { "@types/react": "~19.2.2", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.31.10", + "tailwindcss": "^4", "typescript": "~6.0.3" }, "scripts": { "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web" + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "db:generate": "drizzle-kit generate" }, - "private": true + "private": true, + "overrides": { + "lightningcss": "1.30.1" + } } diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c2ddf74 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..072832c --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,35 @@ +import { Pressable, Text, ActivityIndicator, type PressableProps } from "react-native"; + +type ButtonProps = PressableProps & { + title: string; + variant?: "primary" | "secondary" | "ghost"; + loading?: boolean; +}; + +export function Button({ title, variant = "primary", loading, disabled, ...props }: ButtonProps) { + const base = "rounded-xl px-6 py-3 items-center justify-center"; + const variants = { + primary: "bg-sky-500", + secondary: "bg-slate-100", + ghost: "bg-transparent", + }; + const textVariants = { + primary: "text-white font-semibold", + secondary: "text-slate-700 font-semibold", + ghost: "text-sky-500 font-medium", + }; + + return ( + + {loading ? ( + + ) : ( + {title} + )} + + ); +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 0000000..670f150 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,19 @@ +import { View, type ViewProps } from "react-native"; + +type CardProps = ViewProps & { + variant?: "default" | "highlighted"; +}; + +export function Card({ variant = "default", className = "", children, ...props }: CardProps) { + const base = "rounded-2xl p-4"; + const variants = { + default: "bg-white border border-slate-200", + highlighted: "bg-sky-50 border border-sky-200", + }; + + return ( + + {children} + + ); +} diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..bdd693c --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,29 @@ +import { View, Text, ActivityIndicator } from "react-native"; + +type EmptyStateProps = { + title: string; + message?: string; + icon?: string; +}; + +export function EmptyState({ title, message }: EmptyStateProps) { + return ( + + {title} + {message && {message}} + + ); +} + +type LoadingSpinnerProps = { + message?: string; +}; + +export function LoadingSpinner({ message }: LoadingSpinnerProps) { + return ( + + + {message && {message}} + + ); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..30b5315 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,20 @@ +import { TextInput, View, Text, type TextInputProps } from "react-native"; + +type InputProps = TextInputProps & { + label?: string; + error?: string; +}; + +export function Input({ label, error, className = "", ...props }: InputProps) { + return ( + + {label && {label}} + + {error && {error}} + + ); +} diff --git a/src/components/ScreenContainer.tsx b/src/components/ScreenContainer.tsx new file mode 100644 index 0000000..cd67ac0 --- /dev/null +++ b/src/components/ScreenContainer.tsx @@ -0,0 +1,20 @@ +import { View, type ViewProps } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +type ScreenContainerProps = ViewProps & { + padded?: boolean; +}; + +export function ScreenContainer({ padded = true, className = "", children, ...props }: ScreenContainerProps) { + const insets = useSafeAreaInsets(); + + return ( + + {children} + + ); +} diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..80db190 --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,13 @@ +export const COLORS = { + primary: "#0ea5e9", + primaryDark: "#0284c7", + primaryLight: "#e0f2fe", + background: "#ffffff", + surface: "#f8fafc", + text: "#0f172a", + textSecondary: "#64748b", + border: "#e2e8f0", + success: "#10b981", + warning: "#f59e0b", + error: "#ef4444", +} as const; diff --git a/src/constants/poolLengths.ts b/src/constants/poolLengths.ts new file mode 100644 index 0000000..e955721 --- /dev/null +++ b/src/constants/poolLengths.ts @@ -0,0 +1,3 @@ +export const POOL_LENGTHS = [25, 50] as const; + +export const DEFAULT_POOL_LENGTH = 25; diff --git a/src/constants/strokes.ts b/src/constants/strokes.ts new file mode 100644 index 0000000..c354d9b --- /dev/null +++ b/src/constants/strokes.ts @@ -0,0 +1,15 @@ +export const STROKE_TYPES = [ + "Freestyle", + "Backstroke", + "Breaststroke", + "Butterfly", + "Mixed", +] as const; + +export const STROKE_COLORS: Record = { + Freestyle: "#0ea5e9", + Backstroke: "#8b5cf6", + Breaststroke: "#10b981", + Butterfly: "#f59e0b", + Mixed: "#6b7280", +}; diff --git a/src/db/client.ts b/src/db/client.ts new file mode 100644 index 0000000..94d52d2 --- /dev/null +++ b/src/db/client.ts @@ -0,0 +1,9 @@ +import { openDatabaseSync } from "expo-sqlite"; +import { drizzle } from "drizzle-orm/expo-sqlite"; +import * as schema from "./schema"; + +export const DATABASE_NAME = "swimbuddy.db"; + +const expoDb = openDatabaseSync(DATABASE_NAME, { enableChangeListener: true }); + +export const db = drizzle(expoDb, { schema }); diff --git a/src/db/operations.ts b/src/db/operations.ts new file mode 100644 index 0000000..5b97b7a --- /dev/null +++ b/src/db/operations.ts @@ -0,0 +1,177 @@ +import { db } from "./client"; +import { workouts, sets, laps, templates } from "./schema"; +import { eq, desc, sql } from "drizzle-orm"; +import type { WorkoutSet, LapData } from "@types/workout"; + +// ─── Templates ─────────────────────────────────────────────── + +export async function saveTemplate(name: string, poolLength: number, setsData: WorkoutSet[]) { + const id = crypto.randomUUID(); + await db.insert(templates).values({ + id, + name, + poolLength, + jsonData: JSON.stringify(setsData), + createdAt: new Date().toISOString(), + }); + return id; +} + +export async function getTemplates() { + return db.select().from(templates).orderBy(desc(templates.createdAt)); +} + +export async function deleteTemplate(id: string) { + await db.delete(templates).where(eq(templates.id, id)); +} + +export async function toggleTemplateFavorite(id: string, isFavorite: boolean) { + await db.update(templates).set({ isFavorite }).where(eq(templates.id, id)); +} + +// ─── Workouts ──────────────────────────────────────────────── + +export async function saveWorkout( + name: string | null, + poolLength: number, + setsData: WorkoutSet[], + templateId?: string +) { + const workoutId = crypto.randomUUID(); + const totalDistance = setsData.reduce((acc, s) => acc + s.distancePerRep * s.repCount, 0); + + await db.insert(workouts).values({ + id: workoutId, + name, + templateId: templateId ?? null, + poolLength, + startedAt: new Date().toISOString(), + totalDistance, + source: "manual", + }); + + for (let i = 0; i < setsData.length; i++) { + const s = setsData[i]; + await db.insert(sets).values({ + id: crypto.randomUUID(), + workoutId, + position: i + 1, + stroke: s.stroke, + distancePerRep: s.distancePerRep, + repCount: s.repCount, + restType: s.restType, + restDuration: s.restDuration, + targetTime: s.targetTime ?? null, + }); + } + + return workoutId; +} + +export async function finishWorkout( + workoutId: string, + duration: number, + calories: number, + avgHeartRate: number +) { + await db + .update(workouts) + .set({ + finishedAt: new Date().toISOString(), + totalDuration: duration, + calories, + avgHeartRate, + }) + .where(eq(workouts.id, workoutId)); +} + +export async function getWorkoutWithSets(workoutId: string) { + const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get(); + if (!workout) return null; + + const workoutSets = await db.select().from(sets).where(eq(sets.workoutId, workoutId)); + const workoutLaps = await db.select().from(laps).where(eq(laps.workoutId, workoutId)); + + return { ...workout, sets: workoutSets, laps: workoutLaps }; +} + +export async function getAllWorkouts() { + return db.select().from(workouts).orderBy(desc(workouts.startedAt)); +} + +export async function deleteWorkout(id: string) { + await db.delete(laps).where(eq(laps.workoutId, id)); + await db.delete(sets).where(eq(sets.workoutId, id)); + await db.delete(workouts).where(eq(workouts.id, id)); +} + +// ─── Laps ──────────────────────────────────────────────────── + +export async function saveLap(workoutId: string, lap: LapData) { + const swolf = + lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null; + + await db.insert(laps).values({ + id: crypto.randomUUID(), + workoutId, + setPosition: lap.setPosition, + repNumber: lap.repNumber, + distance: lap.distance, + duration: lap.duration, + strokeCount: lap.strokeCount ?? null, + heartRate: lap.heartRate ?? null, + swolf, + }); +} + +export async function saveLaps(workoutId: string, lapsData: LapData[]) { + for (const lap of lapsData) { + await saveLap(workoutId, lap); + } +} + +export async function getWorkoutLaps(workoutId: string) { + return db.select().from(laps).where(eq(laps.workoutId, workoutId)); +} + +// ─── Stats ─────────────────────────────────────────────────── + +export async function getWeeklyStats() { + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + + return db + .select({ + count: sql`count(*)`, + totalDistance: sql`coalesce(sum(${workouts.totalDistance}), 0)`, + totalDuration: sql`coalesce(sum(${workouts.totalDuration}), 0)`, + avgCalories: sql`coalesce(avg(${workouts.calories}), 0)`, + }) + .from(workouts) + .where(sql`${workouts.startedAt} >= ${weekAgo.toISOString()}`) + .get(); +} + +export async function getPersonalBests() { + return db + .select({ + distance: laps.distance, + bestTime: sql`min(${laps.duration})`, + bestSwolf: sql`min(${laps.swolf})`, + }) + .from(laps) + .groupBy(laps.distance) + .having(sql`${laps.distance} > 0`); +} + +export async function getTotalStats() { + return db + .select({ + totalWorkouts: sql`count(*)`, + totalDistance: sql`coalesce(sum(${workouts.totalDistance}), 0)`, + totalDuration: sql`coalesce(sum(${workouts.totalDuration}), 0)`, + }) + .from(workouts) + .where(sql`${workouts.finishedAt} IS NOT NULL`) + .get(); +} diff --git a/src/db/relations.ts b/src/db/relations.ts new file mode 100644 index 0000000..b193f66 --- /dev/null +++ b/src/db/relations.ts @@ -0,0 +1,29 @@ +import { relations } from "drizzle-orm"; +import { workouts, sets, laps, templates } from "./schema"; + +export const templatesRelations = relations(templates, ({ many }) => ({ + workouts: many(workouts), +})); + +export const workoutsRelations = relations(workouts, ({ one, many }) => ({ + template: one(templates, { + fields: [workouts.templateId], + references: [templates.id], + }), + sets: many(sets), + laps: many(laps), +})); + +export const setsRelations = relations(sets, ({ one }) => ({ + workout: one(workouts, { + fields: [sets.workoutId], + references: [workouts.id], + }), +})); + +export const lapsRelations = relations(laps, ({ one }) => ({ + workout: one(workouts, { + fields: [laps.workoutId], + references: [workouts.id], + }), +})); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..7ec8704 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,53 @@ +import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; + +export const templates = sqliteTable("templates", { + id: text("id").primaryKey(), + name: text("name").notNull(), + description: text("description"), + poolLength: real("pool_length").notNull().default(25), + jsonData: text("json_data").notNull(), + isFavorite: integer("is_favorite", { mode: "boolean" }).notNull().default(false), + createdAt: text("created_at").notNull(), +}); + +export const workouts = sqliteTable("workouts", { + id: text("id").primaryKey(), + name: text("name"), + templateId: text("template_id"), + poolLength: real("pool_length").notNull().default(25), + startedAt: text("started_at").notNull(), + finishedAt: text("finished_at"), + totalDistance: integer("total_distance"), + totalDuration: real("total_duration"), + calories: real("calories"), + avgHeartRate: integer("avg_heart_rate"), + source: text("source").notNull().default("manual"), +}); + +export const sets = sqliteTable("sets", { + id: text("id").primaryKey(), + workoutId: text("workout_id") + .notNull() + .references(() => workouts.id), + position: integer("position").notNull(), + stroke: text("stroke").notNull(), + distancePerRep: integer("distance_per_rep").notNull(), + repCount: integer("rep_count").notNull(), + restType: text("rest_type").notNull().default("fixed_rest"), + restDuration: real("rest_duration").notNull().default(0), + targetTime: real("target_time"), +}); + +export const laps = sqliteTable("laps", { + id: text("id").primaryKey(), + workoutId: text("workout_id") + .notNull() + .references(() => workouts.id), + setPosition: integer("set_position").notNull(), + repNumber: integer("rep_number").notNull(), + distance: integer("distance").notNull(), + duration: real("duration").notNull(), + strokeCount: integer("stroke_count"), + heartRate: integer("heart_rate"), + swolf: real("swolf"), +}); diff --git a/src/features/active-workout/screens/PostSwimScreen.tsx b/src/features/active-workout/screens/PostSwimScreen.tsx new file mode 100644 index 0000000..0d8ad37 --- /dev/null +++ b/src/features/active-workout/screens/PostSwimScreen.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from "react"; +import { View, Text, Pressable } from "react-native"; +import { useNavigation, useRoute } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "@navigation/types"; +import type { RouteProp } from "@react-navigation/native"; +import { getWorkoutWithSets, getWorkoutLaps } from "@db/operations"; +import { formatTime } from "@utils/formatTime"; +import { formatDistance } from "@utils/formatDistance"; + +export function PostSwimScreen() { + const navigation = useNavigation>(); + const route = useRoute>(); + const [summary, setSummary] = useState<{ + distance: number; + duration: number; + laps: number; + avgSwolf: number | null; + } | null>(null); + + useEffect(() => { + loadSummary(); + }, []); + + const loadSummary = async () => { + const workout = await getWorkoutWithSets(route.params.workoutId); + const laps = await getWorkoutLaps(route.params.workoutId); + + if (!workout) return; + + const totalDuration = workout.laps?.reduce((acc: number, l: any) => acc + l.duration, 0) ?? 0; + const avgSwolf = + laps.length > 0 + ? Math.round(laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / laps.length) + : null; + + setSummary({ + distance: workout.totalDistance ?? 0, + duration: totalDuration, + laps: laps.length, + avgSwolf, + }); + }; + + return ( + + + 🏊 + Swim Complete! + Great job getting in the water + + + {summary ? ( + + + Distance + + {formatDistance(summary.distance)} + + + + Duration + + {formatTime(summary.duration)} + + + + Laps + {summary.laps} + + {summary.avgSwolf && ( + + Avg SWOLF + {summary.avgSwolf} + + )} + + ) : ( + + Loading summary... + + )} + + navigation.popToTop()} + className="bg-slate-900 rounded-2xl py-4 items-center" + > + Done + + + ); +} diff --git a/src/features/active-workout/screens/PreSwimScreen.tsx b/src/features/active-workout/screens/PreSwimScreen.tsx new file mode 100644 index 0000000..fbd4059 --- /dev/null +++ b/src/features/active-workout/screens/PreSwimScreen.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from "react"; +import { View, Text, Pressable, ActivityIndicator } from "react-native"; +import { useNavigation, useRoute } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "@navigation/types"; +import type { RouteProp } from "@react-navigation/native"; +import { getWorkoutWithSets } from "@db/operations"; +import { useSyncWorkout } from "../hooks/useSyncWorkout"; +import { useWatchConnection } from "../hooks/useWatchConnection"; +import { formatDistance } from "@utils/formatDistance"; +import { STROKE_COLORS } from "@constants/strokes"; +import type { WorkoutSet } from "@types/workout"; + +type WorkoutWithSets = { + id: string; + name: string | null; + poolLength: number; + totalDistance: number | null; + sets: WorkoutSet[]; +}; + +export function PreSwimScreen() { + const navigation = useNavigation>(); + const route = useRoute>(); + const [workout, setWorkout] = useState(null); + const { isSyncing, sendWorkoutToWatch } = useSyncWorkout(); + const { status, isPaired, isInstalled } = useWatchConnection(); + + useEffect(() => { + loadWorkout(); + }, []); + + const loadWorkout = async () => { + const data = await getWorkoutWithSets(route.params.workoutId); + setWorkout(data as WorkoutWithSets | null); + }; + + const handleStartSwim = async () => { + if (!workout) return; + + if (status !== "connected") { + navigation.navigate("PostSwim", { workoutId: route.params.workoutId }); + return; + } + + const result = await sendWorkoutToWatch( + workout.id, + workout.name, + workout.poolLength, + workout.sets + ); + + if ("error" in result) { + navigation.navigate("PostSwim", { workoutId: route.params.workoutId }); + return; + } + + navigation.navigate("PostSwim", { workoutId: route.params.workoutId }); + }; + + if (!workout) { + return ( + + + + ); + } + + return ( + + + + {workout.name || "Workout"} + + + {formatDistance(workout.totalDistance ?? 0)} · {workout.poolLength}m pool + + + + + + Sets + + {workout.sets.map((set, i) => ( + + + + {set.stroke} + + + {set.repCount}×{set.distancePerRep}m + + + ))} + + + + + Rest + + {workout.sets[0]?.restDuration ?? 0}s between sets + + + + + + + + {status === "connected" + ? "Apple Watch connected" + : isPaired + ? "Watch paired but not connected" + : "No Watch paired"} + + + + + {isSyncing ? ( + + ) : ( + + {status === "connected" ? "Send to Watch & Start" : "Start Without Watch"} + + )} + + + + {status === "connected" + ? "Workout will be sent to your Apple Watch" + : "Workout will be tracked on this device"} + + + ); +} diff --git a/src/features/history/screens/HistoryScreen.tsx b/src/features/history/screens/HistoryScreen.tsx new file mode 100644 index 0000000..b681dce --- /dev/null +++ b/src/features/history/screens/HistoryScreen.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { View, Text, Pressable, FlatList } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { HistoryStackParamList } from "@navigation/types"; +import { getAllWorkouts } from "@db/operations"; +import { formatTime } from "@utils/formatTime"; +import { formatDistance } from "@utils/formatDistance"; + +type WorkoutRow = { + id: string; + name: string | null; + totalDistance: number | null; + totalDuration: number | null; + calories: number | null; + startedAt: string; + finishedAt: string | null; +}; + +export function HistoryScreen() { + const navigation = useNavigation>(); + const [workouts, setWorkouts] = useState([]); + + useEffect(() => { + loadWorkouts(); + }, []); + + const loadWorkouts = async () => { + const data = await getAllWorkouts(); + setWorkouts((data as WorkoutRow[]).filter((w) => w.finishedAt)); + }; + + const grouped = workouts.reduce>((acc, w) => { + const date = new Date(w.startedAt).toLocaleDateString(); + if (!acc[date]) acc[date] = []; + acc[date].push(w); + return acc; + }, {}); + + const sections = Object.entries(grouped); + + return ( + + + navigation.navigate("Stats")} + className="bg-slate-100 rounded-2xl p-3 items-center mb-4" + > + View Statistics + + + + date} + contentContainerClassName="px-4 pb-8" + ListEmptyComponent={ + + No completed workouts yet + + } + renderItem={({ item: [date, rows] }) => ( + + + {date} + + {rows.map((w) => ( + navigation.navigate("SessionDetail", { sessionId: w.id })} + className="bg-slate-50 rounded-2xl p-4 mb-2 border border-slate-200" + > + + + + {w.name || "Swim Session"} + + + {formatDistance(w.totalDistance ?? 0)} + {w.totalDuration ? ` · ${formatTime(w.totalDuration)}` : ""} + {w.calories ? ` · ${Math.round(w.calories)} kcal` : ""} + + + + {new Date(w.startedAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + + ))} + + )} + /> + + ); +} diff --git a/src/features/history/screens/SessionDetailScreen.tsx b/src/features/history/screens/SessionDetailScreen.tsx new file mode 100644 index 0000000..e798ac1 --- /dev/null +++ b/src/features/history/screens/SessionDetailScreen.tsx @@ -0,0 +1,147 @@ +import { useEffect, useState } from "react"; +import { View, Text, ScrollView } from "react-native"; +import type { HistoryStackProps } from "@navigation/types"; +import { getWorkoutWithSets } from "@db/operations"; +import { formatTime } from "@utils/formatTime"; +import { formatDistance } from "@utils/formatDistance"; +import { STROKE_COLORS } from "@constants/strokes"; + +type WorkoutDetail = { + id: string; + name: string | null; + poolLength: number; + totalDistance: number | null; + totalDuration: number | null; + calories: number | null; + avgHeartRate: number | null; + startedAt: string; + finishedAt: string | null; + sets: { + stroke: string; + distancePerRep: number; + repCount: number; + position: number; + }[]; + laps: { + setPosition: number; + repNumber: number; + distance: number; + duration: number; + strokeCount: number | null; + heartRate: number | null; + swolf: number | null; + }[]; +}; + +export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) { + const [workout, setWorkout] = useState(null); + + useEffect(() => { + loadWorkout(); + }, []); + + const loadWorkout = async () => { + const data = await getWorkoutWithSets(route.params.sessionId); + setWorkout(data as WorkoutDetail | null); + }; + + if (!workout) { + return ( + + Loading... + + ); + } + + const avgSwolf = + workout.laps.length > 0 + ? Math.round( + workout.laps.reduce((acc, l) => acc + (l.swolf ?? 0), 0) / workout.laps.length + ) + : null; + + return ( + + + + {workout.name || "Swim Session"} + + + {new Date(workout.startedAt).toLocaleDateString()} + + + + + + + {formatDistance(workout.totalDistance ?? 0)} + + Distance + + + + {formatTime(workout.totalDuration ?? 0)} + + Duration + + {workout.calories && ( + + + {Math.round(workout.calories)} + + kcal + + )} + + + {avgSwolf && ( + + Avg SWOLF + {avgSwolf} + + )} + + + Sets + + {workout.sets.map((set, i) => ( + + + + {set.stroke} + + + {set.repCount}×{set.distancePerRep}m + + + ))} + + {workout.laps.length > 0 && ( + <> + + Laps + + {workout.laps.map((lap, i) => ( + + + Lap {i + 1} · {lap.distance}m + + + {formatTime(lap.duration)} + + + ))} + + )} + + ); +} diff --git a/src/features/history/screens/StatsScreen.tsx b/src/features/history/screens/StatsScreen.tsx new file mode 100644 index 0000000..545457c --- /dev/null +++ b/src/features/history/screens/StatsScreen.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState } from "react"; +import { View, Text, ScrollView } from "react-native"; +import { getWeeklyStats, getTotalStats, getPersonalBests } from "@db/operations"; +import { formatDistance } from "@utils/formatDistance"; +import { formatTime } from "@utils/formatTime"; + +type WeeklyStats = { + count: number; + totalDistance: number; + totalDuration: number; + avgCalories: number; +}; + +type TotalStats = { + totalWorkouts: number; + totalDistance: number; + totalDuration: number; +}; + +type PersonalBest = { + distance: number; + bestTime: number; + bestSwolf: number | null; +}; + +export function StatsScreen() { + const [weekly, setWeekly] = useState(null); + const [total, setTotal] = useState(null); + const [bests, setBests] = useState([]); + + useEffect(() => { + loadStats(); + }, []); + + const loadStats = async () => { + const [w, t, b] = await Promise.all([ + getWeeklyStats(), + getTotalStats(), + getPersonalBests(), + ]); + setWeekly(w as WeeklyStats); + setTotal(t as TotalStats); + setBests(b as PersonalBest[]); + }; + + return ( + + Statistics + + + This Week + + + {weekly ? ( + + + {weekly.count} + Workouts + + + + {formatDistance(weekly.totalDistance)} + + Distance + + + + {formatTime(weekly.totalDuration)} + + Duration + + + ) : ( + No data yet + )} + + + + All Time + + + {total ? ( + + + {total.totalWorkouts} + Workouts + + + + {formatDistance(total.totalDistance)} + + Distance + + + + {formatTime(total.totalDuration)} + + Duration + + + ) : ( + No data yet + )} + + + {bests.length > 0 && ( + <> + + Personal Bests + + {bests.map((best) => ( + + {best.distance}m + + {formatTime(best.bestTime)} + {best.bestSwolf && ( + SWOLF: {best.bestSwolf} + )} + + + ))} + + )} + + ); +} diff --git a/src/features/settings/screens/SettingsScreen.tsx b/src/features/settings/screens/SettingsScreen.tsx new file mode 100644 index 0000000..89df3cf --- /dev/null +++ b/src/features/settings/screens/SettingsScreen.tsx @@ -0,0 +1,39 @@ +import { View, Text, Pressable } from "react-native"; +import { useSettings } from "../../providers/WatchProvider"; + +export function SettingsScreen() { + const { poolLength, setPoolLength } = useSettings(); + + return ( + + Settings + + + Default Pool Length + + {[25, 50].map((len) => ( + setPoolLength(len)} + className={`flex-1 py-3 rounded-xl items-center ${ + poolLength === len ? "bg-sky-500" : "bg-slate-100" + }`} + > + + {len}m + + + ))} + + + + + SwimBuddy v1.0.0 + + + ); +} diff --git a/src/features/templates/hooks/useTemplates.ts b/src/features/templates/hooks/useTemplates.ts new file mode 100644 index 0000000..28fe327 --- /dev/null +++ b/src/features/templates/hooks/useTemplates.ts @@ -0,0 +1,60 @@ +import { useEffect, useState, useCallback } from "react"; +import { getTemplates, deleteTemplate, toggleTemplateFavorite } from "@db/operations"; +import type { WorkoutSet } from "@types/workout"; + +type Template = { + id: string; + name: string; + description: string | null; + poolLength: number; + jsonData: string; + isFavorite: boolean; + createdAt: string; +}; + +export function useTemplates() { + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const load = useCallback(async () => { + setIsLoading(true); + try { + const data = await getTemplates(); + setTemplates(data as Template[]); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + load(); + }, [load]); + + const remove = useCallback( + async (id: string) => { + await deleteTemplate(id); + setTemplates((prev) => prev.filter((t) => t.id !== id)); + }, + [] + ); + + const toggleFavorite = useCallback( + async (id: string, current: boolean) => { + await toggleTemplateFavorite(id, !current); + setTemplates((prev) => + prev.map((t) => (t.id === id ? { ...t, isFavorite: !current } : t)) + ); + }, + [] + ); + + const parseSets = (template: Template): WorkoutSet[] => { + try { + return JSON.parse(template.jsonData); + } catch { + return []; + } + }; + + return { templates, isLoading, remove, toggleFavorite, parseSets, reload: load }; +} diff --git a/src/features/templates/screens/TemplatesScreen.tsx b/src/features/templates/screens/TemplatesScreen.tsx new file mode 100644 index 0000000..a8ebdf9 --- /dev/null +++ b/src/features/templates/screens/TemplatesScreen.tsx @@ -0,0 +1,64 @@ +import { View, Text, Pressable, FlatList, Alert } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { WorkoutStackParamList } from "@navigation/types"; +import { useTemplates } from "../hooks/useTemplates"; + +export function TemplatesScreen() { + const navigation = useNavigation>(); + const { templates, remove, toggleFavorite } = useTemplates(); + + const handleDelete = (id: string, name: string) => { + Alert.alert("Delete Template", `Delete "${name}"?`, [ + { text: "Cancel", style: "cancel" }, + { text: "Delete", style: "destructive", onPress: () => remove(id) }, + ]); + }; + + return ( + + item.id} + contentContainerClassName="p-4" + ListEmptyComponent={ + + No templates yet + + Save a workout as a template to reuse it + + + } + renderItem={({ item }) => ( + navigation.navigate("Builder", { templateId: item.id })} + className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200" + > + + + {item.name} + + {item.poolLength}m pool + + + + toggleFavorite(item.id, item.isFavorite)} + className="p-1" + > + {item.isFavorite ? "★" : "☆"} + + handleDelete(item.id, item.name)} + className="p-1" + > + × + + + + + )} + /> + + ); +} diff --git a/src/features/watch-connectivity/hooks/useSyncWorkout.ts b/src/features/watch-connectivity/hooks/useSyncWorkout.ts new file mode 100644 index 0000000..09c478f --- /dev/null +++ b/src/features/watch-connectivity/hooks/useSyncWorkout.ts @@ -0,0 +1,42 @@ +import { useState } from "react"; +import WatchConnectivity from "../../modules/watch-connectivity"; +import type { WorkoutSet } from "@types/workout"; + +type SyncResult = { status: string } | { error: string }; + +export function useSyncWorkout() { + const [isSyncing, setIsSyncing] = useState(false); + + const sendWorkoutToWatch = async ( + workoutId: string, + name: string | null, + poolLength: number, + sets: WorkoutSet[] + ): Promise => { + setIsSyncing(true); + try { + const plan = { + id: workoutId, + name: name ?? "Workout", + poolLength, + sets: sets.map((s) => ({ + stroke: s.stroke, + distancePerRep: s.distancePerRep, + repCount: s.repCount, + restType: s.restType, + restDuration: s.restDuration, + targetTime: s.targetTime ?? null, + })), + }; + + const result = await WatchConnectivity.sendWorkoutPlan(plan); + return result as SyncResult; + } catch (error: any) { + return { error: error.message ?? "Failed to sync" }; + } finally { + setIsSyncing(false); + } + }; + + return { isSyncing, sendWorkoutToWatch }; +} diff --git a/src/features/watch-connectivity/hooks/useWatchConnection.ts b/src/features/watch-connectivity/hooks/useWatchConnection.ts new file mode 100644 index 0000000..0b63095 --- /dev/null +++ b/src/features/watch-connectivity/hooks/useWatchConnection.ts @@ -0,0 +1,38 @@ +import { useState, useEffect } from "react"; +import WatchConnectivity from "../../modules/watch-connectivity"; + +type WatchConnectionStatus = "connected" | "disconnected" | "pending"; + +export function useWatchConnection() { + const [status, setStatus] = useState("pending"); + const [isPaired, setIsPaired] = useState(false); + const [isInstalled, setIsInstalled] = useState(false); + + useEffect(() => { + checkStatus(); + const interval = setInterval(checkStatus, 5000); + return () => clearInterval(interval); + }, []); + + const checkStatus = async () => { + try { + const supported = await WatchConnectivity.isSupported(); + if (!supported) { + setStatus("disconnected"); + return; + } + + const paired = await WatchConnectivity.isPaired(); + const installed = await WatchConnectivity.isWatchAppInstalled(); + const reachable = await WatchConnectivity.isReachable(); + + setIsPaired(paired); + setIsInstalled(installed); + setStatus(reachable ? "connected" : "disconnected"); + } catch { + setStatus("disconnected"); + } + }; + + return { status, isPaired, isInstalled }; +} diff --git a/src/features/watch-connectivity/hooks/useWatchReachability.ts b/src/features/watch-connectivity/hooks/useWatchReachability.ts new file mode 100644 index 0000000..9a74e81 --- /dev/null +++ b/src/features/watch-connectivity/hooks/useWatchReachability.ts @@ -0,0 +1,26 @@ +import { useState, useEffect } from "react"; +import WatchConnectivity from "../../modules/watch-connectivity"; + +export function useWatchReachability() { + const [isReachable, setIsReachable] = useState(false); + + useEffect(() => { + checkReachability(); + const interval = setInterval(checkReachability, 5000); + return () => clearInterval(interval); + }, []); + + const checkReachability = async () => { + try { + const supported = await WatchConnectivity.isSupported(); + if (supported) { + const reachable = await WatchConnectivity.isReachable(); + setIsReachable(reachable); + } + } catch { + setIsReachable(false); + } + }; + + return isReachable; +} diff --git a/src/features/workout-builder/hooks/useWorkoutBuilder.ts b/src/features/workout-builder/hooks/useWorkoutBuilder.ts new file mode 100644 index 0000000..0427ac4 --- /dev/null +++ b/src/features/workout-builder/hooks/useWorkoutBuilder.ts @@ -0,0 +1,83 @@ +import { useState, useCallback } from "react"; +import type { WorkoutSet, StrokeType, RestType } from "@types/workout"; +import { saveWorkout, saveTemplate } from "@db/operations"; + +const DEFAULT_SET: WorkoutSet = { + stroke: "Freestyle", + distancePerRep: 200, + repCount: 1, + restType: "fixed_rest", + restDuration: 30, +}; + +export function useWorkoutBuilder(initialSets?: WorkoutSet[]) { + const [sets, setSets] = useState(initialSets ?? [{ ...DEFAULT_SET }]); + const [workoutName, setWorkoutName] = useState(""); + const [isSaving, setIsSaving] = useState(false); + + const addSet = useCallback(() => { + setSets((prev) => [...prev, { ...DEFAULT_SET }]); + }, []); + + const updateSet = useCallback((index: number, updates: Partial) => { + setSets((prev) => prev.map((s, i) => (i === index ? { ...s, ...updates } : s))); + }, []); + + const removeSet = useCallback((index: number) => { + setSets((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const totalDistance = sets.reduce((acc, s) => acc + s.distancePerRep * s.repCount, 0); + + const save = useCallback( + async (poolLength: number, templateId?: string) => { + setIsSaving(true); + try { + const name = workoutName.trim() || `Workout ${new Date().toLocaleDateString()}`; + const id = await saveWorkout(name, poolLength, sets, templateId); + return id; + } finally { + setIsSaving(false); + } + }, + [workoutName, sets] + ); + + const saveAsTemplate = useCallback( + async (templateName: string, poolLength: number) => { + setIsSaving(true); + try { + const id = await saveTemplate(templateName, poolLength, sets); + return id; + } finally { + setIsSaving(false); + } + }, + [sets] + ); + + const loadTemplate = useCallback((templateSets: WorkoutSet[], name?: string) => { + setSets(templateSets); + if (name) setWorkoutName(name); + }, []); + + const reset = useCallback(() => { + setSets([{ ...DEFAULT_SET }]); + setWorkoutName(""); + }, []); + + return { + sets, + workoutName, + setWorkoutName, + totalDistance, + isSaving, + addSet, + updateSet, + removeSet, + save, + saveAsTemplate, + loadTemplate, + reset, + }; +} diff --git a/src/features/workout-builder/screens/BuilderScreen.tsx b/src/features/workout-builder/screens/BuilderScreen.tsx new file mode 100644 index 0000000..831c25d --- /dev/null +++ b/src/features/workout-builder/screens/BuilderScreen.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { View, Text, TextInput, Pressable, ScrollView, Alert } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "@navigation/types"; +import { useWorkoutBuilder } from "../hooks/useWorkoutBuilder"; +import { useSettings } from "../../../providers/WatchProvider"; +import type { StrokeType } from "@types/workout"; + +const STROKES: StrokeType[] = ["Freestyle", "Backstroke", "Breaststroke", "Butterfly", "Mixed"]; + +export function BuilderScreen() { + const navigation = useNavigation>(); + const { poolLength } = useSettings(); + const { + sets, + workoutName, + setWorkoutName, + totalDistance, + isSaving, + addSet, + updateSet, + removeSet, + save, + } = useWorkoutBuilder(); + + const handleSave = async () => { + try { + const workoutId = await save(poolLength); + navigation.navigate("PreSwim", { workoutId }); + } catch (e) { + Alert.alert("Error", "Failed to save workout"); + } + }; + + return ( + + + Build Workout + + {totalDistance}m total · {poolLength}m pool + + + + Workout Name + + + + {sets.map((set, index) => ( + + + Set {index + 1} + {sets.length > 1 && ( + removeSet(index)}> + Remove + + )} + + + + Stroke + + + {STROKES.map((stroke) => ( + updateSet(index, { stroke })} + className={`mr-2 px-3 py-1.5 rounded-full ${ + set.stroke === stroke ? "bg-sky-500" : "bg-slate-200" + }`} + > + + {stroke} + + + ))} + + + + + Distance (m) + updateSet(index, { distancePerRep: Number(t) || 0 })} + className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 text-center" + /> + + + Reps + updateSet(index, { repCount: Number(t) || 1 })} + className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 text-center" + /> + + + Rest (s) + updateSet(index, { restDuration: Number(t) || 0 })} + className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 text-center" + /> + + + + ))} + + + + Add Set + + + + + {isSaving ? "Saving..." : "Start Swim"} + + + + + ); +} diff --git a/src/features/workout-builder/screens/WorkoutListScreen.tsx b/src/features/workout-builder/screens/WorkoutListScreen.tsx new file mode 100644 index 0000000..c27d872 --- /dev/null +++ b/src/features/workout-builder/screens/WorkoutListScreen.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { View, Text, Pressable, FlatList } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { WorkoutStackParamList } from "@navigation/types"; +import { getAllWorkouts, deleteWorkout } from "@db/operations"; +import { formatTime } from "@utils/formatTime"; +import { formatDistance } from "@utils/formatDistance"; + +type WorkoutRow = { + id: string; + name: string | null; + totalDistance: number | null; + totalDuration: number | null; + startedAt: string; + finishedAt: string | null; +}; + +export function WorkoutListScreen() { + const navigation = useNavigation>(); + const [workouts, setWorkouts] = useState([]); + + useEffect(() => { + loadWorkouts(); + }, []); + + const loadWorkouts = async () => { + const data = await getAllWorkouts(); + setWorkouts(data as WorkoutRow[]); + }; + + return ( + + + navigation.navigate("Builder")} + className="bg-sky-500 rounded-2xl p-4 items-center mb-3" + > + + New Workout + + navigation.navigate("Templates")} + className="bg-slate-100 rounded-2xl p-3 items-center mb-4" + > + Browse Templates + + + + item.id} + contentContainerClassName="px-4 pb-8" + ListEmptyComponent={ + + No workouts yet + Tap "New Workout" to get started + + } + renderItem={({ item }) => ( + navigation.navigate("PreSwim", { workoutId: item.id })} + className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200" + > + + + + {item.name || "Untitled Workout"} + + + {formatDistance(item.totalDistance ?? 0)} + {item.totalDuration ? ` · ${formatTime(item.totalDuration)}` : ""} + + + {new Date(item.startedAt).toLocaleDateString()} + + + {item.finishedAt && ( + + Done + + )} + + + )} + /> + + ); +} diff --git a/src/modules/watch-connectivity/index.swift b/src/modules/watch-connectivity/index.swift new file mode 100644 index 0000000..020f790 --- /dev/null +++ b/src/modules/watch-connectivity/index.swift @@ -0,0 +1,63 @@ +import ExpoModulesCore + +public class WatchConnectivityModule: Module { + public func definition() -> Definition { + Name("WatchConnectivity") + + AsyncFunction("isSupported") { () -> Bool in + return WCSession.isSupported() + } + + AsyncFunction("isPaired") { () -> Bool in + return WCSession.default.isPaired + } + + AsyncFunction("isWatchAppInstalled") { () -> Bool in + return WCSession.default.isWatchAppInstalled + } + + AsyncFunction("isReachable") { () -> Bool in + return WCSession.default.isReachable + } + + AsyncFunction("sendWorkoutPlan") { (plan: [String: Any], promise: Promise) in + var message = plan + message["type"] = "workoutPlan" + + if WCSession.default.isReachable { + WCSession.default.sendMessage(message, replyHandler: { reply in + promise.resolve(reply) + }, errorHandler: { error in + promise.reject(error) + }) + } else { + WCSession.default.transferUserInfo(message) + promise.resolve(["status": "queued"]) + } + } + + AsyncFunction("sendWorkoutCompletion") { (data: [String: Any], promise: Promise) in + var message = data + message["type"] = "workoutCompleted" + + if WCSession.default.isReachable { + WCSession.default.sendMessage(message, replyHandler: { reply in + promise.resolve(reply) + }, errorHandler: { error in + promise.reject(error) + }) + } else { + WCSession.default.transferUserInfo(message) + promise.resolve(["status": "queued"]) + } + } + + AsyncFunction("getApplicationContext") { () -> [String: Any]? in + return WCSession.default.receivedApplicationContext as? [String: Any] + } + + AsyncFunction("updateApplicationContext") { (context: [String: Any]) in + try? WCSession.default.updateApplicationContext(context) + } + } +} diff --git a/src/modules/watch-connectivity/index.ts b/src/modules/watch-connectivity/index.ts new file mode 100644 index 0000000..6384a88 --- /dev/null +++ b/src/modules/watch-connectivity/index.ts @@ -0,0 +1,3 @@ +import { requireNativeModule } from "expo-modules-core"; + +export default requireNativeModule("WatchConnectivity"); diff --git a/src/navigation/HistoryNavigator.tsx b/src/navigation/HistoryNavigator.tsx new file mode 100644 index 0000000..57d2ab0 --- /dev/null +++ b/src/navigation/HistoryNavigator.tsx @@ -0,0 +1,35 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { HistoryStackParamList } from "./types"; +import { HistoryScreen } from "../features/history/screens/HistoryScreen"; +import { SessionDetailScreen } from "../features/history/screens/SessionDetailScreen"; +import { StatsScreen } from "../features/history/screens/StatsScreen"; + +const Stack = createNativeStackNavigator(); + +export function HistoryNavigator() { + return ( + + + + + + ); +} diff --git a/src/navigation/MainTabNavigator.tsx b/src/navigation/MainTabNavigator.tsx new file mode 100644 index 0000000..7c52ec3 --- /dev/null +++ b/src/navigation/MainTabNavigator.tsx @@ -0,0 +1,52 @@ +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; +import { MainTabParamList } from "./types"; +import { WorkoutNavigator } from "./WorkoutNavigator"; +import { HistoryNavigator } from "./HistoryNavigator"; +import { SettingsScreen } from "../features/settings/screens/SettingsScreen"; + +const Tab = createBottomTabNavigator(); + +export function MainTabNavigator() { + return ( + + { + // Placeholder — replace with Expo vector icons later + return null; + }, + }} + /> + { + return null; + }, + }} + /> + { + return null; + }, + }} + /> + + ); +} diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx new file mode 100644 index 0000000..c647364 --- /dev/null +++ b/src/navigation/RootNavigator.tsx @@ -0,0 +1,53 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { MainTabNavigator } from "./MainTabNavigator"; +import { RootStackParamList } from "./types"; +import { BuilderScreen } from "../features/workout-builder/screens/BuilderScreen"; +import { PreSwimScreen } from "../features/active-workout/screens/PreSwimScreen"; +import { PostSwimScreen } from "../features/active-workout/screens/PostSwimScreen"; +import { SessionDetailScreen } from "../features/history/screens/SessionDetailScreen"; +import { StatsScreen } from "../features/history/screens/StatsScreen"; + +const Stack = createNativeStackNavigator(); + +export function RootNavigator() { + return ( + + + + + null }} + /> + + + + ); +} diff --git a/src/navigation/WorkoutNavigator.tsx b/src/navigation/WorkoutNavigator.tsx new file mode 100644 index 0000000..fef766d --- /dev/null +++ b/src/navigation/WorkoutNavigator.tsx @@ -0,0 +1,41 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { WorkoutStackParamList } from "./types"; +import { WorkoutListScreen } from "../features/workout-builder/screens/WorkoutListScreen"; +import { BuilderScreen } from "../features/workout-builder/screens/BuilderScreen"; +import { TemplatesScreen } from "../features/templates/screens/TemplatesScreen"; +import { PreSwimScreen } from "../features/active-workout/screens/PreSwimScreen"; + +const Stack = createNativeStackNavigator(); + +export function WorkoutNavigator() { + return ( + + + + + + + ); +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 0000000..f3fc035 --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,39 @@ +import { NativeStackScreenProps } from "@react-navigation/native-stack"; + +export type RootStackParamList = { + Main: undefined; + WorkoutBuilder: { templateId?: string } | undefined; + PreSwim: { workoutId: string }; + PostSwim: { workoutId: string }; + SessionDetail: { sessionId: string }; + Stats: undefined; +}; + +export type MainTabParamList = { + Dashboard: undefined; + Workouts: undefined; + History: undefined; + Settings: undefined; +}; + +export type WorkoutStackParamList = { + WorkoutList: undefined; + Builder: { templateId?: string } | undefined; + Templates: undefined; + PreSwim: { workoutId: string }; +}; + +export type HistoryStackParamList = { + HistoryList: undefined; + SessionDetail: { sessionId: string }; + Stats: undefined; +}; + +export type RootStackProps = + NativeStackScreenProps; + +export type WorkoutStackProps = + NativeStackScreenProps; + +export type HistoryStackProps = + NativeStackScreenProps; diff --git a/src/providers/DatabaseProvider.tsx b/src/providers/DatabaseProvider.tsx new file mode 100644 index 0000000..aa568fb --- /dev/null +++ b/src/providers/DatabaseProvider.tsx @@ -0,0 +1,10 @@ +import { SQLiteProvider } from "expo-sqlite"; +import { DATABASE_NAME } from "../db/client"; + +export function DatabaseProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/providers/WatchProvider.tsx b/src/providers/WatchProvider.tsx new file mode 100644 index 0000000..7cfc5b4 --- /dev/null +++ b/src/providers/WatchProvider.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; +import { MMKV } from "react-native-mmkv"; + +const storage = new MMKV(); + +type SettingsContextType = { + poolLength: number; + setPoolLength: (length: number) => void; +}; + +const SettingsContext = createContext({ + poolLength: 25, + setPoolLength: () => {}, +}); + +export function useSettings() { + return useContext(SettingsContext); +} + +export function SettingsProvider({ children }: { children: ReactNode }) { + const [poolLength, setPoolLengthState] = useState(() => { + const stored = storage.getNumber("poolLength"); + return stored ?? 25; + }); + + const setPoolLength = (length: number) => { + setPoolLengthState(length); + storage.set("poolLength", length); + }; + + return ( + + {children} + + ); +} diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..867c174 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1 @@ +export type WorkoutSource = "watch" | "manual"; diff --git a/src/types/workout.ts b/src/types/workout.ts new file mode 100644 index 0000000..9d5ee42 --- /dev/null +++ b/src/types/workout.ts @@ -0,0 +1,28 @@ +export type StrokeType = "Freestyle" | "Backstroke" | "Breaststroke" | "Butterfly" | "Mixed"; + +export type RestType = "fixed_rest" | "leave_time" | "none"; + +export type WorkoutSet = { + stroke: StrokeType; + distancePerRep: number; + repCount: number; + restType: RestType; + restDuration: number; + targetTime?: number; +}; + +export type WorkoutPlan = { + name?: string; + poolLength: number; + sets: WorkoutSet[]; +}; + +export type LapData = { + setPosition: number; + repNumber: number; + distance: number; + duration: number; + strokeCount?: number; + heartRate?: number; + swolf?: number; +}; diff --git a/src/utils/calculatePace.ts b/src/utils/calculatePace.ts new file mode 100644 index 0000000..30894ad --- /dev/null +++ b/src/utils/calculatePace.ts @@ -0,0 +1,15 @@ +import { type WorkoutSet } from "@types/workout"; + +export function calculateTotalDistance(sets: WorkoutSet[]): number { + return sets.reduce((acc, set) => acc + set.distancePerRep * set.repCount, 0); +} + +export function calculateTotalDuration(sets: WorkoutSet[]): number { + return sets.reduce((acc, set) => { + const swimTime = set.targetTime + ? set.targetTime * set.repCount + : 0; + const restTime = set.restType === "none" ? 0 : set.restDuration * (set.repCount - 1); + return acc + swimTime + restTime; + }, 0); +} diff --git a/src/utils/calculateSwolf.ts b/src/utils/calculateSwolf.ts new file mode 100644 index 0000000..d5d09b6 --- /dev/null +++ b/src/utils/calculateSwolf.ts @@ -0,0 +1,15 @@ +export function calculateSwolf(strokeCount: number, durationSeconds: number): number { + return strokeCount + Math.round(durationSeconds); +} + +export function calculateAvgSwolf( + laps: { strokeCount?: number; duration: number }[] +): number { + const validLaps = laps.filter((l) => l.strokeCount != null); + if (validLaps.length === 0) return 0; + const total = validLaps.reduce( + (acc, l) => acc + calculateSwolf(l.strokeCount!, l.duration), + 0 + ); + return Math.round(total / validLaps.length); +} diff --git a/src/utils/formatDistance.ts b/src/utils/formatDistance.ts new file mode 100644 index 0000000..1bddef8 --- /dev/null +++ b/src/utils/formatDistance.ts @@ -0,0 +1,6 @@ +export function formatDistance(meters: number): string { + if (meters >= 1000) { + return `${(meters / 1000).toFixed(1)}km`; + } + return `${meters}m`; +} diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts new file mode 100644 index 0000000..c1535d0 --- /dev/null +++ b/src/utils/formatTime.ts @@ -0,0 +1,18 @@ +export function formatTime(seconds: number): string { + const hrs = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hrs > 0) { + return `${hrs}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; + } + return `${mins}:${String(secs).padStart(2, "0")}`; +} + +export function formatPace(distanceMeters: number, durationSeconds: number): string { + if (distanceMeters === 0) return "--:--"; + const paceSeconds = (durationSeconds / distanceMeters) * 100; + const mins = Math.floor(paceSeconds / 60); + const secs = Math.floor(paceSeconds % 60); + return `${mins}:${String(secs).padStart(2, "0")}/100m`; +} diff --git a/targets/watch/Assets.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/targets/watch/Assets.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..3d6c9ef Binary files /dev/null and b/targets/watch/Assets.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/targets/watch/Assets.xcassets/AppIcon.appiconset/Contents.json b/targets/watch/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..50936f9 --- /dev/null +++ b/targets/watch/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "size": "1024x1024", + "platform": "watchos" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/targets/watch/Connectivity/PhoneConnector.swift b/targets/watch/Connectivity/PhoneConnector.swift new file mode 100644 index 0000000..760608d --- /dev/null +++ b/targets/watch/Connectivity/PhoneConnector.swift @@ -0,0 +1,70 @@ +import Foundation +import WatchConnectivity + +class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate { + static let shared = PhoneConnector() + private var workoutManager: WorkoutManager? + + private override init() { + super.init() + } + + func setup(workoutManager: WorkoutManager) { + self.workoutManager = workoutManager + if WCSession.isSupported() { + let session = WCSession.default + session.delegate = self + session.activate() + } + } + + // Send completed workout data to phone + static func sendCompletedWorkout(_ laps: [[String: Any]], duration: TimeInterval, workoutId: String) { + let message: [String: Any] = [ + "type": "workoutCompleted", + "workoutId": workoutId, + "laps": laps, + "duration": duration, + ] + + if WCSession.default.isReachable { + WCSession.default.sendMessage(message, replyHandler: nil) { error in + print("Failed to send: \(error)") + } + } else { + WCSession.default.transferUserInfo(message) + } + } + + // MARK: - WCSessionDelegate + + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + print("WatchConnectivity activated: \(activationState.rawValue)") + } + + func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + DispatchQueue.main.async { [weak self] in + self?.handleMessage(message) + } + } + + func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + DispatchQueue.main.async { [weak self] in + self?.handleMessage(userInfo) + } + } + + private func handleMessage(_ message: [String: Any]) { + guard let type = message["type"] as? String else { return } + + switch type { + case "workoutPlan": + if let data = try? JSONSerialization.data(withJSONObject: message), + let workout = try? JSONDecoder().decode(StoredWorkout.self, from: data) { + workoutManager?.addWorkoutFromPhone(workout) + } + default: + break + } + } +} diff --git a/targets/watch/ContentView.swift b/targets/watch/ContentView.swift new file mode 100644 index 0000000..024a84f --- /dev/null +++ b/targets/watch/ContentView.swift @@ -0,0 +1,99 @@ +import SwiftUI + +struct ContentView: View { + @StateObject private var workoutManager = WorkoutManager() + + var body: some View { + NavigationView { + VStack(spacing: 16) { + if workoutManager.isWorkoutActive { + ActiveWorkoutView(workoutManager: workoutManager) + } else { + WorkoutListView(workoutManager: workoutManager) + } + } + .navigationTitle("SwimBuddy") + } + } +} + +struct WorkoutListView: View { + @ObservedObject var workoutManager: WorkoutManager + + var body: some View { + List { + if workoutManager.savedWorkouts.isEmpty { + Section { + VStack(spacing: 12) { + Image(systemName: "figure.pool.swim") + .font(.system(size: 40)) + .foregroundColor(.cyan) + Text("No workouts yet") + .font(.headline) + .foregroundColor(.secondary) + Text("Create a workout on your iPhone to get started") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + } + } else { + Section("Saved Workouts") { + ForEach(workoutManager.savedWorkouts) { (workout: StoredWorkout) in + Button { + workoutManager.selectWorkout(workout) + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(workout.name ?? "Workout") + .font(.headline) + .foregroundColor(.primary) + Text("\(workout.totalDistance)m · \(workout.sets.count) sets") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.secondary) + .font(.caption) + } + } + } + } + + if workoutManager.completedWorkouts.count > 0 { + Section("Recent") { + ForEach(workoutManager.completedWorkouts.prefix(3)) { workout in + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(workout.name ?? "Workout") + .font(.subheadline) + .foregroundColor(.primary) + Text(workout.completedAt ?? "") + .font(.caption2) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .font(.caption) + } + } + } + } + } + } + .listStyle(.grouped) + .onAppear { + workoutManager.loadSavedWorkouts() + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/targets/watch/Info.plist b/targets/watch/Info.plist new file mode 100644 index 0000000..f683276 --- /dev/null +++ b/targets/watch/Info.plist @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/targets/watch/Views/ActiveWorkoutView.swift b/targets/watch/Views/ActiveWorkoutView.swift new file mode 100644 index 0000000..c90fe25 --- /dev/null +++ b/targets/watch/Views/ActiveWorkoutView.swift @@ -0,0 +1,109 @@ +import SwiftUI + +struct ActiveWorkoutView: View { + @ObservedObject var workoutManager: WorkoutManager + + var body: some View { + VStack(spacing: 8) { + if workoutManager.isResting { + RestView(workoutManager: workoutManager) + } else { + SwimView(workoutManager: workoutManager) + } + } + .padding() + } +} + +struct SwimView: View { + @ObservedObject var workoutManager: WorkoutManager + + var body: some View { + VStack(spacing: 6) { + if let workout = workoutManager.selectedWorkout, + workoutManager.currentSetIndex < workout.sets.count { + let currentSet = workout.sets[workoutManager.currentSetIndex] + + Text("Set \(workoutManager.currentSetIndex + 1)/\(workout.sets.count)") + .font(.caption2) + .foregroundColor(.secondary) + + Text(currentSet.stroke) + .font(.headline) + .foregroundColor(.cyan) + + Text("Rep \(workoutManager.currentRep + 1)/\(currentSet.repCount)") + .font(.subheadline) + + Text("\(currentSet.distancePerRep)m") + .font(.system(size: 36, weight: .bold, design: .rounded)) + .foregroundColor(.cyan) + } + + Text(formatTime(workoutManager.elapsedSeconds)) + .font(.system(.title, design: .monospaced)) + + if workoutManager.heartRate > 0 { + HStack(spacing: 4) { + Image(systemName: "heart.fill") + .foregroundColor(.red) + .font(.caption) + Text("\(Int(workoutManager.heartRate))") + .font(.caption) + } + } + + Button { + workoutManager.completeLap() + } label: { + Text("Complete Lap") + .font(.headline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .tint(.cyan) + + Button("End") { + workoutManager.endWorkout() + } + .font(.caption) + .foregroundColor(.red) + } + } + + private func formatTime(_ seconds: TimeInterval) -> String { + let mins = Int(seconds) / 60 + let secs = Int(seconds) % 60 + return String(format: "%d:%02d", mins, secs) + } +} + +struct RestView: View { + @ObservedObject var workoutManager: WorkoutManager + + var body: some View { + VStack(spacing: 12) { + Text("Rest") + .font(.headline) + .foregroundColor(.secondary) + + Text(formatTime(workoutManager.restTimeRemaining)) + .font(.system(size: 44, weight: .bold, design: .monospaced)) + .foregroundColor(.cyan) + + if let workout = workoutManager.selectedWorkout, + workoutManager.currentSetIndex + 1 < workout.sets.count { + let nextSet = workout.sets[workoutManager.currentSetIndex + 1] + Text("Next: \(nextSet.stroke) \(nextSet.distancePerRep)m") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + + private func formatTime(_ seconds: TimeInterval) -> String { + let mins = Int(seconds) / 60 + let secs = Int(seconds) % 60 + return String(format: "%d:%02d", mins, secs) + } +} diff --git a/targets/watch/WorkoutManager.swift b/targets/watch/WorkoutManager.swift new file mode 100644 index 0000000..fd68606 --- /dev/null +++ b/targets/watch/WorkoutManager.swift @@ -0,0 +1,283 @@ +import Foundation +import SwiftUI +import HealthKit +import WatchKit +import WatchConnectivity + +class WorkoutManager: ObservableObject { + @Published var isWorkoutActive = false + @Published var savedWorkouts: [StoredWorkout] = [] + @Published var completedWorkouts: [StoredWorkout] = [] + @Published var selectedWorkout: StoredWorkout? + @Published var currentSetIndex = 0 + @Published var currentRep = 0 + @Published var currentLapDistance = 0 + @Published var elapsedSeconds: TimeInterval = 0 + @Published var heartRate: Double = 0 + @Published var isResting = false + @Published var restTimeRemaining: TimeInterval = 0 + + private var workoutTimer: Timer? + private var restTimer: Timer? + private var healthStore: HKHealthStore? + private var activeWorkout: HKWorkoutSession? + private var builder: HKWorkoutBuilder? + private var lapStartTime: Date? + private var completedLaps: [CompletedLap] = [] + + private let suiteName = "group.com.swimbuddy.app" + + init() { + healthStore = HKHealthStore() + } + + // MARK: - Data Persistence + + func loadSavedWorkouts() { + guard let defaults = UserDefaults(suiteName: suiteName) else { return } + + if let data = defaults.data(forKey: "savedWorkouts"), + let workouts = try? JSONDecoder().decode([StoredWorkout].self, from: data) { + savedWorkouts = workouts + } + + if let data = defaults.data(forKey: "completedWorkouts"), + let workouts = try? JSONDecoder().decode([StoredWorkout].self, from: data) { + completedWorkouts = workouts + } + } + + func saveWorkoutsToStorage() { + guard let defaults = UserDefaults(suiteName: suiteName) else { return } + if let data = try? JSONEncoder().encode(savedWorkouts) { + defaults.set(data, forKey: "savedWorkouts") + } + if let data = try? JSONEncoder().encode(completedWorkouts) { + defaults.set(data, forKey: "completedWorkouts") + } + } + + func addWorkoutFromPhone(_ plan: StoredWorkout) { + savedWorkouts.append(plan) + saveWorkoutsToStorage() + } + + // MARK: - Workout Selection + + func selectWorkout(_ workout: StoredWorkout) { + selectedWorkout = workout + } + + // MARK: - Workout Control + + func startWorkout() { + guard selectedWorkout != nil else { return } + + isWorkoutActive = true + currentSetIndex = 0 + currentRep = 0 + elapsedSeconds = 0 + completedLaps = [] + + startHealthKitWorkout() + startTimer() + WKInterfaceDevice.current().play(.start) + } + + func pauseWorkout() { + workoutTimer?.invalidate() + WKInterfaceDevice.current().play(.notification) + } + + func resumeWorkout() { + startTimer() + WKInterfaceDevice.current().play(.start) + } + + func endWorkout() { + workoutTimer?.invalidate() + restTimer?.invalidate() + isWorkoutActive = false + + endHealthKitWorkout() + syncCompletedWorkout() + WKInterfaceDevice.current().play(.success) + } + + // MARK: - Lap Tracking + + func completeLap() { + guard let workout = selectedWorkout else { return } + + let now = Date() + let duration = lapStartTime.map { now.timeIntervalSince($0) } ?? 0 + + let lap = CompletedLap( + setPosition: currentSetIndex + 1, + repNumber: currentRep + 1, + distance: workout.sets[currentSetIndex].distancePerRep, + duration: duration, + heartRate: Int(heartRate) + ) + completedLaps.append(lap) + + WKInterfaceDevice.current().play(.click) + + advanceToNextRep() + } + + private func advanceToNextRep() { + guard let workout = selectedWorkout else { return } + let currentSet = workout.sets[currentSetIndex] + + if currentRep + 1 < currentSet.repCount { + currentRep += 1 + lapStartTime = Date() + } else if currentSetIndex + 1 < workout.sets.count { + startRestPeriod() + } else { + endWorkout() + } + } + + // MARK: - Rest Period + + private func startRestPeriod() { + guard let workout = selectedWorkout else { return } + let currentSet = workout.sets[currentSetIndex] + + guard currentSet.restType != "none", currentSet.restDuration > 0 else { + moveToNextSet() + return + } + + isResting = true + restTimeRemaining = currentSet.restDuration + + WKInterfaceDevice.current().play(.notification) + + restTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.restTimeRemaining -= 1 + + if self.restTimeRemaining <= 0 { + self.restTimer?.invalidate() + self.moveToNextSet() + } + } + } + + private func moveToNextSet() { + isResting = false + currentSetIndex += 1 + currentRep = 0 + lapStartTime = Date() + + if currentSetIndex >= (selectedWorkout?.sets.count ?? 0) { + endWorkout() + } else { + WKInterfaceDevice.current().play(.start) + } + } + + // MARK: - Timer + + private func startTimer() { + lapStartTime = Date() + workoutTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + self?.elapsedSeconds += 1 + } + } + + // MARK: - HealthKit + + private func startHealthKitWorkout() { + guard let healthStore = healthStore else { return } + + let config = HKWorkoutConfiguration() + config.activityType = .swimming + config.locationType = .indoor + + do { + activeWorkout = try HKWorkoutSession(healthStore: healthStore, configuration: config) + let workoutBuilder = HKWorkoutBuilder(healthStore: healthStore, configuration: config, device: nil) + self.builder = workoutBuilder + activeWorkout?.startActivity(with: Date()) + workoutBuilder.beginCollection(withStart: Date()) { _, _ in } + } catch { + print("Failed to start HealthKit workout: \(error)") + } + } + + private func endHealthKitWorkout() { + activeWorkout?.end() + builder?.endCollection(withEnd: Date()) { [weak self] _, _ in + self?.builder?.finishWorkout { _, _ in } + } + } + + // MARK: - Sync to Phone + + private func syncCompletedWorkout() { + let workoutSummary: [String: Any] = [ + "type": "workoutCompleted", + "workoutId": selectedWorkout?.id ?? "", + "laps": completedLaps.map { lap in + [ + "setPosition": lap.setPosition, + "repNumber": lap.repNumber, + "distance": lap.distance, + "duration": lap.duration, + "heartRate": lap.heartRate ?? 0, + ] as [String: Any] + }, + "duration": elapsedSeconds, + ] + + if let defaults = UserDefaults(suiteName: suiteName) { + var completed = completedWorkouts + var summary = selectedWorkout + summary?.completedAt = ISO8601DateFormatter().string(from: Date()) + if let s = summary { + completed.insert(s, at: 0) + } + if let data = try? JSONEncoder().encode(completed) { + defaults.set(data, forKey: "completedWorkouts") + } + } + + if WCSession.default.isReachable { + WCSession.default.sendMessage(workoutSummary, replyHandler: nil) { error in + print("Failed to sync to phone: \(error)") + } + } else { + WCSession.default.transferUserInfo(workoutSummary) + } + } +} + +// MARK: - Models + +struct StoredWorkout: Codable, Identifiable { + let id: String + let name: String? + let poolLength: Double + let sets: [StoredSet] + var completedAt: String? +} + +struct StoredSet: Codable { + let stroke: String + let distancePerRep: Int + let repCount: Int + let restType: String + let restDuration: TimeInterval +} + +struct CompletedLap { + let setPosition: Int + let repNumber: Int + let distance: Int + let duration: TimeInterval + let heartRate: Int? +} diff --git a/targets/watch/expo-target.config.js b/targets/watch/expo-target.config.js new file mode 100644 index 0000000..3fd80cb --- /dev/null +++ b/targets/watch/expo-target.config.js @@ -0,0 +1,10 @@ +/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */ +module.exports = (config) => ({ + type: "watch", + icon: "../../assets/icon.png", + deploymentTarget: "10.0", + entitlements: { + "com.apple.security.application-groups": + config.ios?.entitlements?.["com.apple.security.application-groups"] ?? [], + }, +}); diff --git a/targets/watch/index.swift b/targets/watch/index.swift new file mode 100644 index 0000000..5e05600 --- /dev/null +++ b/targets/watch/index.swift @@ -0,0 +1,16 @@ +import SwiftUI + +@main +struct SwimTrackerWatchApp: App { + @StateObject private var workoutManager = WorkoutManager() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(workoutManager) + .onAppear { + PhoneConnector.shared.setup(workoutManager: workoutManager) + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json index b9567f6..1b67e75 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,17 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { - "strict": true - } + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@features/*": ["./src/features/*"], + "@components/*": ["./src/components/*"], + "@hooks/*": ["./src/hooks/*"], + "@utils/*": ["./src/utils/*"], + "@constants/*": ["./src/constants/*"], + "@db/*": ["./src/db/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"] }